git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH] submodule merge: update conflict error message
@ 2022-06-06 23:54 Calvin Wan
  2022-06-07  0:48 ` Junio C Hamano
  2022-06-10 23:11 ` [PATCH v2] " Calvin Wan
  0 siblings, 2 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-06 23:54 UTC (permalink / raw)
  To: git; +Cc: chooglen, Calvin Wan

When attempting to do a non-fast-forward merge in a project with
conflicts in the submodules, the merge fails and git prints the
following error:

Failed to merge submodule <submodules>
CONFLICT (submodule):Merge conflict in <submodule>
Automatic merge failed; fix conflicts and then commit the result.

Git is left in a conflicted state, which requires the user to:
 1. abort the merge
 2. merge submodules
 3. merge superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers. 

Update error message to the following when attempting to do a
non-fast-forward merge in a project with conflicts in the submodules.
The error message is based off of what would happen when `merge
--recurse-submodules` is eventually supported

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; recursive merging with submodules is currently
not supported. To manually merge, the following steps are recommended:
 - abort the current merge
 - merge submodules individually
 - merge superproject

I considered automatically aborting the merge if git detects the merge
failed because of a submodule conflict, however, doing so causes a
significant amount of tests in `t7610-mergetool.sh` (and some other test
scripts as well) to fail, suggesting users have come to expect this
state and have their workarounds with `git mergetool`

Signed-off-by: Calvin Wan <calvinwan@google.com>

---
 builtin/merge.c            | 12 +++++++++++-
 merge-ort.c                |  4 +++-
 merge-recursive.c          |  4 +++-
 merge-recursive.h          |  1 +
 t/t6437-submodule-merge.sh |  5 ++++-
 5 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/builtin/merge.c b/builtin/merge.c
index f178f5a3ee..39f5ee66d6 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -88,6 +88,7 @@ static const char *sign_commit;
 static int autostash;
 static int no_verify;
 static char *into_name;
+static int submodule_conflict = 0;
 
 static struct strategy all_strategy[] = {
 	{ "recursive",  NO_TRIVIAL },
@@ -757,6 +758,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
 		else
 			clean = merge_recursive(&o, head, remoteheads->item,
 						reversed, &result);
+		if (o.submodule_conflict)
+			submodule_conflict = 1;
 		if (clean < 0)
 			exit(128);
 		if (write_locked_index(&the_index, &lock,
@@ -973,7 +976,14 @@ static int suggest_conflicts(void)
 	strbuf_release(&msgbuf);
 	fclose(fp);
 	repo_rerere(the_repository, allow_rerere_auto);
-	printf(_("Automatic merge failed; "
+	if (submodule_conflict)
+		printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
+			"not supported. To manually merge, the following steps are recommended:\n"
+			" - abort the current merge\n"
+			" - merge submodules individually\n"
+			" - merge superproject\n"));
+	else
+		printf(_("Automatic merge failed; "
 			"fix conflicts and then commit the result.\n"));
 	return 1;
 }
diff --git a/merge-ort.c b/merge-ort.c
index 0d3f42592f..205d6658bc 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -3866,8 +3866,10 @@ static void process_entry(struct merge_options *opt,
 			const char *reason = _("content");
 			if (ci->filemask == 6)
 				reason = _("add/add");
-			if (S_ISGITLINK(merged_file.mode))
+			if (S_ISGITLINK(merged_file.mode)) {
 				reason = _("submodule");
+				opt->submodule_conflict = 1;
+			}
 			path_msg(opt, path, 0,
 				 _("CONFLICT (%s): Merge conflict in %s"),
 				 reason, path);
diff --git a/merge-recursive.c b/merge-recursive.c
index fd1bbde061..535b8cc758 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -3149,8 +3149,10 @@ static int handle_content_merge(struct merge_file_info *mfi,
 	}
 
 	if (!mfi->clean) {
-		if (S_ISGITLINK(mfi->blob.mode))
+		if (S_ISGITLINK(mfi->blob.mode)) {
 			reason = _("submodule");
+			opt->submodule_conflict = 1;
+		}
 		output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
 				reason, path);
 		if (ci && !df_conflict_remains)
diff --git a/merge-recursive.h b/merge-recursive.h
index b88000e3c2..6fd31644ad 100644
--- a/merge-recursive.h
+++ b/merge-recursive.h
@@ -51,6 +51,7 @@ struct merge_options {
 
 	/* internal fields used by the implementation */
 	struct merge_options_internal *priv;
+	unsigned submodule_conflict : 1;
 };
 
 void init_merge_options(struct merge_options *opt, struct repository *repo);
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index 178413c22f..bfcc81cd06 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
 		test_must_fail git merge c 2> actual
 	  fi &&
 	 grep $(cat expect) actual > /dev/null &&
+	 test_i18ngrep "recursive merging with submodules is currently" actual &&
 	 git reset --hard)
 '
 
@@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 fi &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
+	test_i18ngrep "recursive merging with submodules is currently" actual &&
 	git reset --hard)
 '
 
@@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	test_i18ngrep "recursive merging with submodules is currently" actual)
 '
 
 

base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
-- 
2.36.1.255.ge46751e96f-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH] submodule merge: update conflict error message
  2022-06-06 23:54 [PATCH] submodule merge: update conflict error message Calvin Wan
@ 2022-06-07  0:48 ` Junio C Hamano
  2022-06-08 17:19   ` Calvin Wan
  2022-06-10 23:11 ` [PATCH v2] " Calvin Wan
  1 sibling, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-06-07  0:48 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, chooglen

Calvin Wan <calvinwan@google.com> writes:

> When attempting to do a non-fast-forward merge in a project with
> conflicts in the submodules, the merge fails and git prints the
> following error:
>
> Failed to merge submodule <submodules>
> CONFLICT (submodule):Merge conflict in <submodule>
> Automatic merge failed; fix conflicts and then commit the result.
>
> Git is left in a conflicted state, which requires the user to:
>  1. abort the merge
>  2. merge submodules
>  3. merge superproject
> These steps are non-obvious for newer submodule users to figure out

Hmph.  Is 1. necessary?

IOW, based on the information we already have (we may not be
surfacing, which can be corrected), wouldn't it be easier to instead
(A) go to submodule and make a merge and then (B) come back to the
superproject, "git add <submodule" to record the result of submodule
merge, and say "git commit" to conclude?

The thing I am worried most about is that you may be throwing away
information that would help the user by aborting the superproject
merge.  Before doing so, you have stage #2 and stage #3 of the
submodule commit, so which commits in the submodule you need to
merge in (A) above should be fairly clear.  If you abort the merge
first, how does the user know which commits in the submodule the
user needs to merge?

> The error message is based off of what would happen when `merge
> --recurse-submodules` is eventually supported

OK.

> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Automatic merge failed; recursive merging with submodules is currently
> not supported. To manually merge, the following steps are recommended:
>  - abort the current merge
>  - merge submodules individually
>  - merge superproject

Again, I am not sure about the recommendation.  The message saying
"currently not supported" I think is a good idea.

> I considered automatically aborting the merge if git detects the merge
> failed because of a submodule conflict, however, doing so causes a
> significant amount of tests in `t7610-mergetool.sh` (and some other test
> scripts as well) to fail, suggesting users have come to expect this
> state and have their workarounds with `git mergetool`

With or without test failures, my gut feeling sais that it cannot be
a good idea to automatically abort the merge, without first grabbing
some information out of the conflicted state.

Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH] submodule merge: update conflict error message
  2022-06-07  0:48 ` Junio C Hamano
@ 2022-06-08 17:19   ` Calvin Wan
  2022-06-08 17:34     ` Glen Choo
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-06-08 17:19 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, chooglen

> Hmph.  Is 1. necessary?

I just tested it and it is not, so I do agree recommending to abort the
merge is unnecessary/bad advice. How does this sound?

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; recursive merging with submodules is currently
not supported. To manually merge, merge conflicted submodules first
before merging the superproject.

On Mon, Jun 6, 2022 at 5:48 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Calvin Wan <calvinwan@google.com> writes:
>
> > When attempting to do a non-fast-forward merge in a project with
> > conflicts in the submodules, the merge fails and git prints the
> > following error:
> >
> > Failed to merge submodule <submodules>
> > CONFLICT (submodule):Merge conflict in <submodule>
> > Automatic merge failed; fix conflicts and then commit the result.
> >
> > Git is left in a conflicted state, which requires the user to:
> >  1. abort the merge
> >  2. merge submodules
> >  3. merge superproject
> > These steps are non-obvious for newer submodule users to figure out
>
> Hmph.  Is 1. necessary?
>
> IOW, based on the information we already have (we may not be
> surfacing, which can be corrected), wouldn't it be easier to instead
> (A) go to submodule and make a merge and then (B) come back to the
> superproject, "git add <submodule" to record the result of submodule
> merge, and say "git commit" to conclude?
>
> The thing I am worried most about is that you may be throwing away
> information that would help the user by aborting the superproject
> merge.  Before doing so, you have stage #2 and stage #3 of the
> submodule commit, so which commits in the submodule you need to
> merge in (A) above should be fairly clear.  If you abort the merge
> first, how does the user know which commits in the submodule the
> user needs to merge?
>
> > The error message is based off of what would happen when `merge
> > --recurse-submodules` is eventually supported
>
> OK.
>
> > Failed to merge submodule <submodule>
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Automatic merge failed; recursive merging with submodules is currently
> > not supported. To manually merge, the following steps are recommended:
> >  - abort the current merge
> >  - merge submodules individually
> >  - merge superproject
>
> Again, I am not sure about the recommendation.  The message saying
> "currently not supported" I think is a good idea.
>
> > I considered automatically aborting the merge if git detects the merge
> > failed because of a submodule conflict, however, doing so causes a
> > significant amount of tests in `t7610-mergetool.sh` (and some other test
> > scripts as well) to fail, suggesting users have come to expect this
> > state and have their workarounds with `git mergetool`
>
> With or without test failures, my gut feeling sais that it cannot be
> a good idea to automatically abort the merge, without first grabbing
> some information out of the conflicted state.
>
> Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH] submodule merge: update conflict error message
  2022-06-08 17:19   ` Calvin Wan
@ 2022-06-08 17:34     ` Glen Choo
  2022-06-08 18:01       ` Calvin Wan
  0 siblings, 1 reply; 60+ messages in thread
From: Glen Choo @ 2022-06-08 17:34 UTC (permalink / raw)
  To: Calvin Wan, Junio C Hamano; +Cc: git

Calvin Wan <calvinwan@google.com> writes:

>> Hmph.  Is 1. necessary?
>
> I just tested it and it is not, so I do agree recommending to abort the
> merge is unnecessary/bad advice. How does this sound?
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Automatic merge failed; recursive merging with submodules is currently
> not supported. To manually merge, merge conflicted submodules first
> before merging the superproject.

This message sounds ok to me, since this is probably what the user wants
90% of the time. Since we don't abort the merge, this just a 'regular'
merge conflict resolution (albeit with submodules). The user probably
wants to merge the submodules, but they can choose however they want to
resolve the merge conflict, e.g. maybe they'd prefer to just pick one
side (or even more exotically, a different commit altogether.)

An improvement for that other 10% would be to print this help message
with the advice() API so that users can turn it off if they don't find
it helpful. Or maybe it's confusing to some new users who use a
different merging workflow and so an admin might turn off this advice
for them.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH] submodule merge: update conflict error message
  2022-06-08 17:34     ` Glen Choo
@ 2022-06-08 18:01       ` Calvin Wan
  2022-06-08 19:13         ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-06-08 18:01 UTC (permalink / raw)
  To: Glen Choo; +Cc: Junio C Hamano, git

> The user probably
> wants to merge the submodules, but they can choose however they want to
> resolve the merge conflict

It sounds like I should reword "merge conflicted submodules" to
"resolve conflicted submodules". That should cover those 10% cases.

I would prefer to find a generic, but still helpful message that doesn't
require going into the advice() API or require some config change

On Wed, Jun 8, 2022 at 10:35 AM Glen Choo <chooglen@google.com> wrote:
>
> Calvin Wan <calvinwan@google.com> writes:
>
> >> Hmph.  Is 1. necessary?
> >
> > I just tested it and it is not, so I do agree recommending to abort the
> > merge is unnecessary/bad advice. How does this sound?
> >
> > Failed to merge submodule <submodule>
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Automatic merge failed; recursive merging with submodules is currently
> > not supported. To manually merge, merge conflicted submodules first
> > before merging the superproject.
>
> This message sounds ok to me, since this is probably what the user wants
> 90% of the time. Since we don't abort the merge, this just a 'regular'
> merge conflict resolution (albeit with submodules). The user probably
> wants to merge the submodules, but they can choose however they want to
> resolve the merge conflict, e.g. maybe they'd prefer to just pick one
> side (or even more exotically, a different commit altogether.)
>
> An improvement for that other 10% would be to print this help message
> with the advice() API so that users can turn it off if they don't find
> it helpful. Or maybe it's confusing to some new users who use a
> different merging workflow and so an admin might turn off this advice
> for them.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH] submodule merge: update conflict error message
  2022-06-08 18:01       ` Calvin Wan
@ 2022-06-08 19:13         ` Junio C Hamano
  0 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-06-08 19:13 UTC (permalink / raw)
  To: Calvin Wan; +Cc: Glen Choo, git

Calvin Wan <calvinwan@google.com> writes:

>> The user probably
>> wants to merge the submodules, but they can choose however they want to
>> resolve the merge conflict
>
> It sounds like I should reword "merge conflicted submodules" to
> "resolve conflicted submodules". That should cover those 10% cases.
>
> I would prefer to find a generic, but still helpful message that doesn't
> require going into the advice() API or require some config change

We want the users not to blow away the half-merged state in the
working tree.  We are guiding them to first go into submodules and
merge (in which case, we should tell them merge what with what---I
think the first parent should be what they have checked out there,
but the other parent, which is what is recorded in the tree of the
superproject commit being merged as gitlink, may not be at the tip
of any branch you have in the submodule).  And then they come back
to the superproject and resolve the conflict in the working tree and
the index.

> > Failed to merge submodule <submodule>
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Automatic merge failed; recursive merging with submodules is currently
> > not supported. To manually merge, merge conflicted submodules first
> > before merging the superproject.

So,

    to manually complete the merge:
    - go to submodule A, and merge commit a24c4e37d0
    - go to submodule B, and merge commit a6f14c960b
    - come back to superproject, and "git add A B" to record the above merge
    - in superproject, resolve the other conflicts
    - commit the resulting index in the superproject

or something along that line, perhaps?




^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v2] submodule merge: update conflict error message
  2022-06-06 23:54 [PATCH] submodule merge: update conflict error message Calvin Wan
  2022-06-07  0:48 ` Junio C Hamano
@ 2022-06-10 23:11 ` Calvin Wan
  2022-06-11  4:53   ` Elijah Newren
  2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
  1 sibling, 2 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-10 23:11 UTC (permalink / raw)
  To: git; +Cc: chooglen, gitster, Calvin Wan

When attempting to do a non-fast-forward merge in a project with
conflicts in the submodules, the merge fails and git prints the
following error:

Failed to merge submodule <submodules>
CONFLICT (submodule):Merge conflict in <submodule>
Automatic merge failed; fix conflicts and then commit the result.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules
 2. add submodules changes to the superproject
 3. merge superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers. 

Update error message to the following when attempting to do a
non-fast-forward merge in a project with conflicts in the submodules.
The error message is based off of what would happen when `merge
--recurse-submodules` is eventually supported

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; recursive merging with submodules is currently
not supported. To manually complete the merge:
 - go to submodule (<submodule>), and merge commit <commit>
 - come back to superproject, and `git add <submodule>` to record the above merge 
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject

Changes since v1:
 - Removed advice to abort merge
 - Error message updated to contain more commit/submodule information

Signed-off-by: Calvin Wan <calvinwan@google.com>

---
 builtin/merge.c            | 23 ++++++++++++++++++++++-
 merge-ort.c                |  7 ++++++-
 merge-recursive.c          |  7 ++++++-
 merge-recursive.h          |  4 ++++
 t/t6437-submodule-merge.sh |  5 ++++-
 5 files changed, 42 insertions(+), 4 deletions(-)

diff --git a/builtin/merge.c b/builtin/merge.c
index f178f5a3ee..7e2deea7fb 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -88,6 +88,8 @@ static const char *sign_commit;
 static int autostash;
 static int no_verify;
 static char *into_name;
+static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
+static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
 
 static struct strategy all_strategy[] = {
 	{ "recursive",  NO_TRIVIAL },
@@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
 		}
 
 		init_merge_options(&o, the_repository);
+		o.conflicted_submodule_oids = &conflicted_submodule_oids;
+		o.conflicted_submodule_paths = &conflicted_submodule_paths;
 		if (!strcmp(strategy, "subtree"))
 			o.subtree_shift = "";
 
@@ -973,8 +977,25 @@ static int suggest_conflicts(void)
 	strbuf_release(&msgbuf);
 	fclose(fp);
 	repo_rerere(the_repository, allow_rerere_auto);
-	printf(_("Automatic merge failed; "
+	if (conflicted_submodule_oids.nr > 0) {
+		int i;
+		printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
+			"not supported. To manually complete the merge:\n"));
+		for (i = 0; i < conflicted_submodule_oids.nr; i++) {
+			printf(_(" - go to submodule (%s), and merge commit %s\n"),
+				conflicted_submodule_paths.items[i].string,
+				oid_to_hex(&conflicted_submodule_oids.oid[i]));
+		}
+		printf(_(" - come back to superproject, and `git add"));
+		for (i = 0; i < conflicted_submodule_paths.nr; i++)
+			printf(_(" %s"), conflicted_submodule_paths.items[i].string);
+		printf(_("` to record the above merge \n"
+		" - resolve any other conflicts in the superproject\n"
+		" - commit the resulting index in the superproject\n"));
+	} else {
+		printf(_("Automatic merge failed; "
 			"fix conflicts and then commit the result.\n"));
+	}
 	return 1;
 }
 
diff --git a/merge-ort.c b/merge-ort.c
index 0d3f42592f..c86ee11614 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
 			const char *reason = _("content");
 			if (ci->filemask == 6)
 				reason = _("add/add");
-			if (S_ISGITLINK(merged_file.mode))
+			if (S_ISGITLINK(merged_file.mode)) {
 				reason = _("submodule");
+				if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
+					oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
+					string_list_append(opt->conflicted_submodule_paths, path);
+				}
+			}
 			path_msg(opt, path, 0,
 				 _("CONFLICT (%s): Merge conflict in %s"),
 				 reason, path);
diff --git a/merge-recursive.c b/merge-recursive.c
index fd1bbde061..ff7cdbefe9 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
 	}
 
 	if (!mfi->clean) {
-		if (S_ISGITLINK(mfi->blob.mode))
+		if (S_ISGITLINK(mfi->blob.mode)) {
 			reason = _("submodule");
+			if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
+				oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
+				string_list_append(opt->conflicted_submodule_paths, path);
+			}
+		}
 		output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
 				reason, path);
 		if (ci && !df_conflict_remains)
diff --git a/merge-recursive.h b/merge-recursive.h
index b88000e3c2..5d267e7a43 100644
--- a/merge-recursive.h
+++ b/merge-recursive.h
@@ -51,6 +51,10 @@ struct merge_options {
 
 	/* internal fields used by the implementation */
 	struct merge_options_internal *priv;
+
+	/* fields that hold submodule conflict information */
+	struct oid_array *conflicted_submodule_oids;
+	struct string_list *conflicted_submodule_paths;
 };
 
 void init_merge_options(struct merge_options *opt, struct repository *repo);
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index 178413c22f..5b384dedc1 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
 		test_must_fail git merge c 2> actual
 	  fi &&
 	 grep $(cat expect) actual > /dev/null &&
+	 test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
 	 git reset --hard)
 '
 
@@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 fi &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
+	test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
 	git reset --hard)
 '
 
@@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)
 '
 
 
-- 
2.36.1.476.g0c4daa206d-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-10 23:11 ` [PATCH v2] " Calvin Wan
@ 2022-06-11  4:53   ` Elijah Newren
  2022-06-11 17:08     ` Philippe Blain
                       ` (2 more replies)
  2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
  1 sibling, 3 replies; 60+ messages in thread
From: Elijah Newren @ 2022-06-11  4:53 UTC (permalink / raw)
  To: Calvin Wan; +Cc: Git Mailing List, Glen Choo, Junio C Hamano

On Fri, Jun 10, 2022 at 4:29 PM Calvin Wan <calvinwan@google.com> wrote:
>
> When attempting to do a non-fast-forward merge in a project with
> conflicts in the submodules, the merge fails and git prints the
> following error:
>
> Failed to merge submodule <submodules>
> CONFLICT (submodule):Merge conflict in <submodule>
> Automatic merge failed; fix conflicts and then commit the result.
>
> Git is left in a conflicted state, which requires the user to:
>  1. merge submodules
>  2. add submodules changes to the superproject
>  3. merge superproject

I think we may need to tweak these steps a bit:

   1. merge submodules OR update submodules to an already existing
commit that reflects the merge (i.e. as submodules they may well be
independently actively developed.  Someone may have already merged the
appropriate branch/commit and the already extant merges should be used
in preference to creating new ones.)
   2. <just as you said>
   3. FINISH merging the superproject (i.e. don't redo the merge)

I might be off on step 1; I have only used submodules extremely
lightly and usually only for a limited time, so I'm not really sure
what the expected workflow is.  I could also imagine it potentially
being repository-dependent whether you would want to merge or select
an appropriate commit to update to.

> These steps are non-obvious for newer submodule users to figure out
> based on the error message and neither `git submodule status` nor `git
> status` provide any useful pointers.
>
> Update error message to the following when attempting to do a
> non-fast-forward merge in a project with conflicts in the submodules.

Make sense.

> The error message is based off of what would happen when `merge
> --recurse-submodules` is eventually supported
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Automatic merge failed; recursive merging with submodules is currently
> not supported. To manually complete the merge:
>  - go to submodule (<submodule>), and merge commit <commit>
>  - come back to superproject, and `git add <submodule>` to record the above merge
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject

Ah, I see you've fixed step 3 here; that's good.

However, these steps miss out on the merge-or-update submodule
possibility...and since you mention these steps are potentially the
basis for some future work, I think it's worth calling that out again.
I'm slightly worried that the 'update' part of merge-or-update may
throw a wrench in the plans for `merge --recurse-submodules`.

(Also, continuing on the `merge --recurse-submodules` talent but
discussing a different aspect of it, I'm curious if you need to add
extra dirty-worktree/dirty-index checks for each submodule at the
start of a merge, whether you need to try to lock N indexes before
starting, and what other extra details are necessary.  But those are
probably questions to address whenever work on the future series to
implement this option is underway.)

> Changes since v1:
>  - Removed advice to abort merge
>  - Error message updated to contain more commit/submodule information
>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
>
> ---
>  builtin/merge.c            | 23 ++++++++++++++++++++++-
>  merge-ort.c                |  7 ++++++-
>  merge-recursive.c          |  7 ++++++-
>  merge-recursive.h          |  4 ++++
>  t/t6437-submodule-merge.sh |  5 ++++-
>  5 files changed, 42 insertions(+), 4 deletions(-)

So you're modifying the "git merge" porcelain level (builtin/merge.c),
the two merges strategies, their common header, and adding some tests.
No other porcelains are modified...

> diff --git a/builtin/merge.c b/builtin/merge.c
> index f178f5a3ee..7e2deea7fb 100644
> --- a/builtin/merge.c
> +++ b/builtin/merge.c
> @@ -88,6 +88,8 @@ static const char *sign_commit;
>  static int autostash;
>  static int no_verify;
>  static char *into_name;
> +static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
> +static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
>
>  static struct strategy all_strategy[] = {
>         { "recursive",  NO_TRIVIAL },
> @@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
>                 }
>
>                 init_merge_options(&o, the_repository);
> +               o.conflicted_submodule_oids = &conflicted_submodule_oids;
> +               o.conflicted_submodule_paths = &conflicted_submodule_paths;
>                 if (!strcmp(strategy, "subtree"))
>                         o.subtree_shift = "";
>
> @@ -973,8 +977,25 @@ static int suggest_conflicts(void)
>         strbuf_release(&msgbuf);
>         fclose(fp);
>         repo_rerere(the_repository, allow_rerere_auto);
> -       printf(_("Automatic merge failed; "
> +       if (conflicted_submodule_oids.nr > 0) {
> +               int i;
> +               printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
> +                       "not supported. To manually complete the merge:\n"));
> +               for (i = 0; i < conflicted_submodule_oids.nr; i++) {
> +                       printf(_(" - go to submodule (%s), and merge commit %s\n"),
> +                               conflicted_submodule_paths.items[i].string,
> +                               oid_to_hex(&conflicted_submodule_oids.oid[i]));
> +               }
> +               printf(_(" - come back to superproject, and `git add"));
> +               for (i = 0; i < conflicted_submodule_paths.nr; i++)
> +                       printf(_(" %s"), conflicted_submodule_paths.items[i].string);
> +               printf(_("` to record the above merge \n"
> +               " - resolve any other conflicts in the superproject\n"
> +               " - commit the resulting index in the superproject\n"));
> +       } else {
> +               printf(_("Automatic merge failed; "
>                         "fix conflicts and then commit the result.\n"));
> +       }
>         return 1;
>  }

This is kind of nice.  I was worried you were going to embed these
messages in the merge strategies, which could cause problems for other
users of the merge strategies such as the --remerge-diff options to
git log and git show (your new messages would be unwanted noise or
even cause confusion there), and to the merge-tree work.  In fact, a
current submodule-merging message (search for "--cacheinfo") that is
potentially similar to what you are adding here but which was added at
the merge strategy level already feels highly problematic to me.  I've
been considering nuking it from the codebase for some time because of
those issues, though I guess just moving it out elsewhere may also
work.

However, this implementation does have a drawback: these messages
won't appear for rebases, cherry-picks, reverts, attempted unstashing
(git stash apply/pop), or other actions unless you update the relevant
porcelains for those as well.

A possible alternative here would be to move it to the level of
merge-recursive and merge-ort that is only called when the working
tree and index are updated.  For example, placing it in
merge_finalize() in merge-recursive.c and merge_switch_to_result() in
merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
However, I'm also fine with keeping it at the porcelain level, it just
may need to be in a function that is called from several porcelains
that way.

> diff --git a/merge-ort.c b/merge-ort.c
> index 0d3f42592f..c86ee11614 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
>                         const char *reason = _("content");
>                         if (ci->filemask == 6)
>                                 reason = _("add/add");
> -                       if (S_ISGITLINK(merged_file.mode))
> +                       if (S_ISGITLINK(merged_file.mode)) {
>                                 reason = _("submodule");
> +                               if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> +                                       oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
> +                                       string_list_append(opt->conflicted_submodule_paths, path);
> +                               }
> +                       }
>                         path_msg(opt, path, 0,
>                                  _("CONFLICT (%s): Merge conflict in %s"),
>                                  reason, path);
> diff --git a/merge-recursive.c b/merge-recursive.c
> index fd1bbde061..ff7cdbefe9 100644
> --- a/merge-recursive.c
> +++ b/merge-recursive.c
> @@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
>         }
>
>         if (!mfi->clean) {
> -               if (S_ISGITLINK(mfi->blob.mode))
> +               if (S_ISGITLINK(mfi->blob.mode)) {
>                         reason = _("submodule");
> +                       if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> +                               oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
> +                               string_list_append(opt->conflicted_submodule_paths, path);
> +                       }
> +               }
>                 output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
>                                 reason, path);
>                 if (ci && !df_conflict_remains)

Nice that the changes needed to both the ort and recursive strategies
are so localized.  :-)

> diff --git a/merge-recursive.h b/merge-recursive.h
> index b88000e3c2..5d267e7a43 100644
> --- a/merge-recursive.h
> +++ b/merge-recursive.h
> @@ -51,6 +51,10 @@ struct merge_options {
>
>         /* internal fields used by the implementation */
>         struct merge_options_internal *priv;
> +
> +       /* fields that hold submodule conflict information */
> +       struct oid_array *conflicted_submodule_oids;
> +       struct string_list *conflicted_submodule_paths;
>  };

Make sense.

>  void init_merge_options(struct merge_options *opt, struct repository *repo);
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index 178413c22f..5b384dedc1 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
>                 test_must_fail git merge c 2> actual
>           fi &&
>          grep $(cat expect) actual > /dev/null &&
> +        test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
>          git reset --hard)
>  '
>
> @@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          fi &&
>         grep $(cat expect1) actual > /dev/null &&
>         grep $(cat expect2) actual > /dev/null &&
> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
>         git reset --hard)
>  '
>
> @@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
>         git commit -a -m "f" &&
>
>         git checkout -b test-backward e &&
> -       test_must_fail git merge f)
> +       test_must_fail git merge f >actual &&
> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)

test_i18ngrep is apparently on the way out:

    $ grep -B 3 ^test_i18ngrep t/test-lib-functions.sh
    # Wrapper for grep which used to be used for
    # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other
    # in-flight changes. Should not be used and will be removed soon.
    test_i18ngrep () {

I think you just want to use grep instead here for each of these hunks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-11  4:53   ` Elijah Newren
@ 2022-06-11 17:08     ` Philippe Blain
  2022-06-12  6:56       ` Elijah Newren
  2022-06-13  2:03       ` Calvin Wan
  2022-06-12 23:30     ` Junio C Hamano
  2022-06-13  1:54     ` Calvin Wan
  2 siblings, 2 replies; 60+ messages in thread
From: Philippe Blain @ 2022-06-11 17:08 UTC (permalink / raw)
  To: Elijah Newren, Calvin Wan; +Cc: Git Mailing List, Glen Choo, Junio C Hamano

Hi Calvin and Elijah,

Le 2022-06-11 à 00:53, Elijah Newren a écrit :
> On Fri, Jun 10, 2022 at 4:29 PM Calvin Wan <calvinwan@google.com> wrote:
>>
>> When attempting to do a non-fast-forward merge in a project with
>> conflicts in the submodules, the merge fails and git prints the
>> following error:

I think we would want to be slightly more precise here, as 
"conflicts in the submodules" could be understood to mean:

1) conflicting submodule pointers in the superproject being merged
2) 1. + also content conflicts in the submodule merge

Here we are just talking about 1.

Also, the merge does not automatically fail, it only fails if
a fast-forward *of the submodule pointer* is not possible, which 
might be what you meant above; but to me "a non-fast-forward merge in a project
with conflict in the submodules" read like the non-fast-forwardness being talked about
was in the superproject, not in the submodule(s).

[message 0]
>>
>> Failed to merge submodule <submodules>
>> CONFLICT (submodule):Merge conflict in <submodule>
>> Automatic merge failed; fix conflicts and then commit the result.

   Aside: the first <submodules> should be singular.

This is indeed the output you get with the ort strategy if no existing merge commit
exist in the submodule repository that merges the submodule pointers recorded
in the superproject branches being merged. With the older "recursive" strategy,
this message is:

[message 1]
Failed to merge submodule sub (merge following commits not found)
Auto-merging sub
CONFLICT (submodule): Merge conflict in sub
Automatic merge failed; fix conflicts and then commit the result

c73cda76b1 (merge-ort: copy and adapt merge_submodule() 
from merge-recursive.c, 2021-01-01) does not mention why that error message
was changed, but perhaps it is just because it is slightly confusing
to the user (they might not be expecting Git to look for an existing
merge and so they don't know what merge the message is talking about).

Maybe something like "failed to find existing commit merging <hash1> and <hash2>"
would be clearer...

>>
>> Git is left in a conflicted state, which requires the user to:
>>  1. merge submodules
>>  2. add submodules changes to the superproject
>>  3. merge superproject
> 
> I think we may need to tweak these steps a bit:
> 
>    1. merge submodules OR update submodules to an already existing
> commit that reflects the merge (i.e. as submodules they may well be
> independently actively developed.  Someone may have already merged the
> appropriate branch/commit and the already extant merges should be used
> in preference to creating new ones.)
>    2. <just as you said>
>    3. FINISH merging the superproject (i.e. don't redo the merge)
> 
> I might be off on step 1; I have only used submodules extremely
> lightly and usually only for a limited time, so I'm not really sure
> what the expected workflow is.  I could also imagine it potentially
> being repository-dependent whether you would want to merge or select
> an appropriate commit to update to.

I agree with Elijah here, the submodule conflcit resolution might be to:

1) just choose one of the existing submodule commits on either side of 
the superproject branches being merged
2) choose an exisiting merge commit in the submodule repository (maybe after fetching it first)
3) create such a merge commit (what you are talking about here)

I also agree that it is highly repository- and workflow-dependent what
the "right" resolution is.

Note that the code does try to find an existing merge commit in the submodule
repository, in this case the error message is different. If such a merge commit 
exists:

    [message 2]
    Failed to merge submodule sub, but a possible merge resolution exists:
        aafcfa2 Merge branch 'sub-c' into sub-d


    If this is correct simply add it to the index for example
    by using:

      git update-index --cacheinfo 160000 aafcfa2a62764282ab848d5d6bea86ba217c1b24 "sub"

    which will accept this suggestion.

    CONFLICT (submodule): Merge conflict in sub
    Automatic merge failed; fix conflicts and then commit the result.

if multiple merge exist:

    [message 3]
    Failed to merge submodule sub, but multiple possible merges exist:
        2729a0c Merge branch 'sub-c' into ambiguous
        aafcfa2 Merge branch 'sub-c' into sub-d

    CONFLICT (submodule): Merge conflict in sub
    Automatic merge failed; fix conflicts and then commit the result.

Another aside, I really don't think we should instruct users to run
plumbing like 'git update-index --cacheinfo' , they should just cd into
the submodule and checkout the merge commit!

> 
>> These steps are non-obvious for newer submodule users to figure out
>> based on the error message and neither `git submodule status` nor `git
>> status` provide any useful pointers.
>>
>> Update error message to the following when attempting to do a
>> non-fast-forward merge in a project with conflicts in the submodules.
> 
> Make sense.

I agree that more guidance is a very nice addition.

Regarding 'git status' output, it is downright confusing, since it says:

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   sub

which is not at all what you want to do most of the time (that would
just stage whatever the currently checked out commit in the submodule is
at the moment!)

> 
>> The error message is based off of what would happen when `merge
>> --recurse-submodules` is eventually supported
>>
>> Failed to merge submodule <submodule>
>> CONFLICT (submodule): Merge conflict in <submodule>
>> Automatic merge failed; recursive merging with submodules is currently
>> not supported. To manually complete the merge:
>>  - go to submodule (<submodule>), and merge commit <commit>
>>  - come back to superproject, and `git add <submodule>` to record the above merge
>>  - resolve any other conflicts in the superproject
>>  - commit the resulting index in the superproject
> 
> Ah, I see you've fixed step 3 here; that's good.
> 
> However, these steps miss out on the merge-or-update submodule
> possibility...and since you mention these steps are potentially the
> basis for some future work, I think it's worth calling that out again.
> I'm slightly worried that the 'update' part of merge-or-update may
> throw a wrench in the plans for `merge --recurse-submodules`.
> 

Slightly off topic here, but for me the most important improvement that
'git merge --recurse-submodules' would bring is when there is *no submodule
conflicts*, i.e. one side fast-forwards the submodule and the other side
does not touch it, since in that case the worktree of the submodule *is not updated*
by the current code, which is one of the most confusing aspect of using submodules
for new users ("why is "git status" and "git diff" not clean if "git merge"
was fast-forward ?!?"), and the same is true (maybe more even so) for 'git rebase'. 

> (Also, continuing on the `merge --recurse-submodules` talent but
> discussing a different aspect of it, I'm curious if you need to add
> extra dirty-worktree/dirty-index checks for each submodule at the
> start of a merge, whether you need to try to lock N indexes before
> starting, and what other extra details are necessary.  But those are
> probably questions to address whenever work on the future series to
> implement this option is underway.)
> 
>> Changes since v1:
>>  - Removed advice to abort merge
>>  - Error message updated to contain more commit/submodule information
>>
>> Signed-off-by: Calvin Wan <calvinwan@google.com>
>>
>> ---
>>  builtin/merge.c            | 23 ++++++++++++++++++++++-
>>  merge-ort.c                |  7 ++++++-
>>  merge-recursive.c          |  7 ++++++-
>>  merge-recursive.h          |  4 ++++
>>  t/t6437-submodule-merge.sh |  5 ++++-
>>  5 files changed, 42 insertions(+), 4 deletions(-)
> 
> So you're modifying the "git merge" porcelain level (builtin/merge.c),
> the two merges strategies, their common header, and adding some tests.
> No other porcelains are modified...
> 
>> diff --git a/builtin/merge.c b/builtin/merge.c
>> index f178f5a3ee..7e2deea7fb 100644
>> --- a/builtin/merge.c
>> +++ b/builtin/merge.c
>> @@ -88,6 +88,8 @@ static const char *sign_commit;
>>  static int autostash;
>>  static int no_verify;
>>  static char *into_name;
>> +static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
>> +static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
>>
>>  static struct strategy all_strategy[] = {
>>         { "recursive",  NO_TRIVIAL },
>> @@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
>>                 }
>>
>>                 init_merge_options(&o, the_repository);
>> +               o.conflicted_submodule_oids = &conflicted_submodule_oids;
>> +               o.conflicted_submodule_paths = &conflicted_submodule_paths;
>>                 if (!strcmp(strategy, "subtree"))
>>                         o.subtree_shift = "";
>>
>> @@ -973,8 +977,25 @@ static int suggest_conflicts(void)
>>         strbuf_release(&msgbuf);
>>         fclose(fp);
>>         repo_rerere(the_repository, allow_rerere_auto);
>> -       printf(_("Automatic merge failed; "
>> +       if (conflicted_submodule_oids.nr > 0) {
>> +               int i;
>> +               printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
>> +                       "not supported. To manually complete the merge:\n"));
>> +               for (i = 0; i < conflicted_submodule_oids.nr; i++) {
>> +                       printf(_(" - go to submodule (%s), and merge commit %s\n"),
>> +                               conflicted_submodule_paths.items[i].string,
>> +                               oid_to_hex(&conflicted_submodule_oids.oid[i]));
>> +               }
>> +               printf(_(" - come back to superproject, and `git add"));
>> +               for (i = 0; i < conflicted_submodule_paths.nr; i++)
>> +                       printf(_(" %s"), conflicted_submodule_paths.items[i].string);
>> +               printf(_("` to record the above merge \n"
>> +               " - resolve any other conflicts in the superproject\n"
>> +               " - commit the resulting index in the superproject\n"));
>> +       } else {
>> +               printf(_("Automatic merge failed; "
>>                         "fix conflicts and then commit the result.\n"));
>> +       }
>>         return 1;
>>  }
> 
> This is kind of nice.  I was worried you were going to embed these
> messages in the merge strategies, which could cause problems for other
> users of the merge strategies such as the --remerge-diff options to
> git log and git show (your new messages would be unwanted noise or
> even cause confusion there), and to the merge-tree work.  In fact, a
> current submodule-merging message (search for "--cacheinfo") that is
> potentially similar to what you are adding here but which was added at
> the merge strategy level already feels highly problematic to me.  I've
> been considering nuking it from the codebase for some time because of
> those issues, though I guess just moving it out elsewhere may also
> work.
> 

Yes, this is the message I copied above. I agree that if we can tweak this
advice to instead mention 'git checkout' and add it to the message 
that Calvin is adding in this series, it would make for a really better
UX.

> However, this implementation does have a drawback: these messages
> won't appear for rebases, cherry-picks, reverts, attempted unstashing
> (git stash apply/pop), or other actions unless you update the relevant
> porcelains for those as well.
> 
> A possible alternative here would be to move it to the level of
> merge-recursive and merge-ort that is only called when the working
> tree and index are updated.  For example, placing it in
> merge_finalize() in merge-recursive.c and merge_switch_to_result() in
> merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
> However, I'm also fine with keeping it at the porcelain level, it just
> may need to be in a function that is called from several porcelains
> that way.

I think moving it to merge_finalize / merge_switch_to_result is indeed a 
good suggestion, then we might be improving the UX across the board and not just 
for 'git merge'. 

> 
>> diff --git a/merge-ort.c b/merge-ort.c
>> index 0d3f42592f..c86ee11614 100644
>> --- a/merge-ort.c
>> +++ b/merge-ort.c
>> @@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
>>                         const char *reason = _("content");
>>                         if (ci->filemask == 6)
>>                                 reason = _("add/add");
>> -                       if (S_ISGITLINK(merged_file.mode))
>> +                       if (S_ISGITLINK(merged_file.mode)) {
>>                                 reason = _("submodule");
>> +                               if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
>> +                                       oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
>> +                                       string_list_append(opt->conflicted_submodule_paths, path);
>> +                               }
>> +                       }
>>                         path_msg(opt, path, 0,
>>                                  _("CONFLICT (%s): Merge conflict in %s"),
>>                                  reason, path);
>> diff --git a/merge-recursive.c b/merge-recursive.c
>> index fd1bbde061..ff7cdbefe9 100644
>> --- a/merge-recursive.c
>> +++ b/merge-recursive.c
>> @@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
>>         }
>>
>>         if (!mfi->clean) {
>> -               if (S_ISGITLINK(mfi->blob.mode))
>> +               if (S_ISGITLINK(mfi->blob.mode)) {
>>                         reason = _("submodule");
>> +                       if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
>> +                               oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
>> +                               string_list_append(opt->conflicted_submodule_paths, path);
>> +                       }
>> +               }
>>                 output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
>>                                 reason, path);
>>                 if (ci && !df_conflict_remains)
> 
> Nice that the changes needed to both the ort and recursive strategies
> are so localized.  :-)
> 
>> diff --git a/merge-recursive.h b/merge-recursive.h
>> index b88000e3c2..5d267e7a43 100644
>> --- a/merge-recursive.h
>> +++ b/merge-recursive.h
>> @@ -51,6 +51,10 @@ struct merge_options {
>>
>>         /* internal fields used by the implementation */
>>         struct merge_options_internal *priv;
>> +
>> +       /* fields that hold submodule conflict information */
>> +       struct oid_array *conflicted_submodule_oids;
>> +       struct string_list *conflicted_submodule_paths;
>>  };
> 
> Make sense.
> 
>>  void init_merge_options(struct merge_options *opt, struct repository *repo);
>> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
>> index 178413c22f..5b384dedc1 100755
>> --- a/t/t6437-submodule-merge.sh
>> +++ b/t/t6437-submodule-merge.sh
>> @@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
>>                 test_must_fail git merge c 2> actual
>>           fi &&
>>          grep $(cat expect) actual > /dev/null &&
>> +        test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
>>          git reset --hard)
>>  '
>>

In this test, an existing merge does exist, and is suggested to the user with [message 2] above.
So telling users to do a new merge in the submodule is maybe to what we want.

>> @@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>>          fi &&
>>         grep $(cat expect1) actual > /dev/null &&
>>         grep $(cat expect2) actual > /dev/null &&
>> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
>>         git reset --hard)
>>  '

Here, 2 existing merges exist, and they are presented to the users with [message 3] above.
So again we might not want to tell users to do a new merge.

>>
>> @@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
>>         git commit -a -m "f" &&
>>
>>         git checkout -b test-backward e &&
>> -       test_must_fail git merge f)
>> +       test_must_fail git merge f >actual &&
>> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)
> 
> test_i18ngrep is apparently on the way out:
> 
>     $ grep -B 3 ^test_i18ngrep t/test-lib-functions.sh
>     # Wrapper for grep which used to be used for
>     # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other
>     # in-flight changes. Should not be used and will be removed soon.
>     test_i18ngrep () {
> 
> I think you just want to use grep instead here for each of these hunks.
> 

Here, one side regresses the submodule commit and the other side advances it.
In this case, I really think the right resolution is to choose one side or 
the other, and not suggest to do a merge at all. So that means that we might
want to tweak the advice we give based on the type of submodule conflict...

Also, maybe we would want a new test that reproduces exactly the conditions of 
[message 1], i.e. no existing merge exists in the submodule.

Thanks a lot for wanting to improve the submodule UX!

Cheers,

Philippe.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-11 17:08     ` Philippe Blain
@ 2022-06-12  6:56       ` Elijah Newren
  2022-06-13  2:03       ` Calvin Wan
  1 sibling, 0 replies; 60+ messages in thread
From: Elijah Newren @ 2022-06-12  6:56 UTC (permalink / raw)
  To: Philippe Blain; +Cc: Calvin Wan, Git Mailing List, Glen Choo, Junio C Hamano

Hi Philippe,

On Sat, Jun 11, 2022 at 10:08 AM Philippe Blain
<levraiphilippeblain@gmail.com> wrote:
>
> Hi Calvin and Elijah,
>
> Le 2022-06-11 à 00:53, Elijah Newren a écrit :
> > On Fri, Jun 10, 2022 at 4:29 PM Calvin Wan <calvinwan@google.com> wrote:
> >>
> >> When attempting to do a non-fast-forward merge in a project with
> >> conflicts in the submodules, the merge fails and git prints the
> >> following error:
>
> I think we would want to be slightly more precise here, as
> "conflicts in the submodules" could be understood to mean:
>
> 1) conflicting submodule pointers in the superproject being merged
> 2) 1. + also content conflicts in the submodule merge
>
> Here we are just talking about 1.

Isn't that already addressed by Calvin's wording "recursive merging
with submodules is currently not supported..."?  Or are you concerned
that this wording will be separated from the short conflict line?

> Also, the merge does not automatically fail, it only fails if
> a fast-forward *of the submodule pointer* is not possible, which
> might be what you meant above; but to me "a non-fast-forward merge in a project
> with conflict in the submodules" read like the non-fast-forwardness being talked about
> was in the superproject, not in the submodule(s).
>
> [message 0]
> >>
> >> Failed to merge submodule <submodules>
> >> CONFLICT (submodule):Merge conflict in <submodule>
> >> Automatic merge failed; fix conflicts and then commit the result.
>
>    Aside: the first <submodules> should be singular.
>
> This is indeed the output you get with the ort strategy if no existing merge commit
> exist in the submodule repository that merges the submodule pointers recorded
> in the superproject branches being merged. With the older "recursive" strategy,
> this message is:
>
> [message 1]
> Failed to merge submodule sub (merge following commits not found)
> Auto-merging sub
> CONFLICT (submodule): Merge conflict in sub
> Automatic merge failed; fix conflicts and then commit the result
>
> c73cda76b1 (merge-ort: copy and adapt merge_submodule()
> from merge-recursive.c, 2021-01-01) does not mention why that error message
> was changed, but perhaps it is just because it is slightly confusing
> to the user (they might not be expecting Git to look for an existing
> merge and so they don't know what merge the message is talking about).

The text in parentheses from merge-recursive.c didn't parse for me.
Perhaps if it instead read "(failed to find merge commit in submodule
to use as merge resolution)" or something like that then I might have
kept it.  As it was, I found it really hard to parse ("Why are there
no commits after the instruction to 'merge following commits'"?  Is
"merge" a noun or a verb as used in the parenthetical comment? etc.).
The text could have been improved, but a really easy way to improve it
was just to remove it altogether.  Removing it was an easy way to
improve it, so I did that.

Also, a little rant about merge_submodule()....that function
originally had multiple suboptimal error messages, used to write
directly to stdout instead of using the output() function to respect
verbosity, didn't internationalize messages, didn't consider
recursiveness in the resolution, has verbose output that only makes
sense when updating the working tree and index, etc.  It was quite a
mess.  Some of that got cleaned up in merge-recursive.c.  Some only
got cleaned up with the move to merge-ort.c.  Some of it sadly still
persists (as mentioned elsewhere in this email).

> Maybe something like "failed to find existing commit merging <hash1> and <hash2>"
> would be clearer...

Yes, that would be much clearer.  Can I tweak the suggestion to
"failed to find an existing commit in submodule merging <hash1> and
<hash2>"?

However, while it's work in a related area (merging of submodules), I
think this suggestion is separate from what Calvin is trying to fix
and doesn't need to hold up his patch.

> >>
> >> Git is left in a conflicted state, which requires the user to:
> >>  1. merge submodules
> >>  2. add submodules changes to the superproject
> >>  3. merge superproject
> >
> > I think we may need to tweak these steps a bit:
> >
> >    1. merge submodules OR update submodules to an already existing
> > commit that reflects the merge (i.e. as submodules they may well be
> > independently actively developed.  Someone may have already merged the
> > appropriate branch/commit and the already extant merges should be used
> > in preference to creating new ones.)
> >    2. <just as you said>
> >    3. FINISH merging the superproject (i.e. don't redo the merge)
> >
> > I might be off on step 1; I have only used submodules extremely
> > lightly and usually only for a limited time, so I'm not really sure
> > what the expected workflow is.  I could also imagine it potentially
> > being repository-dependent whether you would want to merge or select
> > an appropriate commit to update to.
>
> I agree with Elijah here, the submodule conflcit resolution might be to:
>
> 1) just choose one of the existing submodule commits on either side of
> the superproject branches being merged
> 2) choose an exisiting merge commit in the submodule repository (maybe after fetching it first)
> 3) create such a merge commit (what you are talking about here)
>
> I also agree that it is highly repository- and workflow-dependent what
> the "right" resolution is.
>
> Note that the code does try to find an existing merge commit in the submodule
> repository, in this case the error message is different. If such a merge commit
> exists:
>
>     [message 2]
>     Failed to merge submodule sub, but a possible merge resolution exists:
>         aafcfa2 Merge branch 'sub-c' into sub-d
>
>
>     If this is correct simply add it to the index for example
>     by using:
>
>       git update-index --cacheinfo 160000 aafcfa2a62764282ab848d5d6bea86ba217c1b24 "sub"
>
>     which will accept this suggestion.
>
>     CONFLICT (submodule): Merge conflict in sub
>     Automatic merge failed; fix conflicts and then commit the result.
>
> if multiple merge exist:
>
>     [message 3]
>     Failed to merge submodule sub, but multiple possible merges exist:
>         2729a0c Merge branch 'sub-c' into ambiguous
>         aafcfa2 Merge branch 'sub-c' into sub-d
>
>     CONFLICT (submodule): Merge conflict in sub
>     Automatic merge failed; fix conflicts and then commit the result.
>
> Another aside, I really don't think we should instruct users to run
> plumbing like 'git update-index --cacheinfo' , they should just cd into
> the submodule and checkout the merge commit!

+1

> >
> >> These steps are non-obvious for newer submodule users to figure out
> >> based on the error message and neither `git submodule status` nor `git
> >> status` provide any useful pointers.
> >>
> >> Update error message to the following when attempting to do a
> >> non-fast-forward merge in a project with conflicts in the submodules.
> >
> > Make sense.
>
> I agree that more guidance is a very nice addition.
>
> Regarding 'git status' output, it is downright confusing, since it says:
>
> Unmerged paths:
>   (use "git add <file>..." to mark resolution)
>         both modified:   sub
>
> which is not at all what you want to do most of the time (that would
> just stage whatever the currently checked out commit in the submodule is
> at the moment!)

Sure, but this isn't anything special about submodules.  You don't
want to "git add $path" if you haven't resolved the conflicts in $path
first.  That's advice that is true regardless of whether it's a
regular text file that was present on both sides of the merge, in
which case there will be conflict markers to remind the user to
resolve conflicts first, or whether it's something else.  And there
are a lot of something elses without conflict markers: a submodule, a
binary file, a symlink, path from file/directory conflict, something
with conflicting modes, a file with any of a variety of rename-related
path conflicts, maybe others.

Perhaps you just want the advice changed to

    (use "git add <file>..." after resolving conflicts for the
specified path(s))

?  It's a bit of a mouthful, but I guess it addresses your concern.
It's another possible improvement that belongs outside Calvin's patch
so it can be discussed separately.  :-)

>> The error message is based off of what would happen when `merge
> >> --recurse-submodules` is eventually supported
> >>
> >> Failed to merge submodule <submodule>
> >> CONFLICT (submodule): Merge conflict in <submodule>
> >> Automatic merge failed; recursive merging with submodules is currently
> >> not supported. To manually complete the merge:
> >>  - go to submodule (<submodule>), and merge commit <commit>
> >>  - come back to superproject, and `git add <submodule>` to record the above merge
> >>  - resolve any other conflicts in the superproject
> >>  - commit the resulting index in the superproject
> >
> > Ah, I see you've fixed step 3 here; that's good.
> >
> > However, these steps miss out on the merge-or-update submodule
> > possibility...and since you mention these steps are potentially the
> > basis for some future work, I think it's worth calling that out again.
> > I'm slightly worried that the 'update' part of merge-or-update may
> > throw a wrench in the plans for `merge --recurse-submodules`.
> >
>
> Slightly off topic here, but for me the most important improvement that
> 'git merge --recurse-submodules' would bring is when there is *no submodule
> conflicts*, i.e. one side fast-forwards the submodule and the other side
> does not touch it, since in that case the worktree of the submodule *is not updated*
> by the current code, which is one of the most confusing aspect of using submodules
> for new users ("why is "git status" and "git diff" not clean if "git merge"
> was fast-forward ?!?"), and the same is true (maybe more even so) for 'git rebase'.

Do you really mean "i.e." here or did you mean "e.g."?  I think the
example you give is just one where a submodule can be cleanly merged
and then doesn't get updated.  Another is when an already existing
submodule merge commit can be used as the merge resolution for the
submodule.  The merging code simply doesn't update the working tree
(or indexes) of submodules regardless of whether it updates the
submodule pointers.

Taking a quick glance, I think it might turn out to be trivial to fix
this in merge-ort.c.  I _think_ simply setting
   config_update_recurse_submodules = RECURSE_SUBMODULES_ON
in merge-ort.c:checkout() anywhere before the call to unpack_trees()
might be enough.  Want to try it?

The fix in merge-recursive is probably much more involved, and likely
not worth the effort (there's a related comment in the code in
update_file_flags() about this; look for "submodule").

> > (Also, continuing on the `merge --recurse-submodules` talent but
> > discussing a different aspect of it, I'm curious if you need to add
> > extra dirty-worktree/dirty-index checks for each submodule at the
> > start of a merge, whether you need to try to lock N indexes before
> > starting, and what other extra details are necessary.  But those are
> > probably questions to address whenever work on the future series to
> > implement this option is underway.)
> >
> >> Changes since v1:
> >>  - Removed advice to abort merge
> >>  - Error message updated to contain more commit/submodule information
> >>
> >> Signed-off-by: Calvin Wan <calvinwan@google.com>
> >>
> >> ---
> >>  builtin/merge.c            | 23 ++++++++++++++++++++++-
> >>  merge-ort.c                |  7 ++++++-
> >>  merge-recursive.c          |  7 ++++++-
> >>  merge-recursive.h          |  4 ++++
> >>  t/t6437-submodule-merge.sh |  5 ++++-
> >>  5 files changed, 42 insertions(+), 4 deletions(-)
> >
> > So you're modifying the "git merge" porcelain level (builtin/merge.c),
> > the two merges strategies, their common header, and adding some tests.
> > No other porcelains are modified...
> >
> >> diff --git a/builtin/merge.c b/builtin/merge.c
> >> index f178f5a3ee..7e2deea7fb 100644
> >> --- a/builtin/merge.c
> >> +++ b/builtin/merge.c
> >> @@ -88,6 +88,8 @@ static const char *sign_commit;
> >>  static int autostash;
> >>  static int no_verify;
> >>  static char *into_name;
> >> +static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
> >> +static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
> >>
> >>  static struct strategy all_strategy[] = {
> >>         { "recursive",  NO_TRIVIAL },
> >> @@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
> >>                 }
> >>
> >>                 init_merge_options(&o, the_repository);
> >> +               o.conflicted_submodule_oids = &conflicted_submodule_oids;
> >> +               o.conflicted_submodule_paths = &conflicted_submodule_paths;
> >>                 if (!strcmp(strategy, "subtree"))
> >>                         o.subtree_shift = "";
> >>
> >> @@ -973,8 +977,25 @@ static int suggest_conflicts(void)
> >>         strbuf_release(&msgbuf);
> >>         fclose(fp);
> >>         repo_rerere(the_repository, allow_rerere_auto);
> >> -       printf(_("Automatic merge failed; "
> >> +       if (conflicted_submodule_oids.nr > 0) {
> >> +               int i;
> >> +               printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
> >> +                       "not supported. To manually complete the merge:\n"));
> >> +               for (i = 0; i < conflicted_submodule_oids.nr; i++) {
> >> +                       printf(_(" - go to submodule (%s), and merge commit %s\n"),
> >> +                               conflicted_submodule_paths.items[i].string,
> >> +                               oid_to_hex(&conflicted_submodule_oids.oid[i]));
> >> +               }
> >> +               printf(_(" - come back to superproject, and `git add"));
> >> +               for (i = 0; i < conflicted_submodule_paths.nr; i++)
> >> +                       printf(_(" %s"), conflicted_submodule_paths.items[i].string);
> >> +               printf(_("` to record the above merge \n"
> >> +               " - resolve any other conflicts in the superproject\n"
> >> +               " - commit the resulting index in the superproject\n"));
> >> +       } else {
> >> +               printf(_("Automatic merge failed; "
> >>                         "fix conflicts and then commit the result.\n"));
> >> +       }
> >>         return 1;
> >>  }
> >
> > This is kind of nice.  I was worried you were going to embed these
> > messages in the merge strategies, which could cause problems for other
> > users of the merge strategies such as the --remerge-diff options to
> > git log and git show (your new messages would be unwanted noise or
> > even cause confusion there), and to the merge-tree work.  In fact, a
> > current submodule-merging message (search for "--cacheinfo") that is
> > potentially similar to what you are adding here but which was added at
> > the merge strategy level already feels highly problematic to me.  I've
> > been considering nuking it from the codebase for some time because of
> > those issues, though I guess just moving it out elsewhere may also
> > work.
> >
>
> Yes, this is the message I copied above. I agree that if we can tweak this
> advice to instead mention 'git checkout' and add it to the message
> that Calvin is adding in this series, it would make for a really better
> UX.

This message shouldn't be tweaked; it should be expunged.  It does not
belong in the low-level code where it is.  Now, if someone wants to
move the message somewhere else so that it only is displayed when the
working tree and index are updated, and they want to tweak it at the
same time to avoid the ugly suggestion to use low-level plumbing,
that'd be great.  But neither of those things need to hold up Calvin's
series.  (I think you're providing lots of good points of what can be
improved, Philippe, I just want to be clear that I think they are
related but separate improvements.)


> > However, this implementation does have a drawback: these messages
> > won't appear for rebases, cherry-picks, reverts, attempted unstashing
> > (git stash apply/pop), or other actions unless you update the relevant
> > porcelains for those as well.
> >
> > A possible alternative here would be to move it to the level of
> > merge-recursive and merge-ort that is only called when the working
> > tree and index are updated.  For example, placing it in
> > merge_finalize() in merge-recursive.c and merge_switch_to_result() in
> > merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
> > However, I'm also fine with keeping it at the porcelain level, it just
> > may need to be in a function that is called from several porcelains
> > that way.
>
> I think moving it to merge_finalize / merge_switch_to_result is indeed a
> good suggestion, then we might be improving the UX across the board and not just
> for 'git merge'.
>
> >
> >> diff --git a/merge-ort.c b/merge-ort.c
> >> index 0d3f42592f..c86ee11614 100644
> >> --- a/merge-ort.c
> >> +++ b/merge-ort.c
> >> @@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
> >>                         const char *reason = _("content");
> >>                         if (ci->filemask == 6)
> >>                                 reason = _("add/add");
> >> -                       if (S_ISGITLINK(merged_file.mode))
> >> +                       if (S_ISGITLINK(merged_file.mode)) {
> >>                                 reason = _("submodule");
> >> +                               if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> >> +                                       oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
> >> +                                       string_list_append(opt->conflicted_submodule_paths, path);
> >> +                               }
> >> +                       }
> >>                         path_msg(opt, path, 0,
> >>                                  _("CONFLICT (%s): Merge conflict in %s"),
> >>                                  reason, path);
> >> diff --git a/merge-recursive.c b/merge-recursive.c
> >> index fd1bbde061..ff7cdbefe9 100644
> >> --- a/merge-recursive.c
> >> +++ b/merge-recursive.c
> >> @@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
> >>         }
> >>
> >>         if (!mfi->clean) {
> >> -               if (S_ISGITLINK(mfi->blob.mode))
> >> +               if (S_ISGITLINK(mfi->blob.mode)) {
> >>                         reason = _("submodule");
> >> +                       if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> >> +                               oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
> >> +                               string_list_append(opt->conflicted_submodule_paths, path);
> >> +                       }
> >> +               }
> >>                 output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
> >>                                 reason, path);
> >>                 if (ci && !df_conflict_remains)
> >
> > Nice that the changes needed to both the ort and recursive strategies
> > are so localized.  :-)
> >
> >> diff --git a/merge-recursive.h b/merge-recursive.h
> >> index b88000e3c2..5d267e7a43 100644
> >> --- a/merge-recursive.h
> >> +++ b/merge-recursive.h
> >> @@ -51,6 +51,10 @@ struct merge_options {
> >>
> >>         /* internal fields used by the implementation */
> >>         struct merge_options_internal *priv;
> >> +
> >> +       /* fields that hold submodule conflict information */
> >> +       struct oid_array *conflicted_submodule_oids;
> >> +       struct string_list *conflicted_submodule_paths;
> >>  };
> >
> > Make sense.
> >
> >>  void init_merge_options(struct merge_options *opt, struct repository *repo);
> >> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> >> index 178413c22f..5b384dedc1 100755
> >> --- a/t/t6437-submodule-merge.sh
> >> +++ b/t/t6437-submodule-merge.sh
> >> @@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
> >>                 test_must_fail git merge c 2> actual
> >>           fi &&
> >>          grep $(cat expect) actual > /dev/null &&
> >> +        test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >>          git reset --hard)
> >>  '
> >>
>
> In this test, an existing merge does exist, and is suggested to the user with [message 2] above.
> So telling users to do a new merge in the submodule is maybe to what we want.
>
> >> @@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
> >>          fi &&
> >>         grep $(cat expect1) actual > /dev/null &&
> >>         grep $(cat expect2) actual > /dev/null &&
> >> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >>         git reset --hard)
> >>  '
>
> Here, 2 existing merges exist, and they are presented to the users with [message 3] above.
> So again we might not want to tell users to do a new merge.
>
> >>
> >> @@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
> >>         git commit -a -m "f" &&
> >>
> >>         git checkout -b test-backward e &&
> >> -       test_must_fail git merge f)
> >> +       test_must_fail git merge f >actual &&
> >> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)
> >
> > test_i18ngrep is apparently on the way out:
> >
> >     $ grep -B 3 ^test_i18ngrep t/test-lib-functions.sh
> >     # Wrapper for grep which used to be used for
> >     # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other
> >     # in-flight changes. Should not be used and will be removed soon.
> >     test_i18ngrep () {
> >
> > I think you just want to use grep instead here for each of these hunks.
> >
>
> Here, one side regresses the submodule commit and the other side advances it.
> In this case, I really think the right resolution is to choose one side or
> the other, and not suggest to do a merge at all. So that means that we might
> want to tweak the advice we give based on the type of submodule conflict...
>
> Also, maybe we would want a new test that reproduces exactly the conditions of
> [message 1], i.e. no existing merge exists in the submodule.
>
> Thanks a lot for wanting to improve the submodule UX!
>
> Cheers,
>
> Philippe.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-11  4:53   ` Elijah Newren
  2022-06-11 17:08     ` Philippe Blain
@ 2022-06-12 23:30     ` Junio C Hamano
  2022-06-13  1:54     ` Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-06-12 23:30 UTC (permalink / raw)
  To: Elijah Newren; +Cc: Calvin Wan, Git Mailing List, Glen Choo

Elijah Newren <newren@gmail.com> writes:

>> Git is left in a conflicted state, which requires the user to:
>>  1. merge submodules
>>  2. add submodules changes to the superproject
>>  3. merge superproject
>
> I think we may need to tweak these steps a bit:
>
>    1. merge submodules OR update submodules to an already existing
> commit that reflects the merge (i.e. as submodules they may well be
> independently actively developed.  Someone may have already merged the
> appropriate branch/commit and the already extant merges should be used
> in preference to creating new ones.)

That is a very good point.

I suspect we should encourage users to find an existing merge, and
possibly even discourage them from creating a new one on their own.
There may not be much point in creating the same merge of identical
parent commits with the same result that only has different
metadata.

It may be tempting to argue that an existing merge in a submodule
and the merge you are creating now are made in different context
because the superproject merges that necessitate thse submodule
merges are different, but I doubt that is a healthy argument.  A
commit in the submodule should be able to explain its reason to
exist on its own---after all, the superproject may know which
submodules are used for what purpose, but a submodule does not have
to know where it is used, and that is how it is made more reusable.

>    2. <just as you said>
>    3. FINISH merging the superproject (i.e. don't redo the merge)

Yes, exactly.  FWIW, an earlier draft was telling users to abort the
merge in the first step, and its review corrected it.  1 and 2 are
to be done in the context of the interrupted superproject merge, so
its logical consequence is to conclude the merge at step 3.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-11  4:53   ` Elijah Newren
  2022-06-11 17:08     ` Philippe Blain
  2022-06-12 23:30     ` Junio C Hamano
@ 2022-06-13  1:54     ` Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-13  1:54 UTC (permalink / raw)
  To: Elijah Newren; +Cc: Git Mailing List, Glen Choo, Junio C Hamano

> I think we may need to tweak these steps a bit:

>    1. merge submodules OR update submodules to an already existing
> commit that reflects the merge (i.e. as submodules they may well be
> independently actively developed.  Someone may have already merged the
> appropriate branch/commit and the already extant merges should be used
> in preference to creating new ones.)
>    2. <just as you said>
>    3. FINISH merging the superproject (i.e. don't redo the merge)

> I might be off on step 1; I have only used submodules extremely
> lightly and usually only for a limited time, so I'm not really sure
> what the expected workflow is.  I could also imagine it potentially
> being repository-dependent whether you would want to merge or select
> an appropriate commit to update to.

You are correct that either merging or selecting an appropriate commit to
update to (if it exists) are the two ways to fix a submodule conflict. As
Phillippe mentions below, if merge detects a possible merge resolution,
that merge resolution is printed out as in the suggestion. I wonder if it's
easy to pull out the exact reason for why the submodule merge failed. If
that is the case, then I think changing the first step to something like:

(If no merge resolution already exists, the suggestion stays the same)
  1. - go to submodule (<submodule>) and merge commit <commit>
(If a merge resolution exists)
  1. - go to submodule (<submodule>). Either update the submodule
        to one of the possible merge commits or merge commit <commit>

If it is not easy to pull out the merge fail reason, then I think we can
generalize the first step to:

1. - go to submodule (<submodule>). Merge commit <commit> or if
      a possible merge resolution exists, then you have the option to
      update the submodule to one of the merge commits

> However, this implementation does have a drawback: these messages
> won't appear for rebases, cherry-picks, reverts, attempted unstashing
> (git stash apply/pop), or other actions unless you update the relevant
> porcelains for those as well.

> A possible alternative here would be to move it to the level of
> merge-recursive and merge-ort that is only called when the working
> tree and index are updated.  For example, placing it in
> merge_finalize() in merge-recursive.c and merge_switch_to_result() in
> merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
> However, I'm also fine with keeping it at the porcelain level, it just
> may need to be in a function that is called from several porcelains
> that way.

You make a good point here. I'm inclined to keep it at the porcelain level
since this is a temporary holdover until `merge --recurse-submodules` is
implemented.

> test_i18ngrep is apparently on the way out:

ack

On Fri, Jun 10, 2022 at 9:53 PM Elijah Newren <newren@gmail.com> wrote:
>
> On Fri, Jun 10, 2022 at 4:29 PM Calvin Wan <calvinwan@google.com> wrote:
> >
> > When attempting to do a non-fast-forward merge in a project with
> > conflicts in the submodules, the merge fails and git prints the
> > following error:
> >
> > Failed to merge submodule <submodules>
> > CONFLICT (submodule):Merge conflict in <submodule>
> > Automatic merge failed; fix conflicts and then commit the result.
> >
> > Git is left in a conflicted state, which requires the user to:
> >  1. merge submodules
> >  2. add submodules changes to the superproject
> >  3. merge superproject
>
> I think we may need to tweak these steps a bit:
>
>    1. merge submodules OR update submodules to an already existing
> commit that reflects the merge (i.e. as submodules they may well be
> independently actively developed.  Someone may have already merged the
> appropriate branch/commit and the already extant merges should be used
> in preference to creating new ones.)
>    2. <just as you said>
>    3. FINISH merging the superproject (i.e. don't redo the merge)
>
> I might be off on step 1; I have only used submodules extremely
> lightly and usually only for a limited time, so I'm not really sure
> what the expected workflow is.  I could also imagine it potentially
> being repository-dependent whether you would want to merge or select
> an appropriate commit to update to.
>
> > These steps are non-obvious for newer submodule users to figure out
> > based on the error message and neither `git submodule status` nor `git
> > status` provide any useful pointers.
> >
> > Update error message to the following when attempting to do a
> > non-fast-forward merge in a project with conflicts in the submodules.
>
> Make sense.
>
> > The error message is based off of what would happen when `merge
> > --recurse-submodules` is eventually supported
> >
> > Failed to merge submodule <submodule>
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Automatic merge failed; recursive merging with submodules is currently
> > not supported. To manually complete the merge:
> >  - go to submodule (<submodule>), and merge commit <commit>
> >  - come back to superproject, and `git add <submodule>` to record the above merge
> >  - resolve any other conflicts in the superproject
> >  - commit the resulting index in the superproject
>
> Ah, I see you've fixed step 3 here; that's good.
>
> However, these steps miss out on the merge-or-update submodule
> possibility...and since you mention these steps are potentially the
> basis for some future work, I think it's worth calling that out again.
> I'm slightly worried that the 'update' part of merge-or-update may
> throw a wrench in the plans for `merge --recurse-submodules`.
>
> (Also, continuing on the `merge --recurse-submodules` talent but
> discussing a different aspect of it, I'm curious if you need to add
> extra dirty-worktree/dirty-index checks for each submodule at the
> start of a merge, whether you need to try to lock N indexes before
> starting, and what other extra details are necessary.  But those are
> probably questions to address whenever work on the future series to
> implement this option is underway.)
>
> > Changes since v1:
> >  - Removed advice to abort merge
> >  - Error message updated to contain more commit/submodule information
> >
> > Signed-off-by: Calvin Wan <calvinwan@google.com>
> >
> > ---
> >  builtin/merge.c            | 23 ++++++++++++++++++++++-
> >  merge-ort.c                |  7 ++++++-
> >  merge-recursive.c          |  7 ++++++-
> >  merge-recursive.h          |  4 ++++
> >  t/t6437-submodule-merge.sh |  5 ++++-
> >  5 files changed, 42 insertions(+), 4 deletions(-)
>
> So you're modifying the "git merge" porcelain level (builtin/merge.c),
> the two merges strategies, their common header, and adding some tests.
> No other porcelains are modified...
>
> > diff --git a/builtin/merge.c b/builtin/merge.c
> > index f178f5a3ee..7e2deea7fb 100644
> > --- a/builtin/merge.c
> > +++ b/builtin/merge.c
> > @@ -88,6 +88,8 @@ static const char *sign_commit;
> >  static int autostash;
> >  static int no_verify;
> >  static char *into_name;
> > +static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
> > +static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
> >
> >  static struct strategy all_strategy[] = {
> >         { "recursive",  NO_TRIVIAL },
> > @@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
> >                 }
> >
> >                 init_merge_options(&o, the_repository);
> > +               o.conflicted_submodule_oids = &conflicted_submodule_oids;
> > +               o.conflicted_submodule_paths = &conflicted_submodule_paths;
> >                 if (!strcmp(strategy, "subtree"))
> >                         o.subtree_shift = "";
> >
> > @@ -973,8 +977,25 @@ static int suggest_conflicts(void)
> >         strbuf_release(&msgbuf);
> >         fclose(fp);
> >         repo_rerere(the_repository, allow_rerere_auto);
> > -       printf(_("Automatic merge failed; "
> > +       if (conflicted_submodule_oids.nr > 0) {
> > +               int i;
> > +               printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
> > +                       "not supported. To manually complete the merge:\n"));
> > +               for (i = 0; i < conflicted_submodule_oids.nr; i++) {
> > +                       printf(_(" - go to submodule (%s), and merge commit %s\n"),
> > +                               conflicted_submodule_paths.items[i].string,
> > +                               oid_to_hex(&conflicted_submodule_oids.oid[i]));
> > +               }
> > +               printf(_(" - come back to superproject, and `git add"));
> > +               for (i = 0; i < conflicted_submodule_paths.nr; i++)
> > +                       printf(_(" %s"), conflicted_submodule_paths.items[i].string);
> > +               printf(_("` to record the above merge \n"
> > +               " - resolve any other conflicts in the superproject\n"
> > +               " - commit the resulting index in the superproject\n"));
> > +       } else {
> > +               printf(_("Automatic merge failed; "
> >                         "fix conflicts and then commit the result.\n"));
> > +       }
> >         return 1;
> >  }
>
> This is kind of nice.  I was worried you were going to embed these
> messages in the merge strategies, which could cause problems for other
> users of the merge strategies such as the --remerge-diff options to
> git log and git show (your new messages would be unwanted noise or
> even cause confusion there), and to the merge-tree work.  In fact, a
> current submodule-merging message (search for "--cacheinfo") that is
> potentially similar to what you are adding here but which was added at
> the merge strategy level already feels highly problematic to me.  I've
> been considering nuking it from the codebase for some time because of
> those issues, though I guess just moving it out elsewhere may also
> work.
>
> However, this implementation does have a drawback: these messages
> won't appear for rebases, cherry-picks, reverts, attempted unstashing
> (git stash apply/pop), or other actions unless you update the relevant
> porcelains for those as well.
>
> A possible alternative here would be to move it to the level of
> merge-recursive and merge-ort that is only called when the working
> tree and index are updated.  For example, placing it in
> merge_finalize() in merge-recursive.c and merge_switch_to_result() in
> merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
> However, I'm also fine with keeping it at the porcelain level, it just
> may need to be in a function that is called from several porcelains
> that way.
>
> > diff --git a/merge-ort.c b/merge-ort.c
> > index 0d3f42592f..c86ee11614 100644
> > --- a/merge-ort.c
> > +++ b/merge-ort.c
> > @@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
> >                         const char *reason = _("content");
> >                         if (ci->filemask == 6)
> >                                 reason = _("add/add");
> > -                       if (S_ISGITLINK(merged_file.mode))
> > +                       if (S_ISGITLINK(merged_file.mode)) {
> >                                 reason = _("submodule");
> > +                               if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> > +                                       oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
> > +                                       string_list_append(opt->conflicted_submodule_paths, path);
> > +                               }
> > +                       }
> >                         path_msg(opt, path, 0,
> >                                  _("CONFLICT (%s): Merge conflict in %s"),
> >                                  reason, path);
> > diff --git a/merge-recursive.c b/merge-recursive.c
> > index fd1bbde061..ff7cdbefe9 100644
> > --- a/merge-recursive.c
> > +++ b/merge-recursive.c
> > @@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
> >         }
> >
> >         if (!mfi->clean) {
> > -               if (S_ISGITLINK(mfi->blob.mode))
> > +               if (S_ISGITLINK(mfi->blob.mode)) {
> >                         reason = _("submodule");
> > +                       if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> > +                               oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
> > +                               string_list_append(opt->conflicted_submodule_paths, path);
> > +                       }
> > +               }
> >                 output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
> >                                 reason, path);
> >                 if (ci && !df_conflict_remains)
>
> Nice that the changes needed to both the ort and recursive strategies
> are so localized.  :-)
>
> > diff --git a/merge-recursive.h b/merge-recursive.h
> > index b88000e3c2..5d267e7a43 100644
> > --- a/merge-recursive.h
> > +++ b/merge-recursive.h
> > @@ -51,6 +51,10 @@ struct merge_options {
> >
> >         /* internal fields used by the implementation */
> >         struct merge_options_internal *priv;
> > +
> > +       /* fields that hold submodule conflict information */
> > +       struct oid_array *conflicted_submodule_oids;
> > +       struct string_list *conflicted_submodule_paths;
> >  };
>
> Make sense.
>
> >  void init_merge_options(struct merge_options *opt, struct repository *repo);
> > diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> > index 178413c22f..5b384dedc1 100755
> > --- a/t/t6437-submodule-merge.sh
> > +++ b/t/t6437-submodule-merge.sh
> > @@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
> >                 test_must_fail git merge c 2> actual
> >           fi &&
> >          grep $(cat expect) actual > /dev/null &&
> > +        test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >          git reset --hard)
> >  '
> >
> > @@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
> >          fi &&
> >         grep $(cat expect1) actual > /dev/null &&
> >         grep $(cat expect2) actual > /dev/null &&
> > +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >         git reset --hard)
> >  '
> >
> > @@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
> >         git commit -a -m "f" &&
> >
> >         git checkout -b test-backward e &&
> > -       test_must_fail git merge f)
> > +       test_must_fail git merge f >actual &&
> > +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)
>
> test_i18ngrep is apparently on the way out:
>
>     $ grep -B 3 ^test_i18ngrep t/test-lib-functions.sh
>     # Wrapper for grep which used to be used for
>     # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other
>     # in-flight changes. Should not be used and will be removed soon.
>     test_i18ngrep () {
>
> I think you just want to use grep instead here for each of these hunks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v2] submodule merge: update conflict error message
  2022-06-11 17:08     ` Philippe Blain
  2022-06-12  6:56       ` Elijah Newren
@ 2022-06-13  2:03       ` Calvin Wan
  1 sibling, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-13  2:03 UTC (permalink / raw)
  To: Philippe Blain; +Cc: Elijah Newren, Git Mailing List, Glen Choo, Junio C Hamano

> I think we would want to be slightly more precise here, as
> "conflicts in the submodules" could be understood to mean:

> 1) conflicting submodule pointers in the superproject being merged
> 2) 1. + also content conflicts in the submodule merge

> Here we are just talking about 1.

Will clarify

> Also, the merge does not automatically fail, it only fails if
> a fast-forward *of the submodule pointer* is not possible, which
> might be what you meant above; but to me "a non-fast-forward merge in a project
> with conflict in the submodules" read like the non-fast-forwardness being talked about
> was in the superproject, not in the submodule(s).

It was what I meant above, but I do agree that I can reword this
section as well.

> I agree with Elijah here, the submodule conflcit resolution might be to:

> 1) just choose one of the existing submodule commits on either side of
> the superproject branches being merged
> 2) choose an exisiting merge commit in the submodule repository (maybe after fetching it first)
> 3) create such a merge commit (what you are talking about here)

> I also agree that it is highly repository- and workflow-dependent what
> the "right" resolution is.

I replied to Elijah's message with some ideas I had about updating my
first step, but
to summarize I do agree that my current message doesn't account for
the fact that a
merge resolution may already exist, and the test cases you mention
serve as proof
that my current message is insufficient.

> I think moving it to merge_finalize / merge_switch_to_result is indeed a
> good suggestion, then we might be improving the UX across the board and not just
> for 'git merge'.

I'll spend some time looking into moving the suggestion code into
these functions. My
main concern is knowing which tests I would have to update to test
these changes.

On Sat, Jun 11, 2022 at 10:08 AM Philippe Blain
<levraiphilippeblain@gmail.com> wrote:
>
> Hi Calvin and Elijah,
>
> Le 2022-06-11 à 00:53, Elijah Newren a écrit :
> > On Fri, Jun 10, 2022 at 4:29 PM Calvin Wan <calvinwan@google.com> wrote:
> >>
> >> When attempting to do a non-fast-forward merge in a project with
> >> conflicts in the submodules, the merge fails and git prints the
> >> following error:
>
> I think we would want to be slightly more precise here, as
> "conflicts in the submodules" could be understood to mean:
>
> 1) conflicting submodule pointers in the superproject being merged
> 2) 1. + also content conflicts in the submodule merge
>
> Here we are just talking about 1.
>
> Also, the merge does not automatically fail, it only fails if
> a fast-forward *of the submodule pointer* is not possible, which
> might be what you meant above; but to me "a non-fast-forward merge in a project
> with conflict in the submodules" read like the non-fast-forwardness being talked about
> was in the superproject, not in the submodule(s).
>
> [message 0]
> >>
> >> Failed to merge submodule <submodules>
> >> CONFLICT (submodule):Merge conflict in <submodule>
> >> Automatic merge failed; fix conflicts and then commit the result.
>
>    Aside: the first <submodules> should be singular.
>
> This is indeed the output you get with the ort strategy if no existing merge commit
> exist in the submodule repository that merges the submodule pointers recorded
> in the superproject branches being merged. With the older "recursive" strategy,
> this message is:
>
> [message 1]
> Failed to merge submodule sub (merge following commits not found)
> Auto-merging sub
> CONFLICT (submodule): Merge conflict in sub
> Automatic merge failed; fix conflicts and then commit the result
>
> c73cda76b1 (merge-ort: copy and adapt merge_submodule()
> from merge-recursive.c, 2021-01-01) does not mention why that error message
> was changed, but perhaps it is just because it is slightly confusing
> to the user (they might not be expecting Git to look for an existing
> merge and so they don't know what merge the message is talking about).
>
> Maybe something like "failed to find existing commit merging <hash1> and <hash2>"
> would be clearer...
>
> >>
> >> Git is left in a conflicted state, which requires the user to:
> >>  1. merge submodules
> >>  2. add submodules changes to the superproject
> >>  3. merge superproject
> >
> > I think we may need to tweak these steps a bit:
> >
> >    1. merge submodules OR update submodules to an already existing
> > commit that reflects the merge (i.e. as submodules they may well be
> > independently actively developed.  Someone may have already merged the
> > appropriate branch/commit and the already extant merges should be used
> > in preference to creating new ones.)
> >    2. <just as you said>
> >    3. FINISH merging the superproject (i.e. don't redo the merge)
> >
> > I might be off on step 1; I have only used submodules extremely
> > lightly and usually only for a limited time, so I'm not really sure
> > what the expected workflow is.  I could also imagine it potentially
> > being repository-dependent whether you would want to merge or select
> > an appropriate commit to update to.
>
> I agree with Elijah here, the submodule conflcit resolution might be to:
>
> 1) just choose one of the existing submodule commits on either side of
> the superproject branches being merged
> 2) choose an exisiting merge commit in the submodule repository (maybe after fetching it first)
> 3) create such a merge commit (what you are talking about here)
>
> I also agree that it is highly repository- and workflow-dependent what
> the "right" resolution is.
>
> Note that the code does try to find an existing merge commit in the submodule
> repository, in this case the error message is different. If such a merge commit
> exists:
>
>     [message 2]
>     Failed to merge submodule sub, but a possible merge resolution exists:
>         aafcfa2 Merge branch 'sub-c' into sub-d
>
>
>     If this is correct simply add it to the index for example
>     by using:
>
>       git update-index --cacheinfo 160000 aafcfa2a62764282ab848d5d6bea86ba217c1b24 "sub"
>
>     which will accept this suggestion.
>
>     CONFLICT (submodule): Merge conflict in sub
>     Automatic merge failed; fix conflicts and then commit the result.
>
> if multiple merge exist:
>
>     [message 3]
>     Failed to merge submodule sub, but multiple possible merges exist:
>         2729a0c Merge branch 'sub-c' into ambiguous
>         aafcfa2 Merge branch 'sub-c' into sub-d
>
>     CONFLICT (submodule): Merge conflict in sub
>     Automatic merge failed; fix conflicts and then commit the result.
>
> Another aside, I really don't think we should instruct users to run
> plumbing like 'git update-index --cacheinfo' , they should just cd into
> the submodule and checkout the merge commit!
>
> >
> >> These steps are non-obvious for newer submodule users to figure out
> >> based on the error message and neither `git submodule status` nor `git
> >> status` provide any useful pointers.
> >>
> >> Update error message to the following when attempting to do a
> >> non-fast-forward merge in a project with conflicts in the submodules.
> >
> > Make sense.
>
> I agree that more guidance is a very nice addition.
>
> Regarding 'git status' output, it is downright confusing, since it says:
>
> Unmerged paths:
>   (use "git add <file>..." to mark resolution)
>         both modified:   sub
>
> which is not at all what you want to do most of the time (that would
> just stage whatever the currently checked out commit in the submodule is
> at the moment!)
>
> >
> >> The error message is based off of what would happen when `merge
> >> --recurse-submodules` is eventually supported
> >>
> >> Failed to merge submodule <submodule>
> >> CONFLICT (submodule): Merge conflict in <submodule>
> >> Automatic merge failed; recursive merging with submodules is currently
> >> not supported. To manually complete the merge:
> >>  - go to submodule (<submodule>), and merge commit <commit>
> >>  - come back to superproject, and `git add <submodule>` to record the above merge
> >>  - resolve any other conflicts in the superproject
> >>  - commit the resulting index in the superproject
> >
> > Ah, I see you've fixed step 3 here; that's good.
> >
> > However, these steps miss out on the merge-or-update submodule
> > possibility...and since you mention these steps are potentially the
> > basis for some future work, I think it's worth calling that out again.
> > I'm slightly worried that the 'update' part of merge-or-update may
> > throw a wrench in the plans for `merge --recurse-submodules`.
> >
>
> Slightly off topic here, but for me the most important improvement that
> 'git merge --recurse-submodules' would bring is when there is *no submodule
> conflicts*, i.e. one side fast-forwards the submodule and the other side
> does not touch it, since in that case the worktree of the submodule *is not updated*
> by the current code, which is one of the most confusing aspect of using submodules
> for new users ("why is "git status" and "git diff" not clean if "git merge"
> was fast-forward ?!?"), and the same is true (maybe more even so) for 'git rebase'.
>
> > (Also, continuing on the `merge --recurse-submodules` talent but
> > discussing a different aspect of it, I'm curious if you need to add
> > extra dirty-worktree/dirty-index checks for each submodule at the
> > start of a merge, whether you need to try to lock N indexes before
> > starting, and what other extra details are necessary.  But those are
> > probably questions to address whenever work on the future series to
> > implement this option is underway.)
> >
> >> Changes since v1:
> >>  - Removed advice to abort merge
> >>  - Error message updated to contain more commit/submodule information
> >>
> >> Signed-off-by: Calvin Wan <calvinwan@google.com>
> >>
> >> ---
> >>  builtin/merge.c            | 23 ++++++++++++++++++++++-
> >>  merge-ort.c                |  7 ++++++-
> >>  merge-recursive.c          |  7 ++++++-
> >>  merge-recursive.h          |  4 ++++
> >>  t/t6437-submodule-merge.sh |  5 ++++-
> >>  5 files changed, 42 insertions(+), 4 deletions(-)
> >
> > So you're modifying the "git merge" porcelain level (builtin/merge.c),
> > the two merges strategies, their common header, and adding some tests.
> > No other porcelains are modified...
> >
> >> diff --git a/builtin/merge.c b/builtin/merge.c
> >> index f178f5a3ee..7e2deea7fb 100644
> >> --- a/builtin/merge.c
> >> +++ b/builtin/merge.c
> >> @@ -88,6 +88,8 @@ static const char *sign_commit;
> >>  static int autostash;
> >>  static int no_verify;
> >>  static char *into_name;
> >> +static struct oid_array conflicted_submodule_oids = OID_ARRAY_INIT;
> >> +static struct string_list conflicted_submodule_paths = STRING_LIST_INIT_DUP;
> >>
> >>  static struct strategy all_strategy[] = {
> >>         { "recursive",  NO_TRIVIAL },
> >> @@ -734,6 +736,8 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
> >>                 }
> >>
> >>                 init_merge_options(&o, the_repository);
> >> +               o.conflicted_submodule_oids = &conflicted_submodule_oids;
> >> +               o.conflicted_submodule_paths = &conflicted_submodule_paths;
> >>                 if (!strcmp(strategy, "subtree"))
> >>                         o.subtree_shift = "";
> >>
> >> @@ -973,8 +977,25 @@ static int suggest_conflicts(void)
> >>         strbuf_release(&msgbuf);
> >>         fclose(fp);
> >>         repo_rerere(the_repository, allow_rerere_auto);
> >> -       printf(_("Automatic merge failed; "
> >> +       if (conflicted_submodule_oids.nr > 0) {
> >> +               int i;
> >> +               printf(_("Automatic merge failed; recursive merging with submodules is currently\n"
> >> +                       "not supported. To manually complete the merge:\n"));
> >> +               for (i = 0; i < conflicted_submodule_oids.nr; i++) {
> >> +                       printf(_(" - go to submodule (%s), and merge commit %s\n"),
> >> +                               conflicted_submodule_paths.items[i].string,
> >> +                               oid_to_hex(&conflicted_submodule_oids.oid[i]));
> >> +               }
> >> +               printf(_(" - come back to superproject, and `git add"));
> >> +               for (i = 0; i < conflicted_submodule_paths.nr; i++)
> >> +                       printf(_(" %s"), conflicted_submodule_paths.items[i].string);
> >> +               printf(_("` to record the above merge \n"
> >> +               " - resolve any other conflicts in the superproject\n"
> >> +               " - commit the resulting index in the superproject\n"));
> >> +       } else {
> >> +               printf(_("Automatic merge failed; "
> >>                         "fix conflicts and then commit the result.\n"));
> >> +       }
> >>         return 1;
> >>  }
> >
> > This is kind of nice.  I was worried you were going to embed these
> > messages in the merge strategies, which could cause problems for other
> > users of the merge strategies such as the --remerge-diff options to
> > git log and git show (your new messages would be unwanted noise or
> > even cause confusion there), and to the merge-tree work.  In fact, a
> > current submodule-merging message (search for "--cacheinfo") that is
> > potentially similar to what you are adding here but which was added at
> > the merge strategy level already feels highly problematic to me.  I've
> > been considering nuking it from the codebase for some time because of
> > those issues, though I guess just moving it out elsewhere may also
> > work.
> >
>
> Yes, this is the message I copied above. I agree that if we can tweak this
> advice to instead mention 'git checkout' and add it to the message
> that Calvin is adding in this series, it would make for a really better
> UX.
>
> > However, this implementation does have a drawback: these messages
> > won't appear for rebases, cherry-picks, reverts, attempted unstashing
> > (git stash apply/pop), or other actions unless you update the relevant
> > porcelains for those as well.
> >
> > A possible alternative here would be to move it to the level of
> > merge-recursive and merge-ort that is only called when the working
> > tree and index are updated.  For example, placing it in
> > merge_finalize() in merge-recursive.c and merge_switch_to_result() in
> > merge-ort.c -- next to the diff_warn_rename_limit() call in each case.
> > However, I'm also fine with keeping it at the porcelain level, it just
> > may need to be in a function that is called from several porcelains
> > that way.
>
> I think moving it to merge_finalize / merge_switch_to_result is indeed a
> good suggestion, then we might be improving the UX across the board and not just
> for 'git merge'.
>
> >
> >> diff --git a/merge-ort.c b/merge-ort.c
> >> index 0d3f42592f..c86ee11614 100644
> >> --- a/merge-ort.c
> >> +++ b/merge-ort.c
> >> @@ -3866,8 +3866,13 @@ static void process_entry(struct merge_options *opt,
> >>                         const char *reason = _("content");
> >>                         if (ci->filemask == 6)
> >>                                 reason = _("add/add");
> >> -                       if (S_ISGITLINK(merged_file.mode))
> >> +                       if (S_ISGITLINK(merged_file.mode)) {
> >>                                 reason = _("submodule");
> >> +                               if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> >> +                                       oid_array_append(opt->conflicted_submodule_oids, &merged_file.oid);
> >> +                                       string_list_append(opt->conflicted_submodule_paths, path);
> >> +                               }
> >> +                       }
> >>                         path_msg(opt, path, 0,
> >>                                  _("CONFLICT (%s): Merge conflict in %s"),
> >>                                  reason, path);
> >> diff --git a/merge-recursive.c b/merge-recursive.c
> >> index fd1bbde061..ff7cdbefe9 100644
> >> --- a/merge-recursive.c
> >> +++ b/merge-recursive.c
> >> @@ -3149,8 +3149,13 @@ static int handle_content_merge(struct merge_file_info *mfi,
> >>         }
> >>
> >>         if (!mfi->clean) {
> >> -               if (S_ISGITLINK(mfi->blob.mode))
> >> +               if (S_ISGITLINK(mfi->blob.mode)) {
> >>                         reason = _("submodule");
> >> +                       if (opt->conflicted_submodule_oids && opt->conflicted_submodule_paths) {
> >> +                               oid_array_append(opt->conflicted_submodule_oids, &mfi->blob.oid);
> >> +                               string_list_append(opt->conflicted_submodule_paths, path);
> >> +                       }
> >> +               }
> >>                 output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
> >>                                 reason, path);
> >>                 if (ci && !df_conflict_remains)
> >
> > Nice that the changes needed to both the ort and recursive strategies
> > are so localized.  :-)
> >
> >> diff --git a/merge-recursive.h b/merge-recursive.h
> >> index b88000e3c2..5d267e7a43 100644
> >> --- a/merge-recursive.h
> >> +++ b/merge-recursive.h
> >> @@ -51,6 +51,10 @@ struct merge_options {
> >>
> >>         /* internal fields used by the implementation */
> >>         struct merge_options_internal *priv;
> >> +
> >> +       /* fields that hold submodule conflict information */
> >> +       struct oid_array *conflicted_submodule_oids;
> >> +       struct string_list *conflicted_submodule_paths;
> >>  };
> >
> > Make sense.
> >
> >>  void init_merge_options(struct merge_options *opt, struct repository *repo);
> >> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> >> index 178413c22f..5b384dedc1 100755
> >> --- a/t/t6437-submodule-merge.sh
> >> +++ b/t/t6437-submodule-merge.sh
> >> @@ -141,6 +141,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
> >>                 test_must_fail git merge c 2> actual
> >>           fi &&
> >>          grep $(cat expect) actual > /dev/null &&
> >> +        test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >>          git reset --hard)
> >>  '
> >>
>
> In this test, an existing merge does exist, and is suggested to the user with [message 2] above.
> So telling users to do a new merge in the submodule is maybe to what we want.
>
> >> @@ -167,6 +168,7 @@ test_expect_success 'merging should fail for ambiguous common parent' '
> >>          fi &&
> >>         grep $(cat expect1) actual > /dev/null &&
> >>         grep $(cat expect2) actual > /dev/null &&
> >> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-b)" actual &&
> >>         git reset --hard)
> >>  '
>
> Here, 2 existing merges exist, and they are presented to the users with [message 3] above.
> So again we might not want to tell users to do a new merge.
>
> >>
> >> @@ -205,7 +207,8 @@ test_expect_success 'merging should fail for changes that are backwards' '
> >>         git commit -a -m "f" &&
> >>
> >>         git checkout -b test-backward e &&
> >> -       test_must_fail git merge f)
> >> +       test_must_fail git merge f >actual &&
> >> +       test_i18ngrep "go to submodule (sub), and merge commit $(git -C sub rev-parse sub-a)" actual)
> >
> > test_i18ngrep is apparently on the way out:
> >
> >     $ grep -B 3 ^test_i18ngrep t/test-lib-functions.sh
> >     # Wrapper for grep which used to be used for
> >     # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other
> >     # in-flight changes. Should not be used and will be removed soon.
> >     test_i18ngrep () {
> >
> > I think you just want to use grep instead here for each of these hunks.
> >
>
> Here, one side regresses the submodule commit and the other side advances it.
> In this case, I really think the right resolution is to choose one side or
> the other, and not suggest to do a merge at all. So that means that we might
> want to tweak the advice we give based on the type of submodule conflict...
>
> Also, maybe we would want a new test that reproduces exactly the conditions of
> [message 1], i.e. no existing merge exists in the submodule.
>
> Thanks a lot for wanting to improve the submodule UX!
>
> Cheers,
>
> Philippe.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v3] submodule merge: update conflict error message
  2022-06-10 23:11 ` [PATCH v2] " Calvin Wan
  2022-06-11  4:53   ` Elijah Newren
@ 2022-06-29 22:40   ` Calvin Wan
  2022-06-30  2:40     ` Elijah Newren
                       ` (2 more replies)
  1 sibling, 3 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-29 22:40 UTC (permalink / raw)
  To: git; +Cc: chooglen, gitster, newren, levraiphilippeblain, Calvin Wan

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded, the merge fails and git prints
the following error:

Failed to merge submodule <submodule>
CONFLICT (submodule):Merge conflict in <submodule>
Automatic merge failed; fix conflicts and then commit the result.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers. 

Update error message to match the steps above. If git does not detect a 
merge resolution, the following is printed:

--------

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; recursive merging with submodules is currently
not supported. To manually complete the merge:
 - go to submodule (<submodule>), and merge commit <commit>
 - come back to superproject, and `git add <submodule>` to record the above merge 
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

--------

If git detects a possible merge resolution, the following is printed:

--------

Failed to merge submodule sub, but a possible merge resolution exists:
    <commit> Merge branch '<branch1>' into <branch2>


If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 <commit> "<submodule>"

which will accept this suggestion.

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules is currently not supported.
To manually complete the merge:
 - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
 - come back to superproject, and `git add <submodule>` to record the above merge 
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

--------

If git detects multiple possible merge resolutions, the following is printed:

--------

Failed to merge submodule sub, but multiple possible merges exist:
    <commit> Merge branch '<branch1>' into <branch2>
    <commit> Merge branch '<branch1>' into <branch3>

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules is currently not supported.
To manually complete the merge:
 - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
 - come back to superproject, and `git add <submodule>` to record the above merge 
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

-------

Changes since v2:
There are three major changes in this patch thanks to all the helpful
feedback! I have moved the print function from builtin/merge.c to
merge-ort.c and added it to merge_finalize() in merge-recursive.c and
merge_switch_to_result() in merge-ort.c. This allows other merge
machinery commands besides merge to print submodule conflict advice.

I have moved the check for submodule conflicts from process_entry() to 
merge_submodule(). This also required removing the early returns and
instead going to my submodule conflict check allowing us to pass back
information on whether git detected a possible merge resolution or not.

I have also updated the error message to better reflect the merge
status. Specifically, if git detects a possible merge resolution, the
error message now also suggest updating to one of those resolutions.

Other small changes: 
 - Moved fields that hold submodule conflict information to new object
 - Shortened printed commit id to DEFAULT_ABBREV
 - Added a test to t6437-submodule-merge.sh for merges with no
   resolution existence
 - Modified a test in t7402-submodule-rebase to show error message
   prints in other parts of the merge machinery

Changes since v1:
 - Removed advice to abort merge
 - Error message updated to contain more commit/submodule information

Signed-off-by: Calvin Wan <calvinwan@google.com>
---
 merge-ort.c                 | 53 ++++++++++++++++++++++++++++++++++---
 merge-recursive.c           | 26 +++++++++++++++---
 merge-recursive.h           | 18 +++++++++++++
 t/t6437-submodule-merge.sh  | 36 ++++++++++++++++++++++---
 t/t7402-submodule-rebase.sh |  6 +++--
 5 files changed, 125 insertions(+), 14 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 0d3f42592f..2d9f03ea8c 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -1610,6 +1610,9 @@ static int merge_submodule(struct merge_options *opt,
 	struct commit *commit_o, *commit_a, *commit_b;
 	int parent_count;
 	struct object_array merges;
+	struct conflicted_submodule_list *csub = opt->conflicted_submodules;
+	struct conflicted_submodule_item csub_item;
+	int resolution_exists = 0;
 
 	int i;
 	int search = !opt->priv->call_depth;
@@ -1619,17 +1622,17 @@ static int merge_submodule(struct merge_options *opt,
 
 	/* we can not handle deletion conflicts */
 	if (is_null_oid(o))
-		return 0;
+		goto ret;
 	if (is_null_oid(a))
-		return 0;
+		goto ret;
 	if (is_null_oid(b))
-		return 0;
+		goto ret;
 
 	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
 		path_msg(opt, path, 0,
 				_("Failed to merge submodule %s (not checked out)"),
 				path);
-		return 0;
+		goto ret;
 	}
 
 	if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
@@ -1703,6 +1706,7 @@ static int merge_submodule(struct merge_options *opt,
 			   "which will accept this suggestion.\n"),
 			 oid_to_hex(&merges.objects[0].item->oid), path);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 		break;
 	default:
 		for (i = 0; i < merges.nr; i++)
@@ -1712,11 +1716,24 @@ static int merge_submodule(struct merge_options *opt,
 			 _("Failed to merge submodule %s, but multiple "
 			   "possible merges exist:\n%s"), path, sb.buf);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 	}
 
 	object_array_clear(&merges);
 cleanup:
 	repo_clear(&subrepo);
+ret:
+	if (!ret) {
+		if (!csub) {
+			CALLOC_ARRAY(csub, 1);
+		}
+		csub_item.oid = *result;
+		csub_item.path = xstrdup(path);
+		csub_item.resolution_exists = resolution_exists;
+		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
+		csub->items[csub->nr++] = csub_item;
+		opt->conflicted_submodules = csub;
+	}
 	return ret;
 }
 
@@ -4256,6 +4273,32 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
+	if (csub && csub->nr > 0) {
+		int i;
+		printf(_("Recursive merging with submodules is currently not supported.\n"
+			"To manually complete the merge:\n"));
+		for (i = 0; i < csub->nr; i++) {
+			const struct object_id id = csub->items[i].oid;
+			if (csub->items[i].resolution_exists)
+				printf(_(" - go to submodule (%s), and either update the submodule "
+					"to a possible commit above or merge commit %s\n"),
+					csub->items[i].path,
+					find_unique_abbrev(&id, DEFAULT_ABBREV));
+			else
+				printf(_(" - go to submodule (%s), and merge commit %s\n"),
+					csub->items[i].path,
+					find_unique_abbrev(&id, DEFAULT_ABBREV));
+		}
+		printf(_(" - come back to superproject, and `git add"));
+		for (i = 0; i < csub->nr; i++)
+			printf(_(" %s"), csub->items[i].path);
+		printf(_("` to record the above merge \n"
+			" - resolve any other conflicts in the superproject\n"
+			" - commit the resulting index in the superproject\n"));
+	}
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4324,6 +4367,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		}
 		string_list_clear(&olist, 0);
 
+		print_submodule_conflict_suggestion(opt->conflicted_submodules);
+
 		/* Also include needed rename limit adjustment now */
 		diff_warn_rename_limit("merge.renamelimit",
 				       opti->renames.needed_limit, 0);
diff --git a/merge-recursive.c b/merge-recursive.c
index fd1bbde061..311cc37886 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -1190,6 +1190,9 @@ static int merge_submodule(struct merge_options *opt,
 	struct commit *commit_base, *commit_a, *commit_b;
 	int parent_count;
 	struct object_array merges;
+	struct conflicted_submodule_list *csub = opt->conflicted_submodules;
+	struct conflicted_submodule_item csub_item;
+	int resolution_exists = 0;
 
 	int i;
 	int search = !opt->priv->call_depth;
@@ -1204,15 +1207,15 @@ static int merge_submodule(struct merge_options *opt,
 
 	/* we can not handle deletion conflicts */
 	if (is_null_oid(base))
-		return 0;
+		goto ret;
 	if (is_null_oid(a))
-		return 0;
+		goto ret;
 	if (is_null_oid(b))
-		return 0;
+		goto ret;
 
 	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
 		output(opt, 1, _("Failed to merge submodule %s (not checked out)"), path);
-		return 0;
+		goto ret;
 	}
 
 	if (!(commit_base = lookup_commit_reference(&subrepo, base)) ||
@@ -1287,17 +1290,31 @@ static int merge_submodule(struct merge_options *opt,
 		       "  git update-index --cacheinfo 160000 %s \"%s\"\n\n"
 		       "which will accept this suggestion.\n"),
 		       oid_to_hex(&merges.objects[0].item->oid), path);
+		resolution_exists = 1;
 		break;
 
 	default:
 		output(opt, 1, _("Failed to merge submodule %s (multiple merges found)"), path);
 		for (i = 0; i < merges.nr; i++)
 			print_commit(&subrepo, (struct commit *) merges.objects[i].item);
+		resolution_exists = 1;
 	}
 
 	object_array_clear(&merges);
 cleanup:
 	repo_clear(&subrepo);
+ret:
+	if (!ret) {
+		if (!csub) {
+			CALLOC_ARRAY(csub, 1);
+		}
+		csub_item.oid = *result;
+		csub_item.path = xstrdup(path);
+		csub_item.resolution_exists = resolution_exists;
+		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
+		csub->items[csub->nr++] = csub_item;
+		opt->conflicted_submodules = csub;
+	}
 	return ret;
 }
 
@@ -3736,6 +3753,7 @@ static void merge_finalize(struct merge_options *opt)
 	flush_output(opt);
 	if (!opt->priv->call_depth && opt->buffer_output < 2)
 		strbuf_release(&opt->obuf);
+	print_submodule_conflict_suggestion(opt->conflicted_submodules);
 	if (show(opt, 2))
 		diff_warn_rename_limit("merge.renamelimit",
 				       opt->priv->needed_rename_limit, 0);
diff --git a/merge-recursive.h b/merge-recursive.h
index b88000e3c2..b615955fb7 100644
--- a/merge-recursive.h
+++ b/merge-recursive.h
@@ -9,6 +9,7 @@ struct object_id;
 struct repository;
 struct tree;
 
+struct conflicted_submodule_list;
 struct merge_options_internal;
 struct merge_options {
 	struct repository *repo;
@@ -51,6 +52,21 @@ struct merge_options {
 
 	/* internal fields used by the implementation */
 	struct merge_options_internal *priv;
+
+	/* field that holds submodule conflict information */
+	struct conflicted_submodule_list *conflicted_submodules;
+};
+
+struct conflicted_submodule_item {
+	struct object_id oid;
+	char *path;
+	int resolution_exists;
+};
+
+struct conflicted_submodule_list {
+	struct conflicted_submodule_item *items;
+	size_t nr;
+	size_t alloc;
 };
 
 void init_merge_options(struct merge_options *opt, struct repository *repo);
@@ -122,4 +138,6 @@ int merge_recursive_generic(struct merge_options *opt,
 			    const struct object_id **merge_bases,
 			    struct commit **result);
 
+void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub);
+
 #endif
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index 178413c22f..f0a31c3a61 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,28 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
+
+test_expect_success 'merging should conflict for non fast-forward' '
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	 (cd sub &&
+	  git rev-parse --short sub-b > ../expect) &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi &&
+	 grep $(cat expect) actual > /dev/null &&
+	 sub_expect="go to submodule (sub), and merge commit $(git -C sub rev-parse --short sub-b)" &&
+	 grep "$sub_expect" actual &&
+	 git reset --hard)
+'
 
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,9 +149,9 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
@@ -141,6 +161,9 @@ test_expect_success 'merging should conflict for non fast-forward' '
 		test_must_fail git merge c 2> actual
 	  fi &&
 	 grep $(cat expect) actual > /dev/null &&
+	 sub_expect="go to submodule (sub), and either update the submodule to a \
+possible commit above or merge commit $(git -C sub rev-parse --short sub-b)" &&
+	 grep "$sub_expect" actual &&
 	 git reset --hard)
 '
 
@@ -167,6 +190,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 fi &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
+	sub_expect="go to submodule (sub), and either update the submodule to a \
+possible commit above or merge commit $(git -C sub rev-parse --short sub-b)" &&
+	grep "$sub_expect" actual &&
 	git reset --hard)
 '
 
@@ -205,7 +231,9 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	sub_expect="go to submodule (sub), and merge commit $(git -C sub rev-parse --short sub-a)" &&
+	grep "$sub_expect" actual)
 '
 
 
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..21c8b4e97c 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,9 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	sub_expect="go to submodule (submodule), and merge commit $(git -C submodule rev-parse --short HEAD^^)" &&
+	grep "$sub_expect" actual_output
 '
 
 test_done

base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
-- 
2.37.0.rc0.161.g10f37bed90-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
@ 2022-06-30  2:40     ` Elijah Newren
  2022-06-30 19:48       ` Calvin Wan
  2022-06-30 20:35     ` Glen Choo
  2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
  2 siblings, 1 reply; 60+ messages in thread
From: Elijah Newren @ 2022-06-30  2:40 UTC (permalink / raw)
  To: Calvin Wan; +Cc: Git Mailing List, Glen Choo, Junio C Hamano, Philippe Blain

Hi,

On Wed, Jun 29, 2022 at 3:41 PM Calvin Wan <calvinwan@google.com> wrote:
>
> When attempting to merge in a superproject with conflicting submodule
> pointers that cannot be fast-forwarded, the merge fails and git prints
> the following error:
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule):Merge conflict in <submodule>
> Automatic merge failed; fix conflicts and then commit the result.
>
> Git is left in a conflicted state, which requires the user to:
>  1. merge submodules or update submodules to an already existing
>         commit that reflects the merge
>  2. add submodules changes to the superproject
>  3. finish merging superproject
> These steps are non-obvious for newer submodule users to figure out
> based on the error message and neither `git submodule status` nor `git
> status` provide any useful pointers.
>
> Update error message to match the steps above. If git does not detect a
> merge resolution, the following is printed:
>
> --------
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Automatic merge failed; recursive merging with submodules is currently
> not supported. To manually complete the merge:
>  - go to submodule (<submodule>), and merge commit <commit>

I'm still a little unsure on this.  The merge_submodule() logic we
have may not have detected a merge commit that merges the two branches
together, but that doesn't automatically imply a new merge must be
created in the submodule to resolve this conflict.  There might be
various reasons that other existing commits in the submodule could be
used as the "correct" update.

Perhaps that is uncommon enough to not be worth mentioning; I'm just a
little hesitant to treat the merge_submodule() logic as robust and
finding the only choices users might want to use.  If we do keep the
wording as-is, it might be nice to at least discuss in the commit
message why we chose that and ignored the or-update-submodule option
in this case.

>  - come back to superproject, and `git add <submodule>` to record the above merge
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> --------
>
> If git detects a possible merge resolution, the following is printed:
>
> --------
>
> Failed to merge submodule sub, but a possible merge resolution exists:
>     <commit> Merge branch '<branch1>' into <branch2>
>
>
> If this is correct simply add it to the index for example
> by using:
>
>   git update-index --cacheinfo 160000 <commit> "<submodule>"
>
> which will accept this suggestion.

The last few lines above will no longer be part of the output once
en/merge-tree is merged; see commit a4040cfa35 (merge-ort: remove
command-line-centric submodule message from merge-ort, 2022-06-18).

> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules is currently not supported.
> To manually complete the merge:
>  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>

Again, I'm hesitant to treat the suggestions from merge_submodule() as
the only possibilities.  For example, perhaps the user will want to
pick a commit that contains one of the ones found by merge_submodule()
in its history -- perhaps because the newer commit they want to select
contains an important bugfix for an issue not discovered at the time
of the merge in the submodule.

Also, I'm worried your sentence may be easy to misunderstand, due to
it being ambiguous whether "merge" is a verb or an adjective.  More
precisely, your sentence could be read as "update the submodule to a
possible commit above, or update the submodule to merge commit
<commit>" and then users say, "But <commit> isn't a merge commit; why
does this message claim it is?  Do I just update the submodule to that
commit anyway?"  Or perhaps users just update it to <commit> without
even checking to find out that it's not a merge commit, with the
deleterious consequences of missing all the important changes
currently found in the submodule.

>  - come back to superproject, and `git add <submodule>` to record the above merge

"...to record the above merge or update"?

>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> --------
>
> If git detects multiple possible merge resolutions, the following is printed:
>
> --------
>
> Failed to merge submodule sub, but multiple possible merges exist:
>     <commit> Merge branch '<branch1>' into <branch2>
>     <commit> Merge branch '<branch1>' into <branch3>
>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules is currently not supported.
> To manually complete the merge:
>  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>

Same issues as I mentioned above for the single-merge-resolution-found case.

>  - come back to superproject, and `git add <submodule>` to record the above merge

"merge or update", right?

>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> -------
>
> Changes since v2:
> There are three major changes in this patch thanks to all the helpful
> feedback! I have moved the print function from builtin/merge.c to
> merge-ort.c and added it to merge_finalize() in merge-recursive.c and
> merge_switch_to_result() in merge-ort.c. This allows other merge
> machinery commands besides merge to print submodule conflict advice.
>
> I have moved the check for submodule conflicts from process_entry() to
> merge_submodule(). This also required removing the early returns and
> instead going to my submodule conflict check allowing us to pass back
> information on whether git detected a possible merge resolution or not.
>
> I have also updated the error message to better reflect the merge
> status. Specifically, if git detects a possible merge resolution, the
> error message now also suggest updating to one of those resolutions.
>
> Other small changes:
>  - Moved fields that hold submodule conflict information to new object
>  - Shortened printed commit id to DEFAULT_ABBREV
>  - Added a test to t6437-submodule-merge.sh for merges with no
>    resolution existence
>  - Modified a test in t7402-submodule-rebase to show error message
>    prints in other parts of the merge machinery
>
> Changes since v1:
>  - Removed advice to abort merge
>  - Error message updated to contain more commit/submodule information
>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
> ---
>  merge-ort.c                 | 53 ++++++++++++++++++++++++++++++++++---
>  merge-recursive.c           | 26 +++++++++++++++---
>  merge-recursive.h           | 18 +++++++++++++
>  t/t6437-submodule-merge.sh  | 36 ++++++++++++++++++++++---
>  t/t7402-submodule-rebase.sh |  6 +++--
>  5 files changed, 125 insertions(+), 14 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 0d3f42592f..2d9f03ea8c 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -1610,6 +1610,9 @@ static int merge_submodule(struct merge_options *opt,
>         struct commit *commit_o, *commit_a, *commit_b;
>         int parent_count;
>         struct object_array merges;
> +       struct conflicted_submodule_list *csub = opt->conflicted_submodules;
> +       struct conflicted_submodule_item csub_item;
> +       int resolution_exists = 0;
>
>         int i;
>         int search = !opt->priv->call_depth;
> @@ -1619,17 +1622,17 @@ static int merge_submodule(struct merge_options *opt,
>
>         /* we can not handle deletion conflicts */
>         if (is_null_oid(o))
> -               return 0;
> +               goto ret;
>         if (is_null_oid(a))
> -               return 0;
> +               goto ret;
>         if (is_null_oid(b))
> -               return 0;
> +               goto ret;
>
>         if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
>                 path_msg(opt, path, 0,
>                                 _("Failed to merge submodule %s (not checked out)"),
>                                 path);
> -               return 0;
> +               goto ret;
>         }
>
>         if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
> @@ -1703,6 +1706,7 @@ static int merge_submodule(struct merge_options *opt,
>                            "which will accept this suggestion.\n"),
>                          oid_to_hex(&merges.objects[0].item->oid), path);
>                 strbuf_release(&sb);
> +               resolution_exists = 1;
>                 break;
>         default:
>                 for (i = 0; i < merges.nr; i++)
> @@ -1712,11 +1716,24 @@ static int merge_submodule(struct merge_options *opt,
>                          _("Failed to merge submodule %s, but multiple "
>                            "possible merges exist:\n%s"), path, sb.buf);
>                 strbuf_release(&sb);
> +               resolution_exists = 1;
>         }
>
>         object_array_clear(&merges);
>  cleanup:
>         repo_clear(&subrepo);
> +ret:
> +       if (!ret) {
> +               if (!csub) {
> +                       CALLOC_ARRAY(csub, 1);
> +               }
> +               csub_item.oid = *result;

Shouldn't this be "*b" rather than "*result"?

Also, are we risking telling the user to "merge commit
0000000000000000000000000000000000000000" from the submodule, given
some of the early exits that you redirected to come through here?

> +               csub_item.path = xstrdup(path);
> +               csub_item.resolution_exists = resolution_exists;
> +               ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
> +               csub->items[csub->nr++] = csub_item;
> +               opt->conflicted_submodules = csub;
> +       }
>         return ret;
>  }
>
> @@ -4256,6 +4273,32 @@ static int record_conflicted_index_entries(struct merge_options *opt)
>         return errs;
>  }
>
> +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {

Maybe just make this function be static?

> +       if (csub && csub->nr > 0) {
> +               int i;
> +               printf(_("Recursive merging with submodules is currently not supported.\n"
> +                       "To manually complete the merge:\n"));
> +               for (i = 0; i < csub->nr; i++) {
> +                       const struct object_id id = csub->items[i].oid;
> +                       if (csub->items[i].resolution_exists)
> +                               printf(_(" - go to submodule (%s), and either update the submodule "
> +                                       "to a possible commit above or merge commit %s\n"),
> +                                       csub->items[i].path,
> +                                       find_unique_abbrev(&id, DEFAULT_ABBREV));

Shouldn't this be repo_find_unique_abbrev(), and in particular with
the submodule repository being passed to it?  Or perhaps using the
format_commit() function, since merge_submodule() is already using it
for the earlier submodule-related messages?

> +                       else
> +                               printf(_(" - go to submodule (%s), and merge commit %s\n"),
> +                                       csub->items[i].path,
> +                                       find_unique_abbrev(&id, DEFAULT_ABBREV));

Likewise?

> +               }
> +               printf(_(" - come back to superproject, and `git add"));
> +               for (i = 0; i < csub->nr; i++)
> +                       printf(_(" %s"), csub->items[i].path);
> +               printf(_("` to record the above merge \n"
> +                       " - resolve any other conflicts in the superproject\n"
> +                       " - commit the resulting index in the superproject\n"));
> +       }
> +}
> +
>  void merge_switch_to_result(struct merge_options *opt,
>                             struct tree *head,
>                             struct merge_result *result,
> @@ -4324,6 +4367,8 @@ void merge_switch_to_result(struct merge_options *opt,
>                 }
>                 string_list_clear(&olist, 0);
>
> +               print_submodule_conflict_suggestion(opt->conflicted_submodules);
> +
>                 /* Also include needed rename limit adjustment now */
>                 diff_warn_rename_limit("merge.renamelimit",
>                                        opti->renames.needed_limit, 0);
> diff --git a/merge-recursive.c b/merge-recursive.c
> index fd1bbde061..311cc37886 100644
> --- a/merge-recursive.c
> +++ b/merge-recursive.c

Is it worth updating merge-recursive.c here?  I'd rather only give it
security fix updates until we delete it.

> @@ -1190,6 +1190,9 @@ static int merge_submodule(struct merge_options *opt,
>         struct commit *commit_base, *commit_a, *commit_b;
>         int parent_count;
>         struct object_array merges;
> +       struct conflicted_submodule_list *csub = opt->conflicted_submodules;
> +       struct conflicted_submodule_item csub_item;
> +       int resolution_exists = 0;
>
>         int i;
>         int search = !opt->priv->call_depth;
> @@ -1204,15 +1207,15 @@ static int merge_submodule(struct merge_options *opt,
>
>         /* we can not handle deletion conflicts */
>         if (is_null_oid(base))
> -               return 0;
> +               goto ret;
>         if (is_null_oid(a))
> -               return 0;
> +               goto ret;
>         if (is_null_oid(b))
> -               return 0;
> +               goto ret;
>
>         if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
>                 output(opt, 1, _("Failed to merge submodule %s (not checked out)"), path);
> -               return 0;
> +               goto ret;
>         }
>
>         if (!(commit_base = lookup_commit_reference(&subrepo, base)) ||
> @@ -1287,17 +1290,31 @@ static int merge_submodule(struct merge_options *opt,
>                        "  git update-index --cacheinfo 160000 %s \"%s\"\n\n"
>                        "which will accept this suggestion.\n"),
>                        oid_to_hex(&merges.objects[0].item->oid), path);
> +               resolution_exists = 1;
>                 break;
>
>         default:
>                 output(opt, 1, _("Failed to merge submodule %s (multiple merges found)"), path);
>                 for (i = 0; i < merges.nr; i++)
>                         print_commit(&subrepo, (struct commit *) merges.objects[i].item);
> +               resolution_exists = 1;
>         }
>
>         object_array_clear(&merges);
>  cleanup:
>         repo_clear(&subrepo);
> +ret:
> +       if (!ret) {
> +               if (!csub) {
> +                       CALLOC_ARRAY(csub, 1);
> +               }
> +               csub_item.oid = *result;
> +               csub_item.path = xstrdup(path);
> +               csub_item.resolution_exists = resolution_exists;
> +               ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
> +               csub->items[csub->nr++] = csub_item;
> +               opt->conflicted_submodules = csub;
> +       }
>         return ret;
>  }
>
> @@ -3736,6 +3753,7 @@ static void merge_finalize(struct merge_options *opt)
>         flush_output(opt);
>         if (!opt->priv->call_depth && opt->buffer_output < 2)
>                 strbuf_release(&opt->obuf);
> +       print_submodule_conflict_suggestion(opt->conflicted_submodules);
>         if (show(opt, 2))
>                 diff_warn_rename_limit("merge.renamelimit",
>                                        opt->priv->needed_rename_limit, 0);
> diff --git a/merge-recursive.h b/merge-recursive.h
> index b88000e3c2..b615955fb7 100644
> --- a/merge-recursive.h
> +++ b/merge-recursive.h
> @@ -9,6 +9,7 @@ struct object_id;
>  struct repository;
>  struct tree;
>
> +struct conflicted_submodule_list;
>  struct merge_options_internal;
>  struct merge_options {
>         struct repository *repo;
> @@ -51,6 +52,21 @@ struct merge_options {
>
>         /* internal fields used by the implementation */
>         struct merge_options_internal *priv;
> +
> +       /* field that holds submodule conflict information */
> +       struct conflicted_submodule_list *conflicted_submodules;

I think this should be added to merge_options_internal rather than to
merge_options.  There's no need for an API caller to make use of
these.

Also, if a clear need arises later for API callers to make use of this
information, then it should be part of merge_result for merge-ort.c,
not part of merge_options.

> +};
> +
> +struct conflicted_submodule_item {
> +       struct object_id oid;
> +       char *path;
> +       int resolution_exists;
> +};
> +
> +struct conflicted_submodule_list {
> +       struct conflicted_submodule_item *items;
> +       size_t nr;
> +       size_t alloc;
>  };

Similarly, I think these should be defined in merge-ort.c (and maybe
also merge-recursive.c) near struct merge_options_internal.

>  void init_merge_options(struct merge_options *opt, struct repository *repo);
> @@ -122,4 +138,6 @@ int merge_recursive_generic(struct merge_options *opt,
>                             const struct object_id **merge_bases,
>                             struct commit **result);
>
> +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub);

and I think there's no reason to put this in the header file; it
should just be a static function in merge-ort.c.  (And, if we really
want to update merge-recursive.c, then copy the function over there.
*Nothing* in merge-ort.c should call a function in merge-recursive.c
and similarly nothing in merge-recursive.c should call any function in
merge-ort.c.  Yes, that implies some duplication.  There is a fair
amount already and we've discussed it, and chosen against creating a
shared module as well.  I want merge-recursive to not be broken by
merge-ort updates; it should remain stable until the day we finally
get to nuke it.  I'm personally of the opinion that merge-recursive
should only get security fixes at this point, though I haven't pinged
to see if other folks agree with that point of view yet or not.  I'm
not wasting my time fixing/improving it, though...)

> +
>  #endif
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index 178413c22f..f0a31c3a61 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -103,8 +103,28 @@ test_expect_success 'setup for merge search' '
>          echo "file-c" > file-c &&
>          git add file-c &&
>          git commit -m "sub-c") &&
> -       git commit -a -m "c" &&
> +       git commit -a -m "c")
> +'
> +
> +test_expect_success 'merging should conflict for non fast-forward' '
> +       (cd merge-search &&
> +        git checkout -b test-nonforward-a b &&
> +        (cd sub &&
> +         git rev-parse --short sub-b > ../expect) &&
> +         if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +         then
> +               test_must_fail git merge c >actual
> +         else
> +               test_must_fail git merge c 2> actual
> +         fi &&
> +        grep $(cat expect) actual > /dev/null &&
> +        sub_expect="go to submodule (sub), and merge commit $(git -C sub rev-parse --short sub-b)" &&
> +        grep "$sub_expect" actual &&
> +        git reset --hard)
> +'
>
> +test_expect_success 'finish setup for merge-search' '
> +       (cd merge-search &&
>         git checkout -b d a &&
>         (cd sub &&
>          git checkout -b sub-d sub-b &&
> @@ -129,9 +149,9 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
>          test_cmp expect actual)
>  '
>
> -test_expect_success 'merging should conflict for non fast-forward' '
> +test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
>         (cd merge-search &&
> -        git checkout -b test-nonforward b &&
> +        git checkout -b test-nonforward-b b &&
>          (cd sub &&
>           git rev-parse sub-d > ../expect) &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> @@ -141,6 +161,9 @@ test_expect_success 'merging should conflict for non fast-forward' '
>                 test_must_fail git merge c 2> actual
>           fi &&
>          grep $(cat expect) actual > /dev/null &&
> +        sub_expect="go to submodule (sub), and either update the submodule to a \
> +possible commit above or merge commit $(git -C sub rev-parse --short sub-b)" &&
> +        grep "$sub_expect" actual &&
>          git reset --hard)
>  '
>
> @@ -167,6 +190,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          fi &&
>         grep $(cat expect1) actual > /dev/null &&
>         grep $(cat expect2) actual > /dev/null &&
> +       sub_expect="go to submodule (sub), and either update the submodule to a \
> +possible commit above or merge commit $(git -C sub rev-parse --short sub-b)" &&
> +       grep "$sub_expect" actual &&
>         git reset --hard)
>  '
>
> @@ -205,7 +231,9 @@ test_expect_success 'merging should fail for changes that are backwards' '
>         git commit -a -m "f" &&
>
>         git checkout -b test-backward e &&
> -       test_must_fail git merge f)
> +       test_must_fail git merge f >actual &&
> +       sub_expect="go to submodule (sub), and merge commit $(git -C sub rev-parse --short sub-a)" &&
> +       grep "$sub_expect" actual)
>  '
>
>
> diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
> index 8e32f19007..21c8b4e97c 100755
> --- a/t/t7402-submodule-rebase.sh
> +++ b/t/t7402-submodule-rebase.sh
> @@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
>         test_tick &&
>         git commit -m fourth &&
>
> -       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
> +       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
>         git ls-files -s submodule >actual &&
>         (
>                 cd submodule &&
> @@ -112,7 +112,9 @@ test_expect_success 'rebasing submodule that should conflict' '
>                 echo "160000 $(git rev-parse HEAD^^) 2  submodule" &&
>                 echo "160000 $(git rev-parse HEAD) 3    submodule"
>         ) >expect &&
> -       test_cmp expect actual
> +       test_cmp expect actual &&
> +       sub_expect="go to submodule (submodule), and merge commit $(git -C submodule rev-parse --short HEAD^^)" &&
> +       grep "$sub_expect" actual_output
>  '
>
>  test_done
>
> base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
> --
> 2.37.0.rc0.161.g10f37bed90-goog

Despite my many nitpicks and whatnot, it looks like your change will
make things nicer for the user, and I think your patch is coming along
nicely.  There are some things to fix up, but the overall direction
seems good.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-30  2:40     ` Elijah Newren
@ 2022-06-30 19:48       ` Calvin Wan
  2022-07-01  4:27         ` Elijah Newren
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-06-30 19:48 UTC (permalink / raw)
  To: git, Elijah Newren; +Cc: Calvin Wan, Glen Choo, Junio C Hamano, Philippe Blain

> > Failed to merge submodule <submodule>
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Automatic merge failed; recursive merging with submodules is currently
> > not supported. To manually complete the merge:
> >  - go to submodule (<submodule>), and merge commit <commit>
> 
> I'm still a little unsure on this.  The merge_submodule() logic we
> have may not have detected a merge commit that merges the two branches
> together, but that doesn't automatically imply a new merge must be
> created in the submodule to resolve this conflict.  There might be
> various reasons that other existing commits in the submodule could be
> used as the "correct" update.
> 
> Perhaps that is uncommon enough to not be worth mentioning; I'm just a
> little hesitant to treat the merge_submodule() logic as robust and
> finding the only choices users might want to use.  If we do keep the
> wording as-is, it might be nice to at least discuss in the commit
> message why we chose that and ignored the or-update-submodule option
> in this case.

You make a good point about merge_submodule() possibly not finding all
of the right choices -- I'll add back the or-update-submodule option
back.

> > If this is correct simply add it to the index for example
> > by using:
> >
> >   git update-index --cacheinfo 160000 <commit> "<submodule>"
> >
> > which will accept this suggestion.
> 
> The last few lines above will no longer be part of the output once
> en/merge-tree is merged; see commit a4040cfa35 (merge-ort: remove
> command-line-centric submodule message from merge-ort, 2022-06-18).

ack

> > CONFLICT (submodule): Merge conflict in <submodule>
> > Recursive merging with submodules is currently not supported.
> > To manually complete the merge:
> >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> 
> Again, I'm hesitant to treat the suggestions from merge_submodule() as
> the only possibilities.  For example, perhaps the user will want to
> pick a commit that contains one of the ones found by merge_submodule()
> in its history -- perhaps because the newer commit they want to select
> contains an important bugfix for an issue not discovered at the time
> of the merge in the submodule.
> 
> Also, I'm worried your sentence may be easy to misunderstand, due to
> it being ambiguous whether "merge" is a verb or an adjective.  More
> precisely, your sentence could be read as "update the submodule to a
> possible commit above, or update the submodule to merge commit
> <commit>" and then users say, "But <commit> isn't a merge commit; why
> does this message claim it is?  Do I just update the submodule to that
> commit anyway?"  Or perhaps users just update it to <commit> without
> even checking to find out that it's not a merge commit, with the
> deleterious consequences of missing all the important changes
> currently found in the submodule.

I agree this sentence can be misinterpreted. I also think the main
reason my current message is unclear is because there is not enough
context for the user to understand why the merge failed. In a standard
merge, the reason in "CONFLICT (<reason>)" provides context, whereas in
this case, "CONFLICT (submodule)" only tells the user that the submodule
is conflicted in some way. The user is unaware that we attempted to
fast-forward the submodule, failed, and now require the user to either
update the commit or merge the commit. How does this rewording sound?

 Automatic merge failed; recursive merging with submodules currently
 only supports fast-forward merges. For each conflicted submodule, if
 the current commit does not reflect the desired state, either update or
 merge the commit. This can be accomplished with the following steps:

 - go to submodule (<submodule>), and either update the commit or merge commit <commit>

> >  - come back to superproject, and `git add <submodule>` to record the above merge
> 
> "...to record the above merge or update"?

will... "update" haha

> > If git detects multiple possible merge resolutions, the following is printed:
> >
> > --------
> >
> > Failed to merge submodule sub, but multiple possible merges exist:
> >     <commit> Merge branch '<branch1>' into <branch2>
> >     <commit> Merge branch '<branch1>' into <branch3>
> >
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Recursive merging with submodules is currently not supported.
> > To manually complete the merge:
> >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> 
> Same issues as I mentioned above for the single-merge-resolution-found case.
> 

ditto

> >  - come back to superproject, and `git add <submodule>` to record the above merge
> 
> "merge or update", right?

ditto

> > +ret:
> > +       if (!ret) {
> > +               if (!csub) {
> > +                       CALLOC_ARRAY(csub, 1);
> > +               }
> > +               csub_item.oid = *result;
> 
> Shouldn't this be "*b" rather than "*result"?

Yes it should

> 
> Also, are we risking telling the user to "merge commit
> 0000000000000000000000000000000000000000" from the submodule, given
> some of the early exits that you redirected to come through here?

You are correct, but I'm not quite sure what should happen in this case. What does it mean for either o, a, or b to be null? Did a submodule get deleted on one side? 

> > +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
> 
> Maybe just make this function be static?

It should be static now that this won't be called in merge-recursive

> > +                                       find_unique_abbrev(&id, DEFAULT_ABBREV));
> 
> Shouldn't this be repo_find_unique_abbrev(), and in particular with
> the submodule repository being passed to it?  Or perhaps using the
> format_commit() function, since merge_submodule() is already using it
> for the earlier submodule-related messages?

It should and I will go with the format_commit() option so I don't have to pass subrepo information into the print function.

> >  void merge_switch_to_result(struct merge_options *opt,
> >                             struct tree *head,
> >                             struct merge_result *result,
> > @@ -4324,6 +4367,8 @@ void merge_switch_to_result(struct merge_options *opt,
> >                 }
> >                 string_list_clear(&olist, 0);
> >
> > +               print_submodule_conflict_suggestion(opt->conflicted_submodules);
> > +
> >                 /* Also include needed rename limit adjustment now */
> >                 diff_warn_rename_limit("merge.renamelimit",
> >                                        opti->renames.needed_limit, 0);
> > diff --git a/merge-recursive.c b/merge-recursive.c
> > index fd1bbde061..311cc37886 100644
> > --- a/merge-recursive.c
> > +++ b/merge-recursive.c
> 
> Is it worth updating merge-recursive.c here?  I'd rather only give it
> security fix updates until we delete it.

Ah wasn't aware that was the status of merge-recursive. Will delete.

> > diff --git a/merge-recursive.h b/merge-recursive.h
> > index b88000e3c2..b615955fb7 100644
> > --- a/merge-recursive.h
> > +++ b/merge-recursive.h
> > @@ -9,6 +9,7 @@ struct object_id;
> >  struct repository;
> >  struct tree;
> >
> > +struct conflicted_submodule_list;
> >  struct merge_options_internal;
> >  struct merge_options {
> >         struct repository *repo;
> > @@ -51,6 +52,21 @@ struct merge_options {
> >
> >         /* internal fields used by the implementation */
> >         struct merge_options_internal *priv;
> > +
> > +       /* field that holds submodule conflict information */
> > +       struct conflicted_submodule_list *conflicted_submodules;
> 
> I think this should be added to merge_options_internal rather than to
> merge_options.  There's no need for an API caller to make use of
> these.
> 
> Also, if a clear need arises later for API callers to make use of this
> information, then it should be part of merge_result for merge-ort.c,
> not part of merge_options.

ack

> > +};
> > +
> > +struct conflicted_submodule_item {
> > +       struct object_id oid;
> > +       char *path;
> > +       int resolution_exists;
> > +};
> > +
> > +struct conflicted_submodule_list {
> > +       struct conflicted_submodule_item *items;
> > +       size_t nr;
> > +       size_t alloc;
> >  };
> 
> Similarly, I think these should be defined in merge-ort.c (and maybe
> also merge-recursive.c) near struct merge_options_internal.

ack

> >  void init_merge_options(struct merge_options *opt, struct repository *repo);
> > @@ -122,4 +138,6 @@ int merge_recursive_generic(struct merge_options *opt,
> >                             const struct object_id **merge_bases,
> >                             struct commit **result);
> >
> > +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub);
> 
> and I think there's no reason to put this in the header file; it
> should just be a static function in merge-ort.c.  (And, if we really
> want to update merge-recursive.c, then copy the function over there.
> *Nothing* in merge-ort.c should call a function in merge-recursive.c
> and similarly nothing in merge-recursive.c should call any function in
> merge-ort.c.  Yes, that implies some duplication.  There is a fair
> amount already and we've discussed it, and chosen against creating a
> shared module as well.  I want merge-recursive to not be broken by
> merge-ort updates; it should remain stable until the day we finally
> get to nuke it.  I'm personally of the opinion that merge-recursive
> should only get security fixes at this point, though I haven't pinged
> to see if other folks agree with that point of view yet or not.  I'm
> not wasting my time fixing/improving it, though...)

Good to know going forward I should only update merge-ort (unless for security fixes). I'll keep this in mind refactoring my patch.
 
> Despite my many nitpicks and whatnot, it looks like your change will
> make things nicer for the user, and I think your patch is coming along
> nicely.  There are some things to fix up, but the overall direction
> seems good.

Thank you for all of the helpful feedback!

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
  2022-06-30  2:40     ` Elijah Newren
@ 2022-06-30 20:35     ` Glen Choo
  2022-06-30 20:45       ` Glen Choo
  2022-06-30 21:08       ` Calvin Wan
  2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
  2 siblings, 2 replies; 60+ messages in thread
From: Glen Choo @ 2022-06-30 20:35 UTC (permalink / raw)
  To: Calvin Wan, git; +Cc: gitster, newren, levraiphilippeblain, Calvin Wan

Hi! I have a suggestion for the output text; I haven't looked closely at
the code changes.

Calvin Wan <calvinwan@google.com> writes:

>  Changes since v2:
>  [...]
>  Changes since v1:
>  [...]

I notice that this is all above the "---", i.e. this becomes part of the
commit message when "git am"-ed. Intentional?

> If git detects a possible merge resolution, the following is printed:
>
> --------
>
> Failed to merge submodule sub, but a possible merge resolution exists:
>     <commit> Merge branch '<branch1>' into <branch2>
>
>
> If this is correct simply add it to the index for example
> by using:
>
>   git update-index --cacheinfo 160000 <commit> "<submodule>"
>
> which will accept this suggestion.
>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules is currently not supported.
> To manually complete the merge:
>  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
>  - come back to superproject, and `git add <submodule>` to record the above merge 
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> --------

I'm hesitant to recommend a plumbing command like "git update-index" to
the user, especially if the user is one who needs help resolving a
submodule merge conflict. I also believe this would be the first time we
recommend "git update-index".

To do this using only porcelain commands, maybe:

   git -C <submodule> checkout <commit> &&
   git add <submodule>

(Though this might need to be broken up into two commands because I'm
not sure if we ever include "&&" in a help message. I'm guessing we
don't for portability reasons?)

> If git detects multiple possible merge resolutions, the following is printed:
>
> --------
>
> Failed to merge submodule sub, but multiple possible merges exist:
>     <commit> Merge branch '<branch1>' into <branch2>
>     <commit> Merge branch '<branch1>' into <branch3>
>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules is currently not supported.
> To manually complete the merge:
>  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
>  - come back to superproject, and `git add <submodule>` to record the above merge 
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> -------

For consistency, perhaps include the "here's how to use the suggestion"
instructions here as well?

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-30 20:35     ` Glen Choo
@ 2022-06-30 20:45       ` Glen Choo
  2022-06-30 21:08       ` Calvin Wan
  1 sibling, 0 replies; 60+ messages in thread
From: Glen Choo @ 2022-06-30 20:45 UTC (permalink / raw)
  To: Calvin Wan, git; +Cc: gitster, newren, levraiphilippeblain, Calvin Wan

Glen Choo <chooglen@google.com> writes:

> Hi! I have a suggestion for the output text; I haven't looked closely at
> the code changes.
>
>> If git detects a possible merge resolution, the following is printed:
>>
>> --------
>>
>> Failed to merge submodule sub, but a possible merge resolution exists:
>>     <commit> Merge branch '<branch1>' into <branch2>
>>
>>
>> If this is correct simply add it to the index for example
>> by using:
>>
>>   git update-index --cacheinfo 160000 <commit> "<submodule>"
>>
>> which will accept this suggestion.
>>
>> CONFLICT (submodule): Merge conflict in <submodule>
>> Recursive merging with submodules is currently not supported.
>> To manually complete the merge:
>>  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
>>  - come back to superproject, and `git add <submodule>` to record the above merge 
>>  - resolve any other conflicts in the superproject
>>  - commit the resulting index in the superproject
>> Automatic merge failed; fix conflicts and then commit the result.
>>
>> --------
>
> I'm hesitant to recommend a plumbing command like "git update-index" to
> the user, especially if the user is one who needs help resolving a
> submodule merge conflict. I also believe this would be the first time we
> recommend "git update-index".
>
> To do this using only porcelain commands, maybe:
>
>    git -C <submodule> checkout <commit> &&
>    git add <submodule>
>
> (Though this might need to be broken up into two commands because I'm
> not sure if we ever include "&&" in a help message. I'm guessing we
> don't for portability reasons?)

Gah, ignore everything I said here. I should have read the thread
closer:
- The update-index suggestion didn't come from you; it had already
  existed prior to this series.
- Both Philppe and Elijah have already suggested the exact same thing.


^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-30 20:35     ` Glen Choo
  2022-06-30 20:45       ` Glen Choo
@ 2022-06-30 21:08       ` Calvin Wan
  1 sibling, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-06-30 21:08 UTC (permalink / raw)
  To: Glen Choo; +Cc: git, gitster, newren, levraiphilippeblain

> I notice that this is all above the "---", i.e. this becomes part of the
> commit message when "git am"-ed. Intentional?

Good catch. Not intentional at all.

On Thu, Jun 30, 2022 at 1:35 PM Glen Choo <chooglen@google.com> wrote:
>
> Hi! I have a suggestion for the output text; I haven't looked closely at
> the code changes.
>
> Calvin Wan <calvinwan@google.com> writes:
>
> >  Changes since v2:
> >  [...]
> >  Changes since v1:
> >  [...]
>
> I notice that this is all above the "---", i.e. this becomes part of the
> commit message when "git am"-ed. Intentional?
>
> > If git detects a possible merge resolution, the following is printed:
> >
> > --------
> >
> > Failed to merge submodule sub, but a possible merge resolution exists:
> >     <commit> Merge branch '<branch1>' into <branch2>
> >
> >
> > If this is correct simply add it to the index for example
> > by using:
> >
> >   git update-index --cacheinfo 160000 <commit> "<submodule>"
> >
> > which will accept this suggestion.
> >
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Recursive merging with submodules is currently not supported.
> > To manually complete the merge:
> >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> >  - come back to superproject, and `git add <submodule>` to record the above merge
> >  - resolve any other conflicts in the superproject
> >  - commit the resulting index in the superproject
> > Automatic merge failed; fix conflicts and then commit the result.
> >
> > --------
>
> I'm hesitant to recommend a plumbing command like "git update-index" to
> the user, especially if the user is one who needs help resolving a
> submodule merge conflict. I also believe this would be the first time we
> recommend "git update-index".
>
> To do this using only porcelain commands, maybe:
>
>    git -C <submodule> checkout <commit> &&
>    git add <submodule>
>
> (Though this might need to be broken up into two commands because I'm
> not sure if we ever include "&&" in a help message. I'm guessing we
> don't for portability reasons?)
>
> > If git detects multiple possible merge resolutions, the following is printed:
> >
> > --------
> >
> > Failed to merge submodule sub, but multiple possible merges exist:
> >     <commit> Merge branch '<branch1>' into <branch2>
> >     <commit> Merge branch '<branch1>' into <branch3>
> >
> > CONFLICT (submodule): Merge conflict in <submodule>
> > Recursive merging with submodules is currently not supported.
> > To manually complete the merge:
> >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> >  - come back to superproject, and `git add <submodule>` to record the above merge
> >  - resolve any other conflicts in the superproject
> >  - commit the resulting index in the superproject
> > Automatic merge failed; fix conflicts and then commit the result.
> >
> > -------
>
> For consistency, perhaps include the "here's how to use the suggestion"
> instructions here as well?

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v3] submodule merge: update conflict error message
  2022-06-30 19:48       ` Calvin Wan
@ 2022-07-01  4:27         ` Elijah Newren
  0 siblings, 0 replies; 60+ messages in thread
From: Elijah Newren @ 2022-07-01  4:27 UTC (permalink / raw)
  To: Calvin Wan; +Cc: Git Mailing List, Glen Choo, Junio C Hamano, Philippe Blain

On Thu, Jun 30, 2022 at 12:48 PM Calvin Wan <calvinwan@google.com> wrote:
>
> > > Failed to merge submodule <submodule>
> > > CONFLICT (submodule): Merge conflict in <submodule>
> > > Automatic merge failed; recursive merging with submodules is currently
> > > not supported. To manually complete the merge:
> > >  - go to submodule (<submodule>), and merge commit <commit>
> >
> > I'm still a little unsure on this.  The merge_submodule() logic we
> > have may not have detected a merge commit that merges the two branches
> > together, but that doesn't automatically imply a new merge must be
> > created in the submodule to resolve this conflict.  There might be
> > various reasons that other existing commits in the submodule could be
> > used as the "correct" update.
> >
> > Perhaps that is uncommon enough to not be worth mentioning; I'm just a
> > little hesitant to treat the merge_submodule() logic as robust and
> > finding the only choices users might want to use.  If we do keep the
> > wording as-is, it might be nice to at least discuss in the commit
> > message why we chose that and ignored the or-update-submodule option
> > in this case.
>
> You make a good point about merge_submodule() possibly not finding all
> of the right choices -- I'll add back the or-update-submodule option
> back.
>
> > > If this is correct simply add it to the index for example
> > > by using:
> > >
> > >   git update-index --cacheinfo 160000 <commit> "<submodule>"
> > >
> > > which will accept this suggestion.
> >
> > The last few lines above will no longer be part of the output once
> > en/merge-tree is merged; see commit a4040cfa35 (merge-ort: remove
> > command-line-centric submodule message from merge-ort, 2022-06-18).
>
> ack
>
> > > CONFLICT (submodule): Merge conflict in <submodule>
> > > Recursive merging with submodules is currently not supported.
> > > To manually complete the merge:
> > >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> >
> > Again, I'm hesitant to treat the suggestions from merge_submodule() as
> > the only possibilities.  For example, perhaps the user will want to
> > pick a commit that contains one of the ones found by merge_submodule()
> > in its history -- perhaps because the newer commit they want to select
> > contains an important bugfix for an issue not discovered at the time
> > of the merge in the submodule.
> >
> > Also, I'm worried your sentence may be easy to misunderstand, due to
> > it being ambiguous whether "merge" is a verb or an adjective.  More
> > precisely, your sentence could be read as "update the submodule to a
> > possible commit above, or update the submodule to merge commit
> > <commit>" and then users say, "But <commit> isn't a merge commit; why
> > does this message claim it is?  Do I just update the submodule to that
> > commit anyway?"  Or perhaps users just update it to <commit> without
> > even checking to find out that it's not a merge commit, with the
> > deleterious consequences of missing all the important changes
> > currently found in the submodule.

I probably should have mentioned that listing "merge" first and
"update" second in your instructions at least avoids the ambiguity
because it makes it clear that "merge" is a verb that way:

    - go to submodule (<submodule>), and either merge commit <commit>
or update the submodule to a possible commit above

> I agree this sentence can be misinterpreted. I also think the main
> reason my current message is unclear is because there is not enough
> context for the user to understand why the merge failed. In a standard
> merge, the reason in "CONFLICT (<reason>)" provides context, whereas in
> this case, "CONFLICT (submodule)" only tells the user that the submodule
> is conflicted in some way. The user is unaware that we attempted to
> fast-forward the submodule, failed, and now require the user to either
> update the commit or merge the commit. How does this rewording sound?

Do they need to know what we attempted?  I'm not sure why that
matters, beyond maybe telling them that actual automatic merging of
submodules is currently only done by Git's merging machinery in very
trivial cases.  All that should really matter at this point is that
there was a submodule modified on both sides of history, and we need
them to handle the merge of the submodule.

>  Automatic merge failed; recursive merging with submodules currently
>  only supports fast-forward merges. For each conflicted submodule, if

I'd rather substitute "trivial cases" instead of "fast-forward
merges".  For example, we handle deletions on both sides, it's just
that that's done before it ever gets to merge_submodule().  And if we
add more types of special cases beyond fast-forwards at some point,
it'd be nice to not need to update this text.

>  the current commit does not reflect the desired state, either update or
>  merge the commit. This can be accomplished with the following steps:

Maybe replace the last two sentences with "Please manually handle the
merging of each conflicted submodule; this can be accomplished with
the following steps:"

>  - go to submodule (<submodule>), and either update the commit or merge commit <commit>

What would you think of switching this to

   - go to submodule <submodule>, and either merge commit <commit> or
update to an existing commit which has merged those changes such as
one of the ones listed above

> > >  - come back to superproject, and `git add <submodule>` to record the above merge
> >
> > "...to record the above merge or update"?
>
> will... "update" haha

:-)

> > > If git detects multiple possible merge resolutions, the following is printed:
> > >
> > > --------
> > >
> > > Failed to merge submodule sub, but multiple possible merges exist:
> > >     <commit> Merge branch '<branch1>' into <branch2>
> > >     <commit> Merge branch '<branch1>' into <branch3>
> > >
> > > CONFLICT (submodule): Merge conflict in <submodule>
> > > Recursive merging with submodules is currently not supported.
> > > To manually complete the merge:
> > >  - go to submodule (<submodule>), and either update the submodule to a possible commit above or merge commit <commit>
> >
> > Same issues as I mentioned above for the single-merge-resolution-found case.
> >
>
> ditto
>
> > >  - come back to superproject, and `git add <submodule>` to record the above merge
> >
> > "merge or update", right?
>
> ditto
>
> > > +ret:
> > > +       if (!ret) {
> > > +               if (!csub) {
> > > +                       CALLOC_ARRAY(csub, 1);
> > > +               }
> > > +               csub_item.oid = *result;
> >
> > Shouldn't this be "*b" rather than "*result"?
>
> Yes it should
>
> >
> > Also, are we risking telling the user to "merge commit
> > 0000000000000000000000000000000000000000" from the submodule, given
> > some of the early exits that you redirected to come through here?
>
> You are correct, but I'm not quite sure what should happen in this case. What does it mean for either o, a, or b to be null? Did a submodule get deleted on one side?

o is the version in the merge-base, a is the version from the first
parent (thus the submodule version stored within HEAD), and b is the
version from the second parent (thus the submodule version stored
within the main module's MERGE_HEAD commit).

We can't have all three be null, because that would just mean there
was never a submodule at this path.

We can't have two of the three be null, because that would either be
deleted on both sides, or added on one side, and those cases are
trivially resolvable elsewhere in the merge machinery and there's no
need to call merge_submodule().

If o is null, the submodule didn't exist in the merge base.  So it
must be added on both sides (and the two sides have to have different
submodule commits, or the merge of the submodule would be trivially
handle-able elsewhere).

If a is null, it didn't exist in HEAD, meaning it was deleted on our
side of history.  (And b has to be different than o, or else this
would be trivially resolvable as a deletion.)

If b is null, then it's similar to the above case, but the submodule
was deleted on the other side of history that we are trying to merge
into HEAD rather than on HEAD's side of history.

However, now that I've said this, I took another look through the
code.  I think this actually isn't relevant right now.
merge_submodule() is only called from handle_content_merge(), which in
turn is only called from two places: (1) process_renames(), and (2)
the filemask >= 6 section of process_entry().  The process_renames()
cases, since we can't detect renames involving submodules, can't
involve a case with a null oid for a submodule.  And the filemask >= 6
implies that only o could have a null hash.  That means the checks for
a and b being null oids within merge_submodule will never trigger.
And we don't actually run the risk of telling users to merge the all
null commit.

Any time we have a modify/delete issue with submodules, we'll just end
up at the codepath that reports "CONFLICT (modify/delete): ...", and
which doesn't mention anything about the path being a submodule, but
it really doesn't need to; the text is accurate regardless.


> > > +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
> >
> > Maybe just make this function be static?
>
> It should be static now that this won't be called in merge-recursive
>
> > > +                                       find_unique_abbrev(&id, DEFAULT_ABBREV));
> >
> > Shouldn't this be repo_find_unique_abbrev(), and in particular with
> > the submodule repository being passed to it?  Or perhaps using the
> > format_commit() function, since merge_submodule() is already using it
> > for the earlier submodule-related messages?
>
> It should and I will go with the format_commit() option so I don't have to pass subrepo information into the print function.
>
> > >  void merge_switch_to_result(struct merge_options *opt,
> > >                             struct tree *head,
> > >                             struct merge_result *result,
> > > @@ -4324,6 +4367,8 @@ void merge_switch_to_result(struct merge_options *opt,
> > >                 }
> > >                 string_list_clear(&olist, 0);
> > >
> > > +               print_submodule_conflict_suggestion(opt->conflicted_submodules);
> > > +
> > >                 /* Also include needed rename limit adjustment now */
> > >                 diff_warn_rename_limit("merge.renamelimit",
> > >                                        opti->renames.needed_limit, 0);
> > > diff --git a/merge-recursive.c b/merge-recursive.c
> > > index fd1bbde061..311cc37886 100644
> > > --- a/merge-recursive.c
> > > +++ b/merge-recursive.c
> >
> > Is it worth updating merge-recursive.c here?  I'd rather only give it
> > security fix updates until we delete it.
>
> Ah wasn't aware that was the status of merge-recursive. Will delete.
>
> > > diff --git a/merge-recursive.h b/merge-recursive.h
> > > index b88000e3c2..b615955fb7 100644
> > > --- a/merge-recursive.h
> > > +++ b/merge-recursive.h
> > > @@ -9,6 +9,7 @@ struct object_id;
> > >  struct repository;
> > >  struct tree;
> > >
> > > +struct conflicted_submodule_list;
> > >  struct merge_options_internal;
> > >  struct merge_options {
> > >         struct repository *repo;
> > > @@ -51,6 +52,21 @@ struct merge_options {
> > >
> > >         /* internal fields used by the implementation */
> > >         struct merge_options_internal *priv;
> > > +
> > > +       /* field that holds submodule conflict information */
> > > +       struct conflicted_submodule_list *conflicted_submodules;
> >
> > I think this should be added to merge_options_internal rather than to
> > merge_options.  There's no need for an API caller to make use of
> > these.
> >
> > Also, if a clear need arises later for API callers to make use of this
> > information, then it should be part of merge_result for merge-ort.c,
> > not part of merge_options.
>
> ack
>
> > > +};
> > > +
> > > +struct conflicted_submodule_item {
> > > +       struct object_id oid;
> > > +       char *path;
> > > +       int resolution_exists;
> > > +};
> > > +
> > > +struct conflicted_submodule_list {
> > > +       struct conflicted_submodule_item *items;
> > > +       size_t nr;
> > > +       size_t alloc;
> > >  };
> >
> > Similarly, I think these should be defined in merge-ort.c (and maybe
> > also merge-recursive.c) near struct merge_options_internal.
>
> ack
>
> > >  void init_merge_options(struct merge_options *opt, struct repository *repo);
> > > @@ -122,4 +138,6 @@ int merge_recursive_generic(struct merge_options *opt,
> > >                             const struct object_id **merge_bases,
> > >                             struct commit **result);
> > >
> > > +void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub);
> >
> > and I think there's no reason to put this in the header file; it
> > should just be a static function in merge-ort.c.  (And, if we really
> > want to update merge-recursive.c, then copy the function over there.
> > *Nothing* in merge-ort.c should call a function in merge-recursive.c
> > and similarly nothing in merge-recursive.c should call any function in
> > merge-ort.c.  Yes, that implies some duplication.  There is a fair
> > amount already and we've discussed it, and chosen against creating a
> > shared module as well.  I want merge-recursive to not be broken by
> > merge-ort updates; it should remain stable until the day we finally
> > get to nuke it.  I'm personally of the opinion that merge-recursive
> > should only get security fixes at this point, though I haven't pinged
> > to see if other folks agree with that point of view yet or not.  I'm
> > not wasting my time fixing/improving it, though...)
>
> Good to know going forward I should only update merge-ort (unless for security fixes). I'll keep this in mind refactoring my patch.
>
> > Despite my many nitpicks and whatnot, it looks like your change will
> > make things nicer for the user, and I think your patch is coming along
> > nicely.  There are some things to fix up, but the overall direction
> > seems good.
>
> Thank you for all of the helpful feedback!

^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v4] submodule merge: update conflict error message
  2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
  2022-06-30  2:40     ` Elijah Newren
  2022-06-30 20:35     ` Glen Choo
@ 2022-07-12 23:19     ` Calvin Wan
  2022-07-13 18:11       ` Junio C Hamano
                         ` (2 more replies)
  2 siblings, 3 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-12 23:19 UTC (permalink / raw)
  To: git; +Cc: Calvin Wan, chooglen, gitster, newren, levraiphilippeblain

Apologies to any reviewers for the late v4 -- was OOO

Changes since v3:
Thank you again Elijah for the helpful feedback! I have removed any code
touching merge-recursive.c, and refactored the rest into merge-ort.c.
The error message has been updated as well as any relevant test cases. I
had added a jump in v3 to "ret:" in merge_submodule() to accomodate
early returns, but this has been proven to not be necessary since an
early return means the submodule was either renamed or deleted, and this
case is already taken care of with the message "CONFLICT (modify/delete):"

== Description ==

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded or trivially resolved, the merge
fails and git prints the following error:

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; fix conflicts and then commit the result.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers. 

Update error message to match the steps above. If git does not detect a 
merge resolution, the following is printed:

====

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

====

If git detects a possible merge resolution, the following is printed:

====

Failed to merge submodule sub, but a possible merge resolution exists:
    <commit> Merge branch '<branch1>' into <branch2>

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
To manually complete the merge:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
such as one listed above
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

====

If git detects multiple possible merge resolutions, the following is printed:

====

Failed to merge submodule sub, but multiple possible merges exist:
    <commit> Merge branch '<branch1>' into <branch2>
    <commit> Merge branch '<branch1>' into <branch3>

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
To manually complete the merge:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
such as one listed above
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

== Previous Changes ==

Changes since v2:
There are three major changes in this patch thanks to all the helpful
feedback! I have moved the print function from builtin/merge.c to
merge-ort.c and added it to merge_finalize() in merge-recursive.c and
merge_switch_to_result() in merge-ort.c. This allows other merge
machinery commands besides merge to print submodule conflict advice.

I have moved the check for submodule conflicts from process_entry() to 
merge_submodule(). This also required removing the early returns and
instead going to my submodule conflict check allowing us to pass back
information on whether git detected a possible merge resolution or not.

I have also updated the error message to better reflect the merge
status. Specifically, if git detects a possible merge resolution, the
error message now also suggest updating to one of those resolutions.

Other small changes: 
 - Moved fields that hold submodule conflict information to new object
 - Shortened printed commit id to DEFAULT_ABBREV
 - Added a test to t6437-submodule-merge.sh for merges with no
   resolution existence
 - Modified a test in t7402-submodule-rebase to show error message
   prints in other parts of the merge machinery

Changes since v1:
 - Removed advice to abort merge
 - Error message updated to contain more commit/submodule information

Signed-off-by: Calvin Wan <calvinwan@google.com>
---
 merge-ort.c                 | 56 +++++++++++++++++++++++++++++++++++++
 t/t6437-submodule-merge.sh  | 31 +++++++++++++++++---
 t/t7402-submodule-rebase.sh |  6 ++--
 3 files changed, 87 insertions(+), 6 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 0d3f42592f..872d924f58 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -292,6 +292,18 @@ struct rename_info {
 	int needed_limit;
 };
 
+struct conflicted_submodule_item {
+	char *oid;
+	char *path;
+	int resolution_exists;
+};
+
+struct conflicted_submodule_list {
+	struct conflicted_submodule_item *items;
+	size_t nr;
+	size_t alloc;
+};
+
 struct merge_options_internal {
 	/*
 	 * paths: primary data structure in all of merge ort.
@@ -385,6 +397,9 @@ struct merge_options_internal {
 
 	/* call_depth: recursion level counter for merging merge bases */
 	int call_depth;
+
+	/* field that holds submodule conflict information */
+	struct conflicted_submodule_list conflicted_submodules;
 };
 
 struct version_info {
@@ -1610,6 +1625,9 @@ static int merge_submodule(struct merge_options *opt,
 	struct commit *commit_o, *commit_a, *commit_b;
 	int parent_count;
 	struct object_array merges;
+	struct conflicted_submodule_list *csub = &opt->priv->conflicted_submodules;
+	struct conflicted_submodule_item csub_item;
+	int resolution_exists = 0;
 
 	int i;
 	int search = !opt->priv->call_depth;
@@ -1703,6 +1721,7 @@ static int merge_submodule(struct merge_options *opt,
 			   "which will accept this suggestion.\n"),
 			 oid_to_hex(&merges.objects[0].item->oid), path);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 		break;
 	default:
 		for (i = 0; i < merges.nr; i++)
@@ -1712,10 +1731,22 @@ static int merge_submodule(struct merge_options *opt,
 			 _("Failed to merge submodule %s, but multiple "
 			   "possible merges exist:\n%s"), path, sb.buf);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 	}
 
 	object_array_clear(&merges);
 cleanup:
+	if (!ret) {
+		if (!csub) {
+			CALLOC_ARRAY(csub, 1);
+		}
+		csub_item.oid = xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
+		csub_item.path = xstrdup(path);
+		csub_item.resolution_exists = resolution_exists;
+		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
+		csub->items[csub->nr++] = csub_item;
+		opt->priv->conflicted_submodules = *csub;
+	}
 	repo_clear(&subrepo);
 	return ret;
 }
@@ -4256,6 +4287,29 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+static void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
+	if (csub && csub->nr > 0) {
+		int i;
+		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
+			"Please manually handle the merging of each conflicted submodule.\n"
+			"This can be accomplished with the following steps:\n"));
+		for (i = 0; i < csub->nr; i++) {
+			printf(_(" - go to submodule (%s), and either merge commit %s\n"
+				    "or update to an existing commit which has merged those changes\n"),
+					csub->items[i].path,
+					csub->items[i].oid);
+			if (csub->items[i].resolution_exists)
+				printf(_("such as one listed above\n"));
+		}
+		printf(_(" - come back to superproject, and `git add"));
+		for (i = 0; i < csub->nr; i++)
+			printf(_(" %s"), csub->items[i].path);
+		printf(_("` to record the above merge or update \n"
+			" - resolve any other conflicts in the superproject\n"
+			" - commit the resulting index in the superproject\n"));
+	}
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4324,6 +4378,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		}
 		string_list_clear(&olist, 0);
 
+		print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+
 		/* Also include needed rename limit adjustment now */
 		diff_warn_rename_limit("merge.renamelimit",
 				       opti->renames.needed_limit, 0);
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index 178413c22f..cc1a312661 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
+
+test_expect_success 'merging should conflict for non fast-forward' '
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi &&
+	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 grep "$sub_expect" actual &&
+	 git reset --hard)
+'
 
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,9 +146,9 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
@@ -141,6 +158,8 @@ test_expect_success 'merging should conflict for non fast-forward' '
 		test_must_fail git merge c 2> actual
 	  fi &&
 	 grep $(cat expect) actual > /dev/null &&
+	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 grep "$sub_expect" actual &&
 	 git reset --hard)
 '
 
@@ -167,6 +186,8 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 fi &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
+	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	grep "$sub_expect" actual &&
 	git reset --hard)
 '
 
@@ -205,7 +226,9 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+	grep "$sub_expect" actual)
 '
 
 
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..f1bb29681f 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,9 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
+	grep "$sub_expect" actual_output
 '
 
 test_done

base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
-- 
2.37.0.144.g8ac04bfd2-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
@ 2022-07-13 18:11       ` Junio C Hamano
  2022-07-17  2:46         ` Elijah Newren
  2022-07-15 12:57       ` Johannes Schindelin
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
  2 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-07-13 18:11 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, chooglen, newren, levraiphilippeblain

Calvin Wan <calvinwan@google.com> writes:

> Changes since v3:
> Thank you again Elijah for the helpful feedback! I have removed any code
> touching merge-recursive.c, and refactored the rest into merge-ort.c.
> The error message has been updated as well as any relevant test cases. I
> had added a jump in v3 to "ret:" in merge_submodule() to accomodate
> early returns, but this has been proven to not be necessary since an
> early return means the submodule was either renamed or deleted, and this
> case is already taken care of with the message "CONFLICT (modify/delete):"

Unfortunately a34edae6 (merge-ort: split out a separate
display_update_messages() function, 2022-06-18) moves the code this
patch touches to a separate function.

If this is now an ort-specific topic by dropping merge-recursive
support, perhaps we'd need to coordinate with the other branch.

I think en/merge-tree is ready to be merged down to 'master' in a
few days, so I'll wait for [v5] of this patch after that happens.

Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
  2022-07-13 18:11       ` Junio C Hamano
@ 2022-07-15 12:57       ` Johannes Schindelin
  2022-07-16  6:22         ` Junio C Hamano
                           ` (2 more replies)
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
  2 siblings, 3 replies; 60+ messages in thread
From: Johannes Schindelin @ 2022-07-15 12:57 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, chooglen, gitster, newren, levraiphilippeblain

Hi Calvin,

On Tue, 12 Jul 2022, Calvin Wan wrote:

> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index 178413c22f..cc1a312661 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
>  	 echo "file-c" > file-c &&
>  	 git add file-c &&
>  	 git commit -m "sub-c") &&
> -	git commit -a -m "c" &&
> +	git commit -a -m "c")
> +'
> +
> +test_expect_success 'merging should conflict for non fast-forward' '
> +	(cd merge-search &&
> +	 git checkout -b test-nonforward-a b &&
> +	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +	  then
> +		test_must_fail git merge c >actual
> +	  else
> +		test_must_fail git merge c 2> actual
> +	  fi &&
> +	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +	 grep "$sub_expect" actual &&

No matter how hard I tried to stare at the code, no matter whether I
looked at `cw/submodule-merge-messages` or `seen`, I cannot see how this
`grep` could ever succeed when `GIT_TEST_MERGE_ALGORITHM=recursive`: only
the `ort` code has been taught this new trick.

In fact, when I run this locally with
`GIT_TEST_MERGE_ALGORITHM=recursive`, it not only fails (as shown here:
https://github.com/gitgitgadget/git/runs/7349612861?check_suite_focus=true#step:4:1833
and here:
https://github.com/gitgitgadget/git/runs/7326925485?check_suite_focus=true#step:4:1820),
it also leaves no error message in `actual`.

So you probably want to move the `sub_expect` assignment and the `grep`
into the `ort` clause of the conditional above.

With this fixup, things seem to work over here:

-- snip --
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index cc1a3126619..3892d0bf742 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -111,12 +111,12 @@ test_expect_success 'merging should conflict for non fast-forward' '
 	 git checkout -b test-nonforward-a b &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
-	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	 grep "$sub_expect" actual &&
 	 git reset --hard)
 '

@@ -153,13 +153,13 @@ test_expect_success 'merging should conflict for non fast-forward (resolution ex
 	  git rev-parse sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
 	 grep $(cat expect) actual > /dev/null &&
-	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	 grep "$sub_expect" actual &&
 	 git reset --hard)
 '

@@ -180,14 +180,14 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 ) &&
 	 if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	 then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	 else
 		test_must_fail git merge c 2> actual
 	 fi &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
-	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	grep "$sub_expect" actual &&
 	git reset --hard)
 '

@@ -227,8 +227,11 @@ test_expect_success 'merging should fail for changes that are backwards' '

 	git checkout -b test-backward e &&
 	test_must_fail git merge f >actual &&
-	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
-	grep "$sub_expect" actual)
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+		grep "$sub_expect" actual
+	fi)
 '

-- snap --

> +	 git reset --hard)

It would be better to move this before the subshell like this:

	test_when_finished "git -C merge-search reset --hard" &&

Ciao,
Dscho

> +'
>
> +test_expect_success 'finish setup for merge-search' '
> +	(cd merge-search &&
>  	git checkout -b d a &&
>  	(cd sub &&
>  	 git checkout -b sub-d sub-b &&
> @@ -129,9 +146,9 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
>  	 test_cmp expect actual)
>  '
>
> -test_expect_success 'merging should conflict for non fast-forward' '
> +test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
>  	(cd merge-search &&
> -	 git checkout -b test-nonforward b &&
> +	 git checkout -b test-nonforward-b b &&
>  	 (cd sub &&
>  	  git rev-parse sub-d > ../expect) &&
>  	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> @@ -141,6 +158,8 @@ test_expect_success 'merging should conflict for non fast-forward' '
>  		test_must_fail git merge c 2> actual
>  	  fi &&
>  	 grep $(cat expect) actual > /dev/null &&
> +	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +	 grep "$sub_expect" actual &&
>  	 git reset --hard)
>  '
>
> @@ -167,6 +186,8 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>  	 fi &&
>  	grep $(cat expect1) actual > /dev/null &&
>  	grep $(cat expect2) actual > /dev/null &&
> +	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +	grep "$sub_expect" actual &&
>  	git reset --hard)
>  '
>
> @@ -205,7 +226,9 @@ test_expect_success 'merging should fail for changes that are backwards' '
>  	git commit -a -m "f" &&
>
>  	git checkout -b test-backward e &&
> -	test_must_fail git merge f)
> +	test_must_fail git merge f >actual &&
> +	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> +	grep "$sub_expect" actual)
>  '
>
>
> diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
> index 8e32f19007..f1bb29681f 100755
> --- a/t/t7402-submodule-rebase.sh
> +++ b/t/t7402-submodule-rebase.sh
> @@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
>  	test_tick &&
>  	git commit -m fourth &&
>
> -	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
> +	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
>  	git ls-files -s submodule >actual &&
>  	(
>  		cd submodule &&
> @@ -112,7 +112,9 @@ test_expect_success 'rebasing submodule that should conflict' '
>  		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
>  		echo "160000 $(git rev-parse HEAD) 3	submodule"
>  	) >expect &&
> -	test_cmp expect actual
> +	test_cmp expect actual &&
> +	sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
> +	grep "$sub_expect" actual_output
>  '
>
>  test_done
>
> base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
> --
> 2.37.0.144.g8ac04bfd2-goog
>
>
>

^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-15 12:57       ` Johannes Schindelin
@ 2022-07-16  6:22         ` Junio C Hamano
  2022-07-17  2:44         ` Elijah Newren
  2022-07-18 17:03         ` Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-07-16  6:22 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Calvin Wan, git, chooglen, newren, levraiphilippeblain

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

>> +test_expect_success 'merging should conflict for non fast-forward' '
>> +	(cd merge-search &&
>> +	 git checkout -b test-nonforward-a b &&
>> +	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>> +	  then
>> +		test_must_fail git merge c >actual
>> +	  else
>> +		test_must_fail git merge c 2> actual
>> +	  fi &&
>> +	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
>> +	 grep "$sub_expect" actual &&
>
> No matter how hard I tried to stare at the code, no matter whether I
> looked at `cw/submodule-merge-messages` or `seen`, I cannot see how this
> `grep` could ever succeed when `GIT_TEST_MERGE_ALGORITHM=recursive`: only
> the `ort` code has been taught this new trick.

Indeed.  https://github.com/git/git/runs/7366982085 is how it failed
in 'seen'.

Thanks for helping Calvin.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-15 12:57       ` Johannes Schindelin
  2022-07-16  6:22         ` Junio C Hamano
@ 2022-07-17  2:44         ` Elijah Newren
  2022-07-18 17:03         ` Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Elijah Newren @ 2022-07-17  2:44 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: Calvin Wan, Git Mailing List, Glen Choo, Junio C Hamano,
	Philippe Blain

On Fri, Jul 15, 2022 at 5:57 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Calvin,
>
> On Tue, 12 Jul 2022, Calvin Wan wrote:
>
> > diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> > index 178413c22f..cc1a312661 100755
> > --- a/t/t6437-submodule-merge.sh
> > +++ b/t/t6437-submodule-merge.sh
> > @@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
> >        echo "file-c" > file-c &&
> >        git add file-c &&
> >        git commit -m "sub-c") &&
> > -     git commit -a -m "c" &&
> > +     git commit -a -m "c")
> > +'
> > +
> > +test_expect_success 'merging should conflict for non fast-forward' '
> > +     (cd merge-search &&
> > +      git checkout -b test-nonforward-a b &&
> > +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> > +       then
> > +             test_must_fail git merge c >actual
> > +       else
> > +             test_must_fail git merge c 2> actual
> > +       fi &&
> > +      sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> > +      grep "$sub_expect" actual &&
>
> No matter how hard I tried to stare at the code, no matter whether I
> looked at `cw/submodule-merge-messages` or `seen`, I cannot see how this
> `grep` could ever succeed when `GIT_TEST_MERGE_ALGORITHM=recursive`: only
> the `ort` code has been taught this new trick.

Right, that's left over from an earlier version of the patch (which
did also modify merge-recursive).

> In fact, when I run this locally with
> `GIT_TEST_MERGE_ALGORITHM=recursive`, it not only fails (as shown here:
> https://github.com/gitgitgadget/git/runs/7349612861?check_suite_focus=true#step:4:1833
> and here:
> https://github.com/gitgitgadget/git/runs/7326925485?check_suite_focus=true#step:4:1820),
> it also leaves no error message in `actual`.
>
> So you probably want to move the `sub_expect` assignment and the `grep`
> into the `ort` clause of the conditional above.
>
> With this fixup, things seem to work over here:
>
> -- snip --
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index cc1a3126619..3892d0bf742 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -111,12 +111,12 @@ test_expect_success 'merging should conflict for non fast-forward' '
>          git checkout -b test-nonforward-a b &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
> -        sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -        grep "$sub_expect" actual &&
>          git reset --hard)
>  '
>
> @@ -153,13 +153,13 @@ test_expect_success 'merging should conflict for non fast-forward (resolution ex
>           git rev-parse sub-d > ../expect) &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
>          grep $(cat expect) actual > /dev/null &&
> -        sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -        grep "$sub_expect" actual &&
>          git reset --hard)
>  '
>
> @@ -180,14 +180,14 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          ) &&
>          if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>          then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>          else
>                 test_must_fail git merge c 2> actual
>          fi &&
>         grep $(cat expect1) actual > /dev/null &&
>         grep $(cat expect2) actual > /dev/null &&
> -       sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -       grep "$sub_expect" actual &&
>         git reset --hard)
>  '
>
> @@ -227,8 +227,11 @@ test_expect_success 'merging should fail for changes that are backwards' '
>
>         git checkout -b test-backward e &&
>         test_must_fail git merge f >actual &&
> -       sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> -       grep "$sub_expect" actual)
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +       then
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> +               grep "$sub_expect" actual
> +       fi)
>  '
>
> -- snap --

Yeah, this looks good, thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-13 18:11       ` Junio C Hamano
@ 2022-07-17  2:46         ` Elijah Newren
  0 siblings, 0 replies; 60+ messages in thread
From: Elijah Newren @ 2022-07-17  2:46 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Calvin Wan, Git Mailing List, Glen Choo, Philippe Blain

On Wed, Jul 13, 2022 at 11:11 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Calvin Wan <calvinwan@google.com> writes:
>
> > Changes since v3:
> > Thank you again Elijah for the helpful feedback! I have removed any code
> > touching merge-recursive.c, and refactored the rest into merge-ort.c.
> > The error message has been updated as well as any relevant test cases. I
> > had added a jump in v3 to "ret:" in merge_submodule() to accomodate
> > early returns, but this has been proven to not be necessary since an
> > early return means the submodule was either renamed or deleted, and this
> > case is already taken care of with the message "CONFLICT (modify/delete):"
>
> Unfortunately a34edae6 (merge-ort: split out a separate
> display_update_messages() function, 2022-06-18) moves the code this
> patch touches to a separate function.
>
> If this is now an ort-specific topic by dropping merge-recursive
> support, perhaps we'd need to coordinate with the other branch.
>
> I think en/merge-tree is ready to be merged down to 'master' in a
> few days, so I'll wait for [v5] of this patch after that happens.

Yeah, sorry about that Calvin; one of the risks of working in the same
area.  Should be pretty easy to resolve, though; let me know if you
have any questions when you go to re-roll.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v4] submodule merge: update conflict error message
  2022-07-15 12:57       ` Johannes Schindelin
  2022-07-16  6:22         ` Junio C Hamano
  2022-07-17  2:44         ` Elijah Newren
@ 2022-07-18 17:03         ` Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-18 17:03 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, chooglen, gitster, newren, levraiphilippeblain

> No matter how hard I tried to stare at the code, no matter whether I
> looked at `cw/submodule-merge-messages` or `seen`, I cannot see how this
> `grep` could ever succeed when `GIT_TEST_MERGE_ALGORITHM=recursive`: only
> the `ort` code has been taught this new trick.

I definitely overlooked this removing the merge-recursive section of
my code. Thanks
for catching this!

On Fri, Jul 15, 2022 at 5:57 AM Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>
> Hi Calvin,
>
> On Tue, 12 Jul 2022, Calvin Wan wrote:
>
> > diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> > index 178413c22f..cc1a312661 100755
> > --- a/t/t6437-submodule-merge.sh
> > +++ b/t/t6437-submodule-merge.sh
> > @@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
> >        echo "file-c" > file-c &&
> >        git add file-c &&
> >        git commit -m "sub-c") &&
> > -     git commit -a -m "c" &&
> > +     git commit -a -m "c")
> > +'
> > +
> > +test_expect_success 'merging should conflict for non fast-forward' '
> > +     (cd merge-search &&
> > +      git checkout -b test-nonforward-a b &&
> > +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> > +       then
> > +             test_must_fail git merge c >actual
> > +       else
> > +             test_must_fail git merge c 2> actual
> > +       fi &&
> > +      sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> > +      grep "$sub_expect" actual &&
>
> No matter how hard I tried to stare at the code, no matter whether I
> looked at `cw/submodule-merge-messages` or `seen`, I cannot see how this
> `grep` could ever succeed when `GIT_TEST_MERGE_ALGORITHM=recursive`: only
> the `ort` code has been taught this new trick.
>
> In fact, when I run this locally with
> `GIT_TEST_MERGE_ALGORITHM=recursive`, it not only fails (as shown here:
> https://github.com/gitgitgadget/git/runs/7349612861?check_suite_focus=true#step:4:1833
> and here:
> https://github.com/gitgitgadget/git/runs/7326925485?check_suite_focus=true#step:4:1820),
> it also leaves no error message in `actual`.
>
> So you probably want to move the `sub_expect` assignment and the `grep`
> into the `ort` clause of the conditional above.
>
> With this fixup, things seem to work over here:
>
> -- snip --
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index cc1a3126619..3892d0bf742 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -111,12 +111,12 @@ test_expect_success 'merging should conflict for non fast-forward' '
>          git checkout -b test-nonforward-a b &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
> -        sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -        grep "$sub_expect" actual &&
>          git reset --hard)
>  '
>
> @@ -153,13 +153,13 @@ test_expect_success 'merging should conflict for non fast-forward (resolution ex
>           git rev-parse sub-d > ../expect) &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
>          grep $(cat expect) actual > /dev/null &&
> -        sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -        grep "$sub_expect" actual &&
>          git reset --hard)
>  '
>
> @@ -180,14 +180,14 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          ) &&
>          if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>          then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>          else
>                 test_must_fail git merge c 2> actual
>          fi &&
>         grep $(cat expect1) actual > /dev/null &&
>         grep $(cat expect2) actual > /dev/null &&
> -       sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> -       grep "$sub_expect" actual &&
>         git reset --hard)
>  '
>
> @@ -227,8 +227,11 @@ test_expect_success 'merging should fail for changes that are backwards' '
>
>         git checkout -b test-backward e &&
>         test_must_fail git merge f >actual &&
> -       sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> -       grep "$sub_expect" actual)
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +       then
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> +               grep "$sub_expect" actual
> +       fi)
>  '
>
> -- snap --
>
> > +      git reset --hard)
>
> It would be better to move this before the subshell like this:
>
>         test_when_finished "git -C merge-search reset --hard" &&
>
> Ciao,
> Dscho
>
> > +'
> >
> > +test_expect_success 'finish setup for merge-search' '
> > +     (cd merge-search &&
> >       git checkout -b d a &&
> >       (cd sub &&
> >        git checkout -b sub-d sub-b &&
> > @@ -129,9 +146,9 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
> >        test_cmp expect actual)
> >  '
> >
> > -test_expect_success 'merging should conflict for non fast-forward' '
> > +test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
> >       (cd merge-search &&
> > -      git checkout -b test-nonforward b &&
> > +      git checkout -b test-nonforward-b b &&
> >        (cd sub &&
> >         git rev-parse sub-d > ../expect) &&
> >         if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> > @@ -141,6 +158,8 @@ test_expect_success 'merging should conflict for non fast-forward' '
> >               test_must_fail git merge c 2> actual
> >         fi &&
> >        grep $(cat expect) actual > /dev/null &&
> > +      sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> > +      grep "$sub_expect" actual &&
> >        git reset --hard)
> >  '
> >
> > @@ -167,6 +186,8 @@ test_expect_success 'merging should fail for ambiguous common parent' '
> >        fi &&
> >       grep $(cat expect1) actual > /dev/null &&
> >       grep $(cat expect2) actual > /dev/null &&
> > +     sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> > +     grep "$sub_expect" actual &&
> >       git reset --hard)
> >  '
> >
> > @@ -205,7 +226,9 @@ test_expect_success 'merging should fail for changes that are backwards' '
> >       git commit -a -m "f" &&
> >
> >       git checkout -b test-backward e &&
> > -     test_must_fail git merge f)
> > +     test_must_fail git merge f >actual &&
> > +     sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> > +     grep "$sub_expect" actual)
> >  '
> >
> >
> > diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
> > index 8e32f19007..f1bb29681f 100755
> > --- a/t/t7402-submodule-rebase.sh
> > +++ b/t/t7402-submodule-rebase.sh
> > @@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
> >       test_tick &&
> >       git commit -m fourth &&
> >
> > -     test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
> > +     test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
> >       git ls-files -s submodule >actual &&
> >       (
> >               cd submodule &&
> > @@ -112,7 +112,9 @@ test_expect_success 'rebasing submodule that should conflict' '
> >               echo "160000 $(git rev-parse HEAD^^) 2  submodule" &&
> >               echo "160000 $(git rev-parse HEAD) 3    submodule"
> >       ) >expect &&
> > -     test_cmp expect actual
> > +     test_cmp expect actual &&
> > +     sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
> > +     grep "$sub_expect" actual_output
> >  '
> >
> >  test_done
> >
> > base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
> > --
> > 2.37.0.144.g8ac04bfd2-goog
> >
> >
> >

^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v5] submodule merge: update conflict error message
  2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
  2022-07-13 18:11       ` Junio C Hamano
  2022-07-15 12:57       ` Johannes Schindelin
@ 2022-07-18 21:43       ` Calvin Wan
  2022-07-19  6:39         ` Junio C Hamano
                           ` (4 more replies)
  2 siblings, 5 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-18 21:43 UTC (permalink / raw)
  To: git
  Cc: Calvin Wan, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin

Changes since v4:
 - Rebased onto gitster/master
 - Fixed test cases to work with only merge-ort (and not merge-recursive)

== Description ==

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded or trivially resolved, the merge
fails and git prints the following error:

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Automatic merge failed; fix conflicts and then commit the result.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers. 

Update error message to match the steps above. If git does not detect a 
merge resolution, the following is printed:

====

Failed to merge submodule <submodule>
CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

====

If git detects a possible merge resolution, the following is printed:

====

Failed to merge submodule sub, but a possible merge resolution exists:
    <commit> Merge branch '<branch1>' into <branch2>

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
To manually complete the merge:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
such as one listed above
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

====

If git detects multiple possible merge resolutions, the following is printed:

====

Failed to merge submodule sub, but multiple possible merges exist:
    <commit> Merge branch '<branch1>' into <branch2>
    <commit> Merge branch '<branch1>' into <branch3>

CONFLICT (submodule): Merge conflict in <submodule>
Recursive merging with submodules currently only supports trivial cases.
Please manually handle the merging of each conflicted submodule.
This can be accomplished with the following steps:
To manually complete the merge:
 - go to submodule (<submodule>), and either merge commit <commit>
or update to an existing commit which has merged those changes
such as one listed above
 - come back to superproject, and `git add <submodule>` to record the above merge or update
 - resolve any other conflicts in the superproject
 - commit the resulting index in the superproject
Automatic merge failed; fix conflicts and then commit the result.

== Previous Changes ==

Changes since v3:
Thank you again Elijah for the helpful feedback! I have removed any code
touching merge-recursive.c, and refactored the rest into merge-ort.c.
The error message has been updated as well as any relevant test cases. I
had added a jump in v3 to "ret:" in merge_submodule() to accomodate
early returns, but this has been proven to not be necessary since an
early return means the submodule was either renamed or deleted, and this
case is already taken care of with the message "CONFLICT (modify/delete):"

Changes since v2:
There are three major changes in this patch thanks to all the helpful
feedback! I have moved the print function from builtin/merge.c to
merge-ort.c and added it to merge_finalize() in merge-recursive.c and
merge_switch_to_result() in merge-ort.c. This allows other merge
machinery commands besides merge to print submodule conflict advice.

I have moved the check for submodule conflicts from process_entry() to 
merge_submodule(). This also required removing the early returns and
instead going to my submodule conflict check allowing us to pass back
information on whether git detected a possible merge resolution or not.

I have also updated the error message to better reflect the merge
status. Specifically, if git detects a possible merge resolution, the
error message now also suggest updating to one of those resolutions.

Other small changes: 
 - Moved fields that hold submodule conflict information to new object
 - Shortened printed commit id to DEFAULT_ABBREV
 - Added a test to t6437-submodule-merge.sh for merges with no
   resolution existence
 - Modified a test in t7402-submodule-rebase to show error message
   prints in other parts of the merge machinery

Changes since v1:
 - Removed advice to abort merge
 - Error message updated to contain more commit/submodule information

Signed-off-by: Calvin Wan <calvinwan@google.com>
---
 merge-ort.c                 | 56 +++++++++++++++++++++++++++++++++++++
 t/t6437-submodule-merge.sh  | 38 +++++++++++++++++++++----
 t/t7402-submodule-rebase.sh |  9 ++++--
 3 files changed, 95 insertions(+), 8 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 01f150ef3b..125ee3c0d1 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -292,6 +292,18 @@ struct rename_info {
 	int needed_limit;
 };
 
+struct conflicted_submodule_item {
+	char *oid;
+	char *path;
+	int resolution_exists;
+};
+
+struct conflicted_submodule_list {
+	struct conflicted_submodule_item *items;
+	size_t nr;
+	size_t alloc;
+};
+
 struct merge_options_internal {
 	/*
 	 * paths: primary data structure in all of merge ort.
@@ -387,6 +399,9 @@ struct merge_options_internal {
 
 	/* call_depth: recursion level counter for merging merge bases */
 	int call_depth;
+
+	/* field that holds submodule conflict information */
+	struct conflicted_submodule_list conflicted_submodules;
 };
 
 struct version_info {
@@ -1741,6 +1756,9 @@ static int merge_submodule(struct merge_options *opt,
 	struct commit *commit_o, *commit_a, *commit_b;
 	int parent_count;
 	struct object_array merges;
+	struct conflicted_submodule_list *csub = &opt->priv->conflicted_submodules;
+	struct conflicted_submodule_item csub_item;
+	int resolution_exists = 0;
 
 	int i;
 	int search = !opt->priv->call_depth;
@@ -1835,6 +1853,7 @@ static int merge_submodule(struct merge_options *opt,
 			   "resolution exists: %s"),
 			 path, sb.buf);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 		break;
 	default:
 		for (i = 0; i < merges.nr; i++)
@@ -1845,10 +1864,22 @@ static int merge_submodule(struct merge_options *opt,
 			 _("Failed to merge submodule %s, but multiple "
 			   "possible merges exist:\n%s"), path, sb.buf);
 		strbuf_release(&sb);
+		resolution_exists = 1;
 	}
 
 	object_array_clear(&merges);
 cleanup:
+	if (!ret) {
+		if (!csub) {
+			CALLOC_ARRAY(csub, 1);
+		}
+		csub_item.oid = xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
+		csub_item.path = xstrdup(path);
+		csub_item.resolution_exists = resolution_exists;
+		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
+		csub->items[csub->nr++] = csub_item;
+		opt->priv->conflicted_submodules = *csub;
+	}
 	repo_clear(&subrepo);
 	return ret;
 }
@@ -4412,6 +4443,29 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+static void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
+	if (csub && csub->nr > 0) {
+		int i;
+		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
+			"Please manually handle the merging of each conflicted submodule.\n"
+			"This can be accomplished with the following steps:\n"));
+		for (i = 0; i < csub->nr; i++) {
+			printf(_(" - go to submodule (%s), and either merge commit %s\n"
+				    "or update to an existing commit which has merged those changes\n"),
+					csub->items[i].path,
+					csub->items[i].oid);
+			if (csub->items[i].resolution_exists)
+				printf(_("such as one listed above\n"));
+		}
+		printf(_(" - come back to superproject, and `git add"));
+		for (i = 0; i < csub->nr; i++)
+			printf(_(" %s"), csub->items[i].path);
+		printf(_("` to record the above merge or update \n"
+			" - resolve any other conflicts in the superproject\n"
+			" - commit the resulting index in the superproject\n"));
+	}
+}
+
 void merge_display_update_messages(struct merge_options *opt,
 				   int detailed,
 				   struct merge_result *result)
@@ -4461,6 +4515,8 @@ void merge_display_update_messages(struct merge_options *opt,
 	}
 	string_list_clear(&olist, 0);
 
+	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
 			       opti->renames.needed_limit, 0);
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index c253bf759a..84913525cf 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
 
+test_expect_success 'merging should conflict for non fast-forward' '
+	test_when_finished "git -C merge-search reset --hard" &&
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi)
+'
+
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse --short sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
@@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 ) &&
 	 if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	 then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	 else
 		test_must_fail git merge c 2> actual
 	 fi &&
@@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+		grep "$sub_expect" actual
+	fi)
 '
 
 
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..ebeca12a71 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
+		grep "$sub_expect" actual_output
+	fi
 '
 
 test_done

base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
-- 
2.37.0.170.g444d1eabd0-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
@ 2022-07-19  6:39         ` Junio C Hamano
  2022-07-19 19:30           ` Calvin Wan
  2022-07-19  7:13         ` Junio C Hamano
                           ` (3 subsequent siblings)
  4 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-07-19  6:39 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

Calvin Wan <calvinwan@google.com> writes:

> Changes since v4:
>  - Rebased onto gitster/master
>  - Fixed test cases to work with only merge-ort (and not merge-recursive)

That's not a log-message material.  You could have probably written
scissors ...

> == Description ==

--- >8 ---

... here, perhaps?

> When attempting to merge in a superproject with conflicting submodule
> pointers that cannot be fast-forwarded or trivially resolved, the merge
> fails and git prints the following error:
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Automatic merge failed; fix conflicts and then commit the result.
>
> Git is left in a conflicted state, which requires the user to:
>  1. merge submodules or update submodules to an already existing
> 	commit that reflects the merge
>  2. add submodules changes to the superproject
>  3. finish merging superproject
> These steps are non-obvious for newer submodule users to figure out
> based on the error message and neither `git submodule status` nor `git
> status` provide any useful pointers. 
>
> Update error message to match the steps above. If git does not detect a 
> merge resolution, the following is printed:
>
> ====
>
> Failed to merge submodule <submodule>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules currently only supports trivial cases.
> Please manually handle the merging of each conflicted submodule.
> This can be accomplished with the following steps:
>  - go to submodule (<submodule>), and either merge commit <commit>
> or update to an existing commit which has merged those changes
>  - come back to superproject, and `git add <submodule>` to record the above merge or update
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> ====
>
> If git detects a possible merge resolution, the following is printed:
>
> ====
>
> Failed to merge submodule sub, but a possible merge resolution exists:
>     <commit> Merge branch '<branch1>' into <branch2>
>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules currently only supports trivial cases.
> Please manually handle the merging of each conflicted submodule.
> This can be accomplished with the following steps:
> To manually complete the merge:
>  - go to submodule (<submodule>), and either merge commit <commit>
> or update to an existing commit which has merged those changes
> such as one listed above
>  - come back to superproject, and `git add <submodule>` to record the above merge or update
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.
>
> ====
>
> If git detects multiple possible merge resolutions, the following is printed:
>
> ====
>
> Failed to merge submodule sub, but multiple possible merges exist:
>     <commit> Merge branch '<branch1>' into <branch2>
>     <commit> Merge branch '<branch1>' into <branch3>
>
> CONFLICT (submodule): Merge conflict in <submodule>
> Recursive merging with submodules currently only supports trivial cases.
> Please manually handle the merging of each conflicted submodule.
> This can be accomplished with the following steps:
> To manually complete the merge:
>  - go to submodule (<submodule>), and either merge commit <commit>
> or update to an existing commit which has merged those changes
> such as one listed above
>  - come back to superproject, and `git add <submodule>` to record the above merge or update
>  - resolve any other conflicts in the superproject
>  - commit the resulting index in the superproject
> Automatic merge failed; fix conflicts and then commit the result.

But cutting the cruft at the top is still not enough, because the
below are not log-message material, either.

These extraneous stuff may help reviewers while the patch is being
polished, but once committed to our history, readers of "git log"
should not have to care what other attempts were made that did not
become part of the final hstory.  The log message proper should be
understandable standalone.

The stuff to help reviewers who may have seen earlier round are
usually written in the cover letter, or after the three-dash line.

> == Previous Changes ==
>
> Changes since v3:
> ...
>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
> ---
>  merge-ort.c                 | 56 +++++++++++++++++++++++++++++++++++++
>  t/t6437-submodule-merge.sh  | 38 +++++++++++++++++++++----
>  t/t7402-submodule-rebase.sh |  9 ++++--
>  3 files changed, 95 insertions(+), 8 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 01f150ef3b..125ee3c0d1 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> ...
> +	if (csub && csub->nr > 0) {
> +		int i;
> +		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
> +			"Please manually handle the merging of each conflicted submodule.\n"
> +			"This can be accomplished with the following steps:\n"));

This makes me wonder if these "helpful but verbose" messages should
use the advise mechanism.  Also, those reviewers who care about l10n
may suggest use of printf_ln() to lose the LF at the end of these
messages (i.e. not just the above one, but others we see below as
well).

> +		for (i = 0; i < csub->nr; i++) {
> +			printf(_(" - go to submodule (%s), and either merge commit %s\n"
> +				    "or update to an existing commit which has merged those changes\n"),
> +					csub->items[i].path,
> +					csub->items[i].oid);
> +			if (csub->items[i].resolution_exists)
> +				printf(_("such as one listed above\n"));
> +		}
> +		printf(_(" - come back to superproject, and `git add"));
> +		for (i = 0; i < csub->nr; i++)
> +			printf(_(" %s"), csub->items[i].path);
> +		printf(_("` to record the above merge or update \n"
> +			" - resolve any other conflicts in the superproject\n"
> +			" - commit the resulting index in the superproject\n"));
> +	}
> +}
> +
>  void merge_display_update_messages(struct merge_options *opt,
>  				   int detailed,
>  				   struct merge_result *result)
> @@ -4461,6 +4515,8 @@ void merge_display_update_messages(struct merge_options *opt,
>  	}
>  	string_list_clear(&olist, 0);
>  
> +	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
> +
>  	/* Also include needed rename limit adjustment now */
>  	diff_warn_rename_limit("merge.renamelimit",
>  			       opti->renames.needed_limit, 0);

Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
  2022-07-19  6:39         ` Junio C Hamano
@ 2022-07-19  7:13         ` Junio C Hamano
  2022-07-19 19:07           ` Calvin Wan
  2022-07-25  6:05         ` Ævar Arnfjörð Bjarmason
                           ` (2 subsequent siblings)
  4 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-07-19  7:13 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

Calvin Wan <calvinwan@google.com> writes:

> @@ -1835,6 +1853,7 @@ static int merge_submodule(struct merge_options *opt,
>  			   "resolution exists: %s"),
>  			 path, sb.buf);
>  		strbuf_release(&sb);
> +		resolution_exists = 1;
>  		break;
>  	default:
>  		for (i = 0; i < merges.nr; i++)
> @@ -1845,10 +1864,22 @@ static int merge_submodule(struct merge_options *opt,
>  			 _("Failed to merge submodule %s, but multiple "
>  			   "possible merges exist:\n%s"), path, sb.buf);
>  		strbuf_release(&sb);
> +		resolution_exists = 1;
>  	}

These work on the result of calling find_first_merges(), but is it
possible that we are asked to call this function more than once
because we see conflicted submodule updates at two or more paths?

I may be misreading the code, but find_first_merges(), either the
version we see in this file, or the one in merge-recursive.c, or its
original introduced in 68d03e4a (Implement automatic fast-forward
merge for submodules, 2010-07-07), look safe to be called twice.  It
runs the get_revision() machinery, smudging the object flags while
walking the history, but I do not see any code that cleans up these
flags for the second traversal.

Also, this is not a new problem, but I am afraid that the logic to
find existing merges in find_first_merges() might be overly loose.
It tries to find existing merges that can reach the two commits, and
then finds, among these merges, the one that is not descendant of
any other such merges.  Don't we end up finding a merge M

    A---o---M
           /
          B

when a superproject merge needs a merge of A and B in the submodule?
That is certainly a merge that contains both A and B and it may be
closer to A and B than any other existing merges, but it still may
not be a merge between A and B (in the depicted case, an extra
commit 'o' nobody ordered is included for free in the result).  I am
not seeing how existing code tries to avoid such a situation.

Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-19  7:13         ` Junio C Hamano
@ 2022-07-19 19:07           ` Calvin Wan
  2022-07-19 20:30             ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-07-19 19:07 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

> These work on the result of calling find_first_merges(), but is it
> possible that we are asked to call this function more than once
> because we see conflicted submodule updates at two or more paths?

This does get called multiple times if we see conflicted submodule
updates at two or more paths.

> I may be misreading the code, but find_first_merges(), either the
> version we see in this file, or the one in merge-recursive.c, or its
> original introduced in 68d03e4a (Implement automatic fast-forward
> merge for submodules, 2010-07-07), look safe to be called twice.  It
> runs the get_revision() machinery, smudging the object flags while
> walking the history, but I do not see any code that cleans up these
> flags for the second traversal.

I don't quite understand which flags need to be cleaned up for the
second traversal.

> Also, this is not a new problem, but I am afraid that the logic to
> find existing merges in find_first_merges() might be overly loose.
> It tries to find existing merges that can reach the two commits, and
> then finds, among these merges, the one that is not descendant of
> any other such merges.  Don't we end up finding a merge M
>
>     A---o---M
>            /
>           B
>
> when a superproject merge needs a merge of A and B in the submodule?
> That is certainly a merge that contains both A and B and it may be
> closer to A and B than any other existing merges, but it still may
> not be a merge between A and B (in the depicted case, an extra
> commit 'o' nobody ordered is included for free in the result).  I am
> not seeing how existing code tries to avoid such a situation.

It is true that we find merge M and it isn't representative of a merge of A
and B in the submodule. In this case, the existing code prints:

"Failed to merge submodule %s, but a possible merge resolution exists: %s"

While this part doesn't claim M to be a guaranteed merge resolution, my
change adds this line:

"or update to an existing commit which has merged those changes such as
one listed above"

Instead of adding more verbosity to this language, it seems like a better
idea to remove "such as one listed above" entirely (and subsequently any
of my code that flags merge resolutions).

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-19  6:39         ` Junio C Hamano
@ 2022-07-19 19:30           ` Calvin Wan
  2022-07-19 20:16             ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-07-19 19:30 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

> The stuff to help reviewers who may have seen earlier round are
> usually written in the cover letter, or after the three-dash line.
Ah I see since this patch doesn't have a cover letter, I should put
all the reviewer-centric stuff after the three-dash line.

> > +     if (csub && csub->nr > 0) {
> > +             int i;
> > +             printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
> > +                     "Please manually handle the merging of each conflicted submodule.\n"
> > +                     "This can be accomplished with the following steps:\n"));
>
> This makes me wonder if these "helpful but verbose" messages should
> use the advise mechanism.

I agree. The only loss of information if someone turned off this message
would be the commit id that possibly needs to be merged.

> Also, those reviewers who care about l10n
> may suggest use of printf_ln() to lose the LF at the end of these
> messages (i.e. not just the above one, but others we see below as
> well).

ack

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-19 19:30           ` Calvin Wan
@ 2022-07-19 20:16             ` Junio C Hamano
  0 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-07-19 20:16 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

Calvin Wan <calvinwan@google.com> writes:

>> The stuff to help reviewers who may have seen earlier round are
>> usually written in the cover letter, or after the three-dash line.
> Ah I see since this patch doesn't have a cover letter, I should put
> all the reviewer-centric stuff after the three-dash line.
>
>> > +     if (csub && csub->nr > 0) {
>> > +             int i;
>> > +             printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
>> > +                     "Please manually handle the merging of each conflicted submodule.\n"
>> > +                     "This can be accomplished with the following steps:\n"));
>>
>> This makes me wonder if these "helpful but verbose" messages should
>> use the advise mechanism.
>
> I agree. The only loss of information if someone turned off this message
> would be the commit id that possibly needs to be merged.

If the commit found by find_first_merges() are useful, then it is
losing information, so it is one argument against using the advise
mechanism.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-19 19:07           ` Calvin Wan
@ 2022-07-19 20:30             ` Junio C Hamano
  0 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-07-19 20:30 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, newren, levraiphilippeblain, Johannes.Schindelin

Calvin Wan <calvinwan@google.com> writes:

>> These work on the result of calling find_first_merges(), but is it
>> possible that we are asked to call this function more than once
>> because we see conflicted submodule updates at two or more paths?
>
> This does get called multiple times if we see conflicted submodule
> updates at two or more paths.
>
>> I may be misreading the code, but find_first_merges(), either the
>> version we see in this file, or the one in merge-recursive.c, or its
>> original introduced in 68d03e4a (Implement automatic fast-forward
>> merge for submodules, 2010-07-07), look safe to be called twice.  It
>> runs the get_revision() machinery, smudging the object flags while
>> walking the history, but I do not see any code that cleans up these
>> flags for the second traversal.
>
> I don't quite understand which flags need to be cleaned up for the
> second traversal.

UNINTERESTING, TREESAME, ADDED, SEEN, SHOWN are among the flags used
by the object walk (if MyFirstObjectWalk does not talk about them,
it probably should), and they need to be cleared before you prepare
a new "struct rev_info" and throw it at setup_revisions(),
prepare_revision_walk(), and start calling get_revision().

submodule.c::collect_changed_submodules() has its own revision walk,
but it calls reset_revision_walk() to clear these flags from all
objects in the superproject (i.e. the_repository).

I _think_ the reason why this never turned out to be a problem in
practice is because we do not run this helper twice for the same
submodule.  Even though we may smudge many objects from a submodule
with an object walk without clearing their flags, as long as we
run the next object walk in a different submodule whose object flags
are still unsmudged, it would be OK.


^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
  2022-07-19  6:39         ` Junio C Hamano
  2022-07-19  7:13         ` Junio C Hamano
@ 2022-07-25  6:05         ` Ævar Arnfjörð Bjarmason
  2022-07-25 12:11           ` Ævar Arnfjörð Bjarmason
  2022-07-25 12:31         ` Ævar Arnfjörð Bjarmason
  2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
  4 siblings, 1 reply; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25  6:05 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin


On Mon, Jul 18 2022, Calvin Wan wrote:


One of the CI failures in "seen" is because of my topic to mark (among
other things) t1500*.sh as passing with SANITIZE=leak, and this change.

Because...

> ..
>  	object_array_clear(&merges);
>  cleanup:
> +	if (!ret) {
> +		if (!csub) {
> +			CALLOC_ARRAY(csub, 1);
> +		}
> +		csub_item.oid = xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
> +		csub_item.path = xstrdup(path);
> +		csub_item.resolution_exists = resolution_exists;
> +		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);

... in "cleanup" we're ALLOC_GROW()-ing? I haven't looked into this yet,
but this seems susppect. This is line 1879 in the following stacktrace:
	
	+ git -C super merge branch1
	Failed to merge submodule dir/sub
	CONFLICT (submodule): Merge conflict in dir/sub
	Recursive merging with submodules currently only supports trivial cases.
	Please manually handle the merging of each conflicted submodule.
	This can be accomplished with the following steps:
	 - go to submodule (dir/sub), and either merge commit 7018b5f
	or update to an existing commit which has merged those changes
	 - come back to superproject, and `git add dir/sub` to record the above merge or update
	 - resolve any other conflicts in the superproject
	 - commit the resulting index in the superproject
	Automatic merge failed; fix conflicts and then commit the result.
	
	=================================================================
	==31261==ERROR: LeakSanitizer: detected memory leaks
	
	Direct leak of 576 byte(s) in 1 object(s) allocated from:
	    #0 0x4565ad in __interceptor_realloc (git+0x4565ad)
	    #1 0x76ecfd in xrealloc wrapper.c:136:8
	    #2 0x64fcd3 in merge_submodule merge-ort.c:1879:3
	    #3 0x64ee9b in handle_content_merge merge-ort.c:2118:11
	    #4 0x651c14 in process_entry merge-ort.c:4056:17
	    #5 0x648c05 in process_entries merge-ort.c:4267:4
	    #6 0x646c03 in merge_ort_nonrecursive_internal merge-ort.c:4893:2
	    #7 0x6470f3 in merge_ort_internal merge-ort.c:4982:2
	    #8 0x646de0 in merge_incore_recursive merge-ort.c:5033:2
	    #9 0x652d1a in merge_ort_recursive merge-ort-wrappers.c:57:2
	    #10 0x4ec0f6 in try_merge_strategy builtin/merge.c:764:12
	    #11 0x4e9bf2 in cmd_merge builtin/merge.c:1710:9
	    #12 0x45a3aa in run_builtin git.c:466:11
	    #13 0x458e41 in handle_builtin git.c:720:3
	    #14 0x459d85 in run_argv git.c:787:4
	    #15 0x458bfa in cmd_main git.c:920:19
	    #16 0x56a049 in main common-main.c:56:11
	    #17 0x7fe592bca81c in __libc_start_main csu/../csu/libc-start.c:332:16
	    #18 0x431139 in _start (git+0x431139)

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-25  6:05         ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 12:11           ` Ævar Arnfjörð Bjarmason
  2022-07-25 22:03             ` Calvin Wan
  0 siblings, 1 reply; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 12:11 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin


On Mon, Jul 25 2022, Ævar Arnfjörð Bjarmason wrote:

> On Mon, Jul 18 2022, Calvin Wan wrote:
>
>
> One of the CI failures in "seen" is because of my topic to mark (among
> other things) t1500*.sh as passing with SANITIZE=leak, and this change.
>
> Because...
>
>> ..
>>  	object_array_clear(&merges);
>>  cleanup:
>> +	if (!ret) {
>> +		if (!csub) {
>> +			CALLOC_ARRAY(csub, 1);
>> +		}
>> +		csub_item.oid = xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
>> +		csub_item.path = xstrdup(path);
>> +		csub_item.resolution_exists = resolution_exists;
>> +		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
>
> ... in "cleanup" we're ALLOC_GROW()-ing? I haven't looked into this yet,
> but this seems susppect. This is line 1879 in the following stacktrace:
> 	
> 	+ git -C super merge branch1
> 	Failed to merge submodule dir/sub
> 	CONFLICT (submodule): Merge conflict in dir/sub
> 	Recursive merging with submodules currently only supports trivial cases.
> 	Please manually handle the merging of each conflicted submodule.
> 	This can be accomplished with the following steps:
> 	 - go to submodule (dir/sub), and either merge commit 7018b5f
> 	or update to an existing commit which has merged those changes
> 	 - come back to superproject, and `git add dir/sub` to record the above merge or update
> 	 - resolve any other conflicts in the superproject
> 	 - commit the resulting index in the superproject
> 	Automatic merge failed; fix conflicts and then commit the result.
> 	
> 	=================================================================
> 	==31261==ERROR: LeakSanitizer: detected memory leaks
> 	
> 	Direct leak of 576 byte(s) in 1 object(s) allocated from:
> 	    #0 0x4565ad in __interceptor_realloc (git+0x4565ad)
> 	    #1 0x76ecfd in xrealloc wrapper.c:136:8
> 	    #2 0x64fcd3 in merge_submodule merge-ort.c:1879:3
> 	    #3 0x64ee9b in handle_content_merge merge-ort.c:2118:11
> 	    #4 0x651c14 in process_entry merge-ort.c:4056:17
> 	    #5 0x648c05 in process_entries merge-ort.c:4267:4
> 	    #6 0x646c03 in merge_ort_nonrecursive_internal merge-ort.c:4893:2
> 	    #7 0x6470f3 in merge_ort_internal merge-ort.c:4982:2
> 	    #8 0x646de0 in merge_incore_recursive merge-ort.c:5033:2
> 	    #9 0x652d1a in merge_ort_recursive merge-ort-wrappers.c:57:2
> 	    #10 0x4ec0f6 in try_merge_strategy builtin/merge.c:764:12
> 	    #11 0x4e9bf2 in cmd_merge builtin/merge.c:1710:9
> 	    #12 0x45a3aa in run_builtin git.c:466:11
> 	    #13 0x458e41 in handle_builtin git.c:720:3
> 	    #14 0x459d85 in run_argv git.c:787:4
> 	    #15 0x458bfa in cmd_main git.c:920:19
> 	    #16 0x56a049 in main common-main.c:56:11
> 	    #17 0x7fe592bca81c in __libc_start_main csu/../csu/libc-start.c:332:16
> 	    #18 0x431139 in _start (git+0x431139)

Looking at this a bit more this fixes it, and also the NIH allocation
pattern & what we can do with a "struct string_list" instead of managing
our own allocations.

I did wonder why you need to allocate the "oid" at all, i.e. at the time
of merge_submodules() can't we just squirrel away the "const struct
object_id *" and only when we emit the message later run
repo_find_unique_abbrev()?

I think we can do that, but it means re-init-ing the submodule, and I'm
not sure if there's any edge cases with the state changing between us
trying merge_submodules() and the eventual messages we emit.

But it would mean that we'd just need to allocate a pointer to that oid
+ resolution_exists.

Perhaps we could also continue down that path and do this with just one
allocation for that "oid/resolution_exists" side-data. I.e. you'd have a
second string_list in the struct, have a non-NULL util mean "resolution
exists", and the "string" there would be nodup'd (we could refer to the
"string" in the first one).

But I think all of that probably isn't worth it, but the below seems
like a good trade-off & good place to stop, and fixes the memory leak
this topic introduces.

 merge-ort.c | 69 +++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 40 insertions(+), 29 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 154d33f2f45..5cf87f70b58 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -293,16 +293,17 @@ struct rename_info {
 };
 
 struct conflicted_submodule_item {
-	char *oid;
-	char *path;
-	int resolution_exists;
+	char *abbrev;
+	int resolution_exists:1;
 };
 
-struct conflicted_submodule_list {
-	struct conflicted_submodule_item *items;
-	size_t nr;
-	size_t alloc;
-};
+static void conflicted_submodule_item_free(void *util, const char *str)
+{
+	struct conflicted_submodule_item *item = util;
+
+	free(item->abbrev);
+	free(item);
+}
 
 struct merge_options_internal {
 	/*
@@ -401,7 +402,7 @@ struct merge_options_internal {
 	int call_depth;
 
 	/* field that holds submodule conflict information */
-	struct conflicted_submodule_list conflicted_submodules;
+	struct string_list conflicted_submodules;
 };
 
 struct version_info {
@@ -701,6 +702,9 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 
 	mem_pool_discard(&opti->pool, 0);
 
+	string_list_clear_func(&opti->conflicted_submodules,
+			       conflicted_submodule_item_free);
+
 	/* Clean out callback_data as well. */
 	FREE_AND_NULL(renames->callback_data);
 	renames->callback_data_nr = renames->callback_data_alloc = 0;
@@ -1756,8 +1760,6 @@ static int merge_submodule(struct merge_options *opt,
 	struct commit *commit_o, *commit_a, *commit_b;
 	int parent_count;
 	struct object_array merges;
-	struct conflicted_submodule_list *csub = &opt->priv->conflicted_submodules;
-	struct conflicted_submodule_item csub_item;
 	int resolution_exists = 0;
 
 	int i;
@@ -1870,15 +1872,17 @@ static int merge_submodule(struct merge_options *opt,
 	object_array_clear(&merges);
 cleanup:
 	if (!ret) {
-		if (!csub) {
-			CALLOC_ARRAY(csub, 1);
-		}
-		csub_item.oid = xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
-		csub_item.path = xstrdup(path);
-		csub_item.resolution_exists = resolution_exists;
-		ALLOC_GROW(csub->items, csub->nr + 1, csub->alloc);
-		csub->items[csub->nr++] = csub_item;
-		opt->priv->conflicted_submodules = *csub;
+		struct string_list *csub = &opt->priv->conflicted_submodules;
+		struct conflicted_submodule_item *util;
+		const char *abbrev;
+
+		abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);;
+
+		util = xmalloc(sizeof(*util));
+		util->abbrev = xstrdup(abbrev);
+		util->resolution_exists = resolution_exists;
+
+		string_list_append(csub, path)->util = util;
 	}
 	repo_clear(&subrepo);
 	return ret;
@@ -4465,23 +4469,26 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
-static void print_submodule_conflict_suggestion(struct conflicted_submodule_list *csub) {
-	if (csub && csub->nr > 0) {
-		int i;
+static void print_submodule_conflict_suggestion(struct string_list *csub) {
+	if (csub->nr > 0) {
+		struct string_list_item *item;
+
 		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
 			"Please manually handle the merging of each conflicted submodule.\n"
 			"This can be accomplished with the following steps:\n"));
-		for (i = 0; i < csub->nr; i++) {
+
+		for_each_string_list_item(item, csub) {
+			struct conflicted_submodule_item *util = item->util;
+
 			printf(_(" - go to submodule (%s), and either merge commit %s\n"
 				    "or update to an existing commit which has merged those changes\n"),
-					csub->items[i].path,
-					csub->items[i].oid);
-			if (csub->items[i].resolution_exists)
+					item->string, util->abbrev);
+			if (util->resolution_exists)
 				printf(_("such as one listed above\n"));
 		}
 		printf(_(" - come back to superproject, and `git add"));
-		for (i = 0; i < csub->nr; i++)
-			printf(_(" %s"), csub->items[i].path);
+		for_each_string_list_item(item, csub)
+			printf(_(" %s"), item->string);
 		printf(_("` to record the above merge or update \n"
 			" - resolve any other conflicts in the superproject\n"
 			" - commit the resulting index in the superproject\n"));
@@ -4538,6 +4545,8 @@ void merge_display_update_messages(struct merge_options *opt,
 	string_list_clear(&olist, 0);
 
 	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+	string_list_clear_func(&opti->conflicted_submodules,
+			       conflicted_submodule_item_free);
 
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
@@ -4795,6 +4804,8 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	 */
 	strmap_init(&opt->priv->conflicts);
 
+	string_list_init_dup(&opt->priv->conflicted_submodules);
+
 	trace2_region_leave("merge", "allocate/init", opt->repo);
 }
 

^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
                           ` (2 preceding siblings ...)
  2022-07-25  6:05         ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 12:31         ` Ævar Arnfjörð Bjarmason
  2022-07-25 21:27           ` Calvin Wan
  2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
  4 siblings, 1 reply; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-25 12:31 UTC (permalink / raw)
  To: Calvin Wan
  Cc: git, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin


On Mon, Jul 18 2022, Calvin Wan wrote:

I'll have some more comments, but just on the output/i18n:

> +		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
> +			"Please manually handle the merging of each conflicted submodule.\n"
> +			"This can be accomplished with the following steps:\n"));
> +		for (i = 0; i < csub->nr; i++) {
> +			printf(_(" - go to submodule (%s), and either merge commit %s\n"
> +				    "or update to an existing commit which has merged those changes\n"),

This seems to have some formatting issues, i.e. we'll emit things like:

 - one
one continued
 - tow
two continued

Whereas we want this, surely?;

 - one
   one continued
 - two
   two continued

I.e. we're using " - " to mark list items, but then not indenting the
items.


> +					csub->items[i].path,
> +					csub->items[i].oid);
> +			if (csub->items[i].resolution_exists)
> +				printf(_("such as one listed above\n"));

	that people might read this backwards
	You need to consider

:)

I.e. this is "translation lego" that we try to avoid.

It's a bit more verbose (but it often is, unfortunately)

I think you can borrow a bit from ba5e8a0eb80 (object-name: make
ambiguous object output translatable, 2022-01-27) here, i.e.:

 1. Just translate a message like "go to submodule ...\nor update to
    an", have another variant for the "resolution exists".

 2. As translators retain those \n split those lines and format them
    with e.g. _(" %s") (you could borrow the string used in
    object-name.c via ba5e8a0eb80), or _("- %s").

    This will give you list-itemized output, which will also format
    correctly in RTL languages, and takes the formatting concerns
    completely out of the hands of translators, and allows us to change
    it later ourselves.

    I.e. we can safely assume that for a \n-delimited translation we can
    take \n-delimited input from a translator and treat the first line
    specially as a "first line in new list item".

> +		}
> +		printf(_(" - come back to superproject, and `git add"));
> +		for (i = 0; i < csub->nr; i++)
> +			printf(_(" %s"), csub->items[i].path);

More inconsistent indentation, and per ba5e8a0eb80 you should explain
any addition of magical formatting like " %s" with a TRANSLATORS
comment.


> +		printf(_("` to record the above merge or update \n"
> +			" - resolve any other conflicts in the superproject\n"
> +			" - commit the resulting index in the superproject\n"));

A bit more odd formatting, i.e.:

 - A trailing  " " before a \n?

 - " " after `, what is that ` doing? At first I thought it was a typo,
   but looking again it's a continuation of `` from above

   It's quite odd to tell a user to run a command with the `` syntax,
   which is used for interpolation. Let's instead suggest:

        blah blah blah run:

		git add foo \
			bar \ [...]
	                baz

   I.e. use \ at the end of lines to note a multi-line command, not wrap
   the whole thing in ``-quotes.

 - If a message must always end in a \n just add it between _()'s,
   instead of making it part of the _() string.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-25 12:31         ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 21:27           ` Calvin Wan
  0 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-25 21:27 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin

Hi Ævar,

> Whereas we want this, surely?;
>
>  - one
>    one continued
>  - two
>    two continued
>
> I.e. we're using " - " to mark list items, but then not indenting the
> items.

Yes we surely do!

> I think you can borrow a bit from ba5e8a0eb80 (object-name: make
> ambiguous object output translatable, 2022-01-27) here, i.e.:
>
>  1. Just translate a message like "go to submodule ...\nor update to
>     an", have another variant for the "resolution exists".

I am removing the "resolution exists" variant because the text that
came with it was unclear to begin with.

> per ba5e8a0eb80 you should explain
> any addition of magical formatting like " %s" with a TRANSLATORS
> comment.

ack

>  - A trailing  " " before a \n?

ack

>    It's quite odd to tell a user to run a command with the `` syntax,
>    which is used for interpolation. Let's instead suggest:
>
>         blah blah blah run:
>
>                 git add foo \
>                         bar \ [...]
>                         baz
>
>    I.e. use \ at the end of lines to note a multi-line command, not wrap
>    the whole thing in ``-quotes.

That looks much better than what I currently have.

>  - If a message must always end in a \n just add it between _()'s,
>    instead of making it part of the _() string.

ack

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v5] submodule merge: update conflict error message
  2022-07-25 12:11           ` Ævar Arnfjörð Bjarmason
@ 2022-07-25 22:03             ` Calvin Wan
  0 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-25 22:03 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, chooglen, gitster, newren, levraiphilippeblain,
	Johannes.Schindelin

Hi Ævar,

Thanks in advance for the suggestions and catching my memory leak.

> Perhaps we could also continue down that path and do this with just one
> allocation for that "oid/resolution_exists" side-data. I.e. you'd have a
> second string_list in the struct, have a non-NULL util mean "resolution
> exists", and the "string" there would be nodup'd (we could refer to the
> "string" in the first one).

Since I am removing the "resolution exists" field, I modified your changes
to use the util to only hold the abbrev.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v6] submodule merge: update conflict error message
  2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
                           ` (3 preceding siblings ...)
  2022-07-25 12:31         ` Ævar Arnfjörð Bjarmason
@ 2022-07-26 21:00         ` Calvin Wan
  2022-07-27  1:13           ` Elijah Newren
                             ` (2 more replies)
  4 siblings, 3 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-26 21:00 UTC (permalink / raw)
  To: git; +Cc: Calvin Wan, gitster, newren, Johannes.Schindelin, avarab

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded or trivially resolved, the merge
fails and git prints an error message that accurately describes the
failure, but does not provide steps for the user to resolve the error.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers.

Update error message to provide steps to resolve submodule merge
conflict. Future work could involve adding an advice flag to the
message. Although the message is long, it also has the id of the 
commit that needs to be merged, which could be useful information
for the user.

Signed-off-by: Calvin Wan <calvinwan@google.com>
---

I'm a little unsure on the code style for the `git add` section of
the error message. It works, but is tricky for a code reviewer
to decipher the formatting. I don't think it should affect
translation since the text is only a git command and folder names.

Changes since v5:
 - Fixed memory leak reported by Ævar
 - Fixed error message formatting and addded TRANSLATOR tags
 - Removed "resolution exists"

 merge-ort.c                 | 61 +++++++++++++++++++++++++++++++++++++
 t/t6437-submodule-merge.sh  | 38 +++++++++++++++++++----
 t/t7402-submodule-rebase.sh |  9 ++++--
 3 files changed, 100 insertions(+), 8 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 01f150ef3b..147be0ce31 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -387,6 +387,9 @@ struct merge_options_internal {
 
 	/* call_depth: recursion level counter for merging merge bases */
 	int call_depth;
+
+	/* field that holds submodule conflict information */
+	struct string_list conflicted_submodules;
 };
 
 struct version_info {
@@ -686,6 +689,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 
 	mem_pool_discard(&opti->pool, 0);
 
+	string_list_clear(&opti->conflicted_submodules, 1);
+
 	/* Clean out callback_data as well. */
 	FREE_AND_NULL(renames->callback_data);
 	renames->callback_data_nr = renames->callback_data_alloc = 0;
@@ -1849,6 +1854,17 @@ static int merge_submodule(struct merge_options *opt,
 
 	object_array_clear(&merges);
 cleanup:
+	if (!ret) {
+		struct string_list *csub = &opt->priv->conflicted_submodules;
+		char *util;
+		const char *abbrev;
+
+		abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);
+		util = xstrdup(abbrev);
+
+		string_list_append(csub, path)->util = util;
+	}
+
 	repo_clear(&subrepo);
 	return ret;
 }
@@ -4412,6 +4428,47 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+static void print_submodule_conflict_suggestion(struct string_list *csub) {
+	int first = 1;
+	if (csub->nr > 0) {
+		struct string_list_item *item;
+		printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
+			"Please manually handle the merging of each conflicted submodule.\n"
+			"This can be accomplished with the following steps:\n"));
+
+		for_each_string_list_item(item, csub) {
+            const char *abbrev= item->util;
+			/*
+			 * TRANSLATORS: This is a line of advice to resolve a merge conflict
+			 * in a submodule. The second argument is the abbreviated id of the
+			 * commit that needs to be merged.
+			 * E.g. - go to submodule (sub), and either merge commit abc1234"
+			 */
+			printf(_(" - go to submodule (%s), and either merge commit %s\n"
+				    "   or update to an existing commit which has merged those changes\n"),
+					item->string, abbrev);
+		}
+		printf(_(" - come back to superproject and run:\n\n"));
+		for_each_string_list_item(item, csub)
+			/*
+			 * TRANSLATORS: This is a line of a recommended `git add` command
+			 * with multiple lines of submodule folders.
+			 * E.g.:     git add sub \
+			 *                   sub2 \
+			 *                   sub3
+			 */
+			if (first) {
+				printf("       git add %s", item->string);
+				first = 0;
+			} else {
+ 				printf(" \\\n               %s", item->string);
+			}
+		printf(_("\n\n   to record the above merge or update\n"
+			" - resolve any other conflicts in the superproject\n"
+			" - commit the resulting index in the superproject\n"));
+	}
+}
+
 void merge_display_update_messages(struct merge_options *opt,
 				   int detailed,
 				   struct merge_result *result)
@@ -4461,6 +4518,9 @@ void merge_display_update_messages(struct merge_options *opt,
 	}
 	string_list_clear(&olist, 0);
 
+	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+	string_list_clear(&opti->conflicted_submodules, 1);
+
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
 			       opti->renames.needed_limit, 0);
@@ -4657,6 +4717,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	trace2_region_enter("merge", "allocate/init", opt->repo);
 	if (opt->priv) {
 		clear_or_reinit_internal_opts(opt->priv, 1);
+		string_list_init_dup(&opt->priv->conflicted_submodules);
 		trace2_region_leave("merge", "allocate/init", opt->repo);
 		return;
 	}
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index c253bf759a..84913525cf 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
 
+test_expect_success 'merging should conflict for non fast-forward' '
+	test_when_finished "git -C merge-search reset --hard" &&
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi)
+'
+
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse --short sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
@@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 ) &&
 	 if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	 then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	 else
 		test_must_fail git merge c 2> actual
 	 fi &&
@@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+		grep "$sub_expect" actual
+	fi)
 '
 
 
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..ebeca12a71 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
+		grep "$sub_expect" actual_output
+	fi
 '
 
 test_done

base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
-- 
2.37.1.359.gd136c6c3e2-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v6] submodule merge: update conflict error message
  2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
@ 2022-07-27  1:13           ` Elijah Newren
  2022-07-27 22:00             ` Calvin Wan
  2022-07-27  9:20           ` Ævar Arnfjörð Bjarmason
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
  2 siblings, 1 reply; 60+ messages in thread
From: Elijah Newren @ 2022-07-27  1:13 UTC (permalink / raw)
  To: Calvin Wan
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

On Tue, Jul 26, 2022 at 2:00 PM Calvin Wan <calvinwan@google.com> wrote:
>
> When attempting to merge in a superproject with conflicting submodule
> pointers that cannot be fast-forwarded or trivially resolved, the merge
> fails and git prints an error message that accurately describes the

micro nit: s/git/Git/  (feel free to ignore since you're already at v6)

> failure, but does not provide steps for the user to resolve the error.
>
> Git is left in a conflicted state, which requires the user to:
>  1. merge submodules or update submodules to an already existing
>         commit that reflects the merge
>  2. add submodules changes to the superproject
>  3. finish merging superproject
> These steps are non-obvious for newer submodule users to figure out
> based on the error message and neither `git submodule status` nor `git
> status` provide any useful pointers.
>
> Update error message to provide steps to resolve submodule merge
> conflict. Future work could involve adding an advice flag to the
> message. Although the message is long, it also has the id of the
> commit that needs to be merged, which could be useful information
> for the user.

Well explained.  One very minor suggestion: perhaps change "id of the
commit" to "id of the submodule commit" just to make it slightly
clearer that this information would take work for the user to discover
on their own?  (When I first read it, I was thinking, "but they have
the commit, it's what they passed to merge", before I realized my
error.)

> Signed-off-by: Calvin Wan <calvinwan@google.com>
> ---
>
> I'm a little unsure on the code style for the `git add` section of
> the error message. It works, but is tricky for a code reviewer
> to decipher the formatting. I don't think it should affect
> translation since the text is only a git command and folder names.
>
> Changes since v5:
>  - Fixed memory leak reported by Ævar
>  - Fixed error message formatting and addded TRANSLATOR tags
>  - Removed "resolution exists"
>
>  merge-ort.c                 | 61 +++++++++++++++++++++++++++++++++++++
>  t/t6437-submodule-merge.sh  | 38 +++++++++++++++++++----
>  t/t7402-submodule-rebase.sh |  9 ++++--
>  3 files changed, 100 insertions(+), 8 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 01f150ef3b..147be0ce31 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -387,6 +387,9 @@ struct merge_options_internal {
>
>         /* call_depth: recursion level counter for merging merge bases */
>         int call_depth;
> +
> +       /* field that holds submodule conflict information */
> +       struct string_list conflicted_submodules;
>  };
>
>  struct version_info {
> @@ -686,6 +689,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
>
>         mem_pool_discard(&opti->pool, 0);
>
> +       string_list_clear(&opti->conflicted_submodules, 1);
> +
>         /* Clean out callback_data as well. */
>         FREE_AND_NULL(renames->callback_data);
>         renames->callback_data_nr = renames->callback_data_alloc = 0;
> @@ -1849,6 +1854,17 @@ static int merge_submodule(struct merge_options *opt,

Sorry for not catching this in an earlier round, but merge_submodule()
has four "return 0" cases, for particular types of conflicts.  Those
should probably be switched to "goto cleanup" or something like that,
so that these messages you are adding are also provided if one of
those conflict cases are hit.

>         object_array_clear(&merges);
>  cleanup:
> +       if (!ret) {

And here's another item I have to apologize for not catching in an
earlier round.  We should also require !opt->priv->call_depth as well
in this if-condition.  If merging of merge bases produced a submodule
conflict, but the outer merge involves two sides that resolved the
inner conflict the exact same way, then there's no conflict at the
outer level and nothing for the user to resolve.  If users don't have
any conflicts to resolve, we don't want to print messages telling them
how to resolve their non-existent conflicts.  And if there is still a
conflict in the submodule for the outer merge as well as in the
recursive merge(s), we don't want to list the module twice (or more)
when we tell the user to fix conflicts in their submodules (especially
since that means we'd be telling them to merge multiple different
commits for the single submodule, which could get confusing).

> +               struct string_list *csub = &opt->priv->conflicted_submodules;
> +               char *util;
> +               const char *abbrev;
> +
> +               abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);
> +               util = xstrdup(abbrev);
> +
> +               string_list_append(csub, path)->util = util;
> +       }
> +
>         repo_clear(&subrepo);
>         return ret;
>  }
> @@ -4412,6 +4428,47 @@ static int record_conflicted_index_entries(struct merge_options *opt)
>         return errs;
>  }
>
> +static void print_submodule_conflict_suggestion(struct string_list *csub) {
> +       int first = 1;
> +       if (csub->nr > 0) {
> +               struct string_list_item *item;
> +               printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
> +                       "Please manually handle the merging of each conflicted submodule.\n"
> +                       "This can be accomplished with the following steps:\n"));
> +
> +               for_each_string_list_item(item, csub) {
> +            const char *abbrev= item->util;

Messed up indent here?

> +                       /*
> +                        * TRANSLATORS: This is a line of advice to resolve a merge conflict
> +                        * in a submodule. The second argument is the abbreviated id of the
> +                        * commit that needs to be merged.
> +                        * E.g. - go to submodule (sub), and either merge commit abc1234"
> +                        */
> +                       printf(_(" - go to submodule (%s), and either merge commit %s\n"
> +                                   "   or update to an existing commit which has merged those changes\n"),

Indent may be wrong here too, at least if you're trying to get the
leftmost quote marks to align.  (Maybe the non-alignment was
intentional, it was just unclear because of the earlier strange
indent.)

> +                                       item->string, abbrev);
> +               }
> +               printf(_(" - come back to superproject and run:\n\n"));
> +               for_each_string_list_item(item, csub)
> +                       /*
> +                        * TRANSLATORS: This is a line of a recommended `git add` command
> +                        * with multiple lines of submodule folders.
> +                        * E.g.:     git add sub \
> +                        *                   sub2 \
> +                        *                   sub3

Why does such a message need to be translated?  It's literal text the
user should type, right?  I'm not sure what a translator would do with
the message other than regurgitate it.

> +                        */
> +                       if (first) {
> +                               printf("       git add %s", item->string);

But if you did mean for there to be a translation and a TRANSLATORS
note, then did you forget to translate it by calling _()?

> +                               first = 0;
> +                       } else {
> +                               printf(" \\\n               %s", item->string);
> +                       }

Can we put braces around this for_each_string_list_item() block?  Or,
as an alternative to the whole block, do you want to consider:

   strub strbuf tmp = STRBUF_INIT;
   strbuf_add_separated_string_list(&tmp, ' ', csub);
   printf(_("    git add %s"), tmp.buf);   /* or maybe remove the
translation; not sure what the point is */
   strbuf_release(&tmp);

?  It is likely easier to copy & paste, and might be understood by
more users (I'm not sure how many are aware that command lines can use
backslashes for line continuation), but on the negative side, if you
have a lot of submodules it might make it harder to read.  Even if you
don't like space separated, though, you could still use this strategy
by changing the second line to

    strbuf_add_separated_string_list(&tmp, " \\\n               ", csub);

> +               printf(_("\n\n   to record the above merge or update\n"
> +                       " - resolve any other conflicts in the superproject\n"
> +                       " - commit the resulting index in the superproject\n"));
> +       }
> +}
> +
>  void merge_display_update_messages(struct merge_options *opt,
>                                    int detailed,
>                                    struct merge_result *result)
> @@ -4461,6 +4518,9 @@ void merge_display_update_messages(struct merge_options *opt,
>         }
>         string_list_clear(&olist, 0);
>
> +       print_submodule_conflict_suggestion(&opti->conflicted_submodules);
> +       string_list_clear(&opti->conflicted_submodules, 1);
> +

It would be more consistent to have things allocated in merge_start()
continue to be cleared out in clear_or_reinit_internal_opts().  This
kind of breaks that pairing, and you're already making sure to clear
it there, so I'd rather remove this duplicate string_list_clear()
call.

>         /* Also include needed rename limit adjustment now */
>         diff_warn_rename_limit("merge.renamelimit",
>                                opti->renames.needed_limit, 0);
> @@ -4657,6 +4717,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
>         trace2_region_enter("merge", "allocate/init", opt->repo);
>         if (opt->priv) {
>                 clear_or_reinit_internal_opts(opt->priv, 1);
> +               string_list_init_dup(&opt->priv->conflicted_submodules);

This works, but there is a minor optimization available here if you're
interested (I understand if you're not since you're already at v6).
Assuming you make the important opt->priv->call_depth fix, you can
replace string_list_init_dup() with string_list_init_nodup() here.
The paths aren't freed until clear_or_reinit_internal_opts() which
(under the assumption previously stated) isn't called until
merge_finalize(), which comes after the call to
merge_display_update_messages(), which is where you use the data.

(As repository paths are used all over merge-ort.c, the optimization
to store them in one place (opt->priv->paths) and avoid duplicating
them is used pretty heavily.  It's more important for a lot of the
other strmaps since they'll have a lot more paths in them, but it is
kind of nice to use this optimization where possible.)

>                 trace2_region_leave("merge", "allocate/init", opt->repo);
>                 return;
>         }
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index c253bf759a..84913525cf 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
>          echo "file-c" > file-c &&
>          git add file-c &&
>          git commit -m "sub-c") &&
> -       git commit -a -m "c" &&
> +       git commit -a -m "c")
> +'
>
> +test_expect_success 'merging should conflict for non fast-forward' '
> +       test_when_finished "git -C merge-search reset --hard" &&
> +       (cd merge-search &&
> +        git checkout -b test-nonforward-a b &&
> +         if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +         then
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
> +         else
> +               test_must_fail git merge c 2> actual
> +         fi)
> +'
> +
> +test_expect_success 'finish setup for merge-search' '
> +       (cd merge-search &&
>         git checkout -b d a &&
>         (cd sub &&
>          git checkout -b sub-d sub-b &&
> @@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
>          test_cmp expect actual)
>  '
>
> -test_expect_success 'merging should conflict for non fast-forward' '
> +test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
>         (cd merge-search &&
> -        git checkout -b test-nonforward b &&
> +        git checkout -b test-nonforward-b b &&
>          (cd sub &&
>           git rev-parse --short sub-d > ../expect) &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
> @@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          ) &&
>          if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>          then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>          else
>                 test_must_fail git merge c 2> actual
>          fi &&
> @@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
>         git commit -a -m "f" &&
>
>         git checkout -b test-backward e &&
> -       test_must_fail git merge f)
> +       test_must_fail git merge f >actual &&
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +    then
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> +               grep "$sub_expect" actual
> +       fi)
>  '
>
>
> diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
> index 8e32f19007..ebeca12a71 100755
> --- a/t/t7402-submodule-rebase.sh
> +++ b/t/t7402-submodule-rebase.sh
> @@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
>         test_tick &&
>         git commit -m fourth &&
>
> -       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
> +       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
>         git ls-files -s submodule >actual &&
>         (
>                 cd submodule &&
> @@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
>                 echo "160000 $(git rev-parse HEAD^^) 2  submodule" &&
>                 echo "160000 $(git rev-parse HEAD) 3    submodule"
>         ) >expect &&
> -       test_cmp expect actual
> +       test_cmp expect actual &&
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +    then
> +               sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
> +               grep "$sub_expect" actual_output
> +       fi
>  '
>
>  test_done
>
> base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
> --
> 2.37.1.359.gd136c6c3e2-goog

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v6] submodule merge: update conflict error message
  2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
  2022-07-27  1:13           ` Elijah Newren
@ 2022-07-27  9:20           ` Ævar Arnfjörð Bjarmason
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
  2 siblings, 0 replies; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-27  9:20 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, gitster, newren, Johannes.Schindelin


On Tue, Jul 26 2022, Calvin Wan wrote:


Aside from what Elijah pointed out already...

> +
> +	/* field that holds submodule conflict information */
> +	struct string_list conflicted_submodules;

Looks good!

>  cleanup:
> +	if (!ret) {
> +		struct string_list *csub = &opt->priv->conflicted_submodules;
> +		char *util;
> +		const char *abbrev;
> +
> +		abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);
> +		util = xstrdup(abbrev);
> +
> +		string_list_append(csub, path)->util = util;

Elijah pointed out that these don't need to be dup'd at all, and you
should follow that advice. I'm not really familiar at all with this code
(compared to him).

FWIW the "util" here could be dropped in any case, in my version it was
because we were making a struct, but the idiom for this would just be:

	string_list_append(....)->util = xstrdup(...);

> +			printf(_(" - go to submodule (%s), and either merge commit %s\n"
> +				    "   or update to an existing commit which has merged those changes\n"),
> +					item->string, abbrev);

FWIW what I mentioned in v5 was to arrange this so that " - " or
whatever would be _()'d separately, so translators wouldn't need to
worry about the formatting...

> +				printf("       git add %s", item->string);
> +				first = 0;
> +			} else {
> + 				printf(" \\\n               %s", item->string);

And if we're translating *some* whitespace we should translate all of
it. In RTL languages the a string like " foo" needs to be translated as
"foo ". I.e. the whitespace from the "right" side of your terminal.

That was what I was pointing out in the object-name.c code,
usage_with_options_internal() is another example.

> +			}
> +		printf(_("\n\n   to record the above merge or update\n"

We can add \n\n unconditionally, no need to put it in the translation.


^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v6] submodule merge: update conflict error message
  2022-07-27  1:13           ` Elijah Newren
@ 2022-07-27 22:00             ` Calvin Wan
  2022-07-28  0:41               ` Elijah Newren
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-07-27 22:00 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

> Well explained.  One very minor suggestion: perhaps change "id of the
> commit" to "id of the submodule commit" just to make it slightly
> clearer that this information would take work for the user to discover
> on their own?  (When I first read it, I was thinking, "but they have
> the commit, it's what they passed to merge", before I realized my
> error.)

ack

> Sorry for not catching this in an earlier round, but merge_submodule()
> has four "return 0" cases, for particular types of conflicts.  Those
> should probably be switched to "goto cleanup" or something like that,
> so that these messages you are adding are also provided if one of
> those conflict cases are hit.

I didn't send these four "return 0" cases to cleanup because I thought
the error message wouldn't accurately reflect the resolution steps. Is
merging or updating the submodule still the correct resolution? The
first three cases are for a null o/a/b, and the fourth case is for a missing
local submodule. Also in cleanup, the subrepo is cleared but the
subrepo hasn't been initialized/failed to initialize in these four cases.

> >         object_array_clear(&merges);
> >  cleanup:
> > +       if (!ret) {
>
> And here's another item I have to apologize for not catching in an
> earlier round.  We should also require !opt->priv->call_depth as well
> in this if-condition.  If merging of merge bases produced a submodule
> conflict, but the outer merge involves two sides that resolved the
> inner conflict the exact same way, then there's no conflict at the
> outer level and nothing for the user to resolve.  If users don't have
> any conflicts to resolve, we don't want to print messages telling them
> how to resolve their non-existent conflicts.  And if there is still a
> conflict in the submodule for the outer merge as well as in the
> recursive merge(s), we don't want to list the module twice (or more)
> when we tell the user to fix conflicts in their submodules (especially
> since that means we'd be telling them to merge multiple different
> commits for the single submodule, which could get confusing).

ack.

> > +               for_each_string_list_item(item, csub) {
> > +            const char *abbrev= item->util;
>
> Messed up indent here?

Looks like going from my editor to `git format-patch` messed
something up here.

> > +               for_each_string_list_item(item, csub)
> > +                       /*
> > +                        * TRANSLATORS: This is a line of a recommended `git add` command
> > +                        * with multiple lines of submodule folders.
> > +                        * E.g.:     git add sub \
> > +                        *                   sub2 \
> > +                        *                   sub3
>
> Why does such a message need to be translated?  It's literal text the
> user should type, right?  I'm not sure what a translator would do with
> the message other than regurgitate it.

It doesn't. My point was to let the translator know that the only text
in this print is for a git command. I should probably add that context
to the comment though.

> > +                        */
> > +                       if (first) {
> > +                               printf("       git add %s", item->string);
>
> But if you did mean for there to be a translation and a TRANSLATORS
> note, then did you forget to translate it by calling _()?

Same reasoning as above.

> > +                               first = 0;
> > +                       } else {
> > +                               printf(" \\\n               %s", item->string);
> > +                       }
>
> Can we put braces around this for_each_string_list_item() block?  Or,
> as an alternative to the whole block, do you want to consider:
>
>    strub strbuf tmp = STRBUF_INIT;
>    strbuf_add_separated_string_list(&tmp, ' ', csub);
>    printf(_("    git add %s"), tmp.buf);   /* or maybe remove the
> translation; not sure what the point is */
>    strbuf_release(&tmp);
> ?  It is likely easier to copy & paste, and might be understood by
> more users (I'm not sure how many are aware that command lines can use
> backslashes for line continuation), but on the negative side, if you
> have a lot of submodules it might make it harder to read.  Even if you
> don't like space separated, though, you could still use this strategy
> by changing the second line to
>
>     strbuf_add_separated_string_list(&tmp, " \\\n               ", csub);

This is a much cleaner implementation, thanks! If my goal is to make
submodule merging easier for newer submodule users, then I think it's a
good assumption to remove any additional possible points of confusion,
aka with the "command lines can use backslashes for line continuation",
so I'll swap over to spaces.

> > +       print_submodule_conflict_suggestion(&opti->conflicted_submodules);
> > +       string_list_clear(&opti->conflicted_submodules, 1);
> > +
>
> It would be more consistent to have things allocated in merge_start()
> continue to be cleared out in clear_or_reinit_internal_opts().  This
> kind of breaks that pairing, and you're already making sure to clear
> it there, so I'd rather remove this duplicate string_list_clear()
> call.

ack.

> >         /* Also include needed rename limit adjustment now */
> >         diff_warn_rename_limit("merge.renamelimit",
> >                                opti->renames.needed_limit, 0);
> > @@ -4657,6 +4717,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
> >         trace2_region_enter("merge", "allocate/init", opt->repo);
> >         if (opt->priv) {
> >                 clear_or_reinit_internal_opts(opt->priv, 1);
> > +               string_list_init_dup(&opt->priv->conflicted_submodules);
>
> This works, but there is a minor optimization available here if you're
> interested (I understand if you're not since you're already at v6).
> Assuming you make the important opt->priv->call_depth fix, you can
> replace string_list_init_dup() with string_list_init_nodup() here.
> The paths aren't freed until clear_or_reinit_internal_opts() which
> (under the assumption previously stated) isn't called until
> merge_finalize(), which comes after the call to
> merge_display_update_messages(), which is where you use the data.
>
> (As repository paths are used all over merge-ort.c, the optimization
> to store them in one place (opt->priv->paths) and avoid duplicating
> them is used pretty heavily.  It's more important for a lot of the
> other strmaps since they'll have a lot more paths in them, but it is
> kind of nice to use this optimization where possible.)

Thanks for all the context of this one. I agree it's minor, but any place
we can provide a better example to future contributors seems like a
more than worthy reason to make this change :)

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v6] submodule merge: update conflict error message
  2022-07-27 22:00             ` Calvin Wan
@ 2022-07-28  0:41               ` Elijah Newren
  2022-07-28 19:06                 ` Calvin Wan
  0 siblings, 1 reply; 60+ messages in thread
From: Elijah Newren @ 2022-07-28  0:41 UTC (permalink / raw)
  To: Calvin Wan
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

Hi,

On Wed, Jul 27, 2022 at 3:00 PM Calvin Wan <calvinwan@google.com> wrote:
[...]
> > Sorry for not catching this in an earlier round, but merge_submodule()
> > has four "return 0" cases, for particular types of conflicts.  Those
> > should probably be switched to "goto cleanup" or something like that,
> > so that these messages you are adding are also provided if one of
> > those conflict cases are hit.
>
> I didn't send these four "return 0" cases to cleanup because I thought
> the error message wouldn't accurately reflect the resolution steps. Is
> merging or updating the submodule still the correct resolution? The
> first three cases are for a null o/a/b, and the fourth case is for a missing
> local submodule. Also in cleanup, the subrepo is cleared but the
> subrepo hasn't been initialized/failed to initialize in these four cases.

Ah, I remember we partially discussed this earlier in this thread;
sorry for forgetting.

For the failed to initialize case, yes we also need a merge -- the
submodule is conflicted due to the lack of one.  The steps the user
needs to take are probably even more involved, though (they also need
to initialize the submodule), so perhaps that one should be special
cased.

The 'a' or 'b' being a null oid is actually dead code, as discussed
earlier in the thread.  Perhaps we should change those two code paths
from "return 0" to 'BUG("submodule deleted on one side; this should be
handled outside of merge_submodule()")', and possibly with a commit
message linking to
https://lore.kernel.org/git/CABPp-BE0qGwUy80dmVszkJQ+tcpfLRW0OZyErymzhZ9+HWY1mw@mail.gmail.com/
(and mentioning the "a and b being null oids within merge_submodule
will never trigger" portion of that email).

The 'o' being a null oid is not dead code.  That particular case means
that there was no submodule in the merge base, but both sides of the
merge introduced the submodule and have it checked out to different
oids.  (At least, hopefully it's the same submodule.)  In that case,
yes we do need some kind of merge.  So I think your message should
probably be included in this case, as-is.  Since the cleanup thing you
mention is an issue, perhaps you need to refactor the code a bit so
that you can make this case somehow get the same message printed for
users?

All that said, if you want to defer any or all of this to a follow-on
series, that's fine...but it would be nice to have it mentioned in the
commit message.

> > > +               for_each_string_list_item(item, csub)
> > > +                       /*
> > > +                        * TRANSLATORS: This is a line of a recommended `git add` command
> > > +                        * with multiple lines of submodule folders.
> > > +                        * E.g.:     git add sub \
> > > +                        *                   sub2 \
> > > +                        *                   sub3
> >
> > Why does such a message need to be translated?  It's literal text the
> > user should type, right?  I'm not sure what a translator would do with
> > the message other than regurgitate it.
>
> It doesn't. My point was to let the translator know that the only text
> in this print is for a git command. I should probably add that context
> to the comment though.

Um...if the string doesn't need to be marked for translation, and
isn't marked for translation, why would translators go looking for a
comment to help explain how to translate something that doesn't appear
in their list of strings they need to translate?

Using
    printf("    git add %s", ...)
means that the string "    git add %s" will not appear in the po/*.po
files.  If it had been
    printf(_("    git add %s"), ...)
then it would appear in those files with filename(s) and line
number(s) stating where the string had come from in case translators
needed to look for clues about the context in order to know how to
translate the string.

So, I think you can just drop the comment.  Or am I still not
understanding some nuance here?

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v6] submodule merge: update conflict error message
  2022-07-28  0:41               ` Elijah Newren
@ 2022-07-28 19:06                 ` Calvin Wan
  0 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-28 19:06 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

> For the failed to initialize case, yes we also need a merge -- the
> submodule is conflicted due to the lack of one.  The steps the user
> needs to take are probably even more involved, though (they also need
> to initialize the submodule), so perhaps that one should be special
> cased.

Adding a needswork bit to this one. Don't quite have the bandwidth to
figure out what would be a useful message in this case.

> The 'a' or 'b' being a null oid is actually dead code, as discussed
> earlier in the thread.  Perhaps we should change those two code paths
> from "return 0" to 'BUG("submodule deleted on one side; this should be
> handled outside of merge_submodule()")', and possibly with a commit
> message linking to
> https://lore.kernel.org/git/CABPp-BE0qGwUy80dmVszkJQ+tcpfLRW0OZyErymzhZ9+HWY1mw@mail.gmail.com/
> (and mentioning the "a and b being null oids within merge_submodule
> will never trigger" portion of that email).
>
> The 'o' being a null oid is not dead code.  That particular case means
> that there was no submodule in the merge base, but both sides of the
> merge introduced the submodule and have it checked out to different
> oids.  (At least, hopefully it's the same submodule.)  In that case,
> yes we do need some kind of merge.  So I think your message should
> probably be included in this case, as-is.  Since the cleanup thing you
> mention is an issue, perhaps you need to refactor the code a bit so
> that you can make this case somehow get the same message printed for
> users?
>
> All that said, if you want to defer any or all of this to a follow-on
> series, that's fine...but it would be nice to have it mentioned in the
> commit message.

BUGging out for 'a' and 'b' sounds reasonable to me. And I also do
believe my error message applies to the 'o' case. I'll add a test to
confirm.

> Um...if the string doesn't need to be marked for translation, and
> isn't marked for translation, why would translators go looking for a
> comment to help explain how to translate something that doesn't appear
> in their list of strings they need to translate?

Ahhhhhhhhh I see...facepalm

> Using
>     printf("    git add %s", ...)
> means that the string "    git add %s" will not appear in the po/*.po
> files.  If it had been
>     printf(_("    git add %s"), ...)
> then it would appear in those files with filename(s) and line
> number(s) stating where the string had come from in case translators
> needed to look for clues about the context in order to know how to
> translate the string.
>
> So, I think you can just drop the comment.  Or am I still not
> understanding some nuance here?

You're not missing anything. Wasn't aware of how translators worked
with the project.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v7] submodule merge: update conflict error message
  2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
  2022-07-27  1:13           ` Elijah Newren
  2022-07-27  9:20           ` Ævar Arnfjörð Bjarmason
@ 2022-07-28 21:12           ` Calvin Wan
  2022-07-28 23:22             ` Ævar Arnfjörð Bjarmason
                               ` (4 more replies)
  2 siblings, 5 replies; 60+ messages in thread
From: Calvin Wan @ 2022-07-28 21:12 UTC (permalink / raw)
  To: git; +Cc: Calvin Wan, gitster, newren, Johannes.Schindelin, avarab

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded or trivially resolved, the merge
fails and Git prints an error message that accurately describes the
failure, but does not provide steps for the user to resolve the error.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers.

Update error message to provide steps to resolve submodule merge
conflict. Future work could involve adding an advice flag to the
message. Although the message is long, it also has the id of the 
submodule commit that needs to be merged, which could be useful
information for the user.

Additionally, 4 merge failures that resulted in an early return have
been updated to reflect the status of the merge.
1. Null merge base (null o): CONFLICT_SUBMODULE_NULL_MERGE_BASE added
   as a new conflict type and will print updated error message.
2. Null merge side a (null a): BUG(). See [1] for discussion
3. Null merge side b (null b): BUG(). See [1] for discussion
4. Submodule not checked out: Still returns early, but added a
   NEEDSWORK bit since current error message does not reflect the
   correct resolution

[1] https://lore.kernel.org/git/CABPp-BE0qGwUy80dmVszkJQ+tcpfLRW0OZyErymzhZ9+HWY1mw@mail.gmail.com/

Signed-off-by: Calvin Wan <calvinwan@google.com>
---

For Elijah: Cleaned up the small nits and updated resolutions for
those 4 cases we discussed.

For Ævar: Apologies for misunderstanding your suggestions to make
my messages easier for translators to work with. I have reformatted
all of the messages to separate text vs formatting translations. Let
me know if this is what you were expecting.

 merge-ort.c                 | 112 ++++++++++++++++++++++++++++++++++--
 t/t6437-submodule-merge.sh  |  78 +++++++++++++++++++++++--
 t/t7402-submodule-rebase.sh |   9 ++-
 3 files changed, 185 insertions(+), 14 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 01f150ef3b..4302e900ee 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -387,6 +387,9 @@ struct merge_options_internal {
 
 	/* call_depth: recursion level counter for merging merge bases */
 	int call_depth;
+
+	/* field that holds submodule conflict information */
+	struct string_list conflicted_submodules;
 };
 
 struct version_info {
@@ -517,6 +520,7 @@ enum conflict_and_info_types {
 	CONFLICT_SUBMODULE_NOT_INITIALIZED,
 	CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
 	CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
+	CONFLICT_SUBMODULE_NULL_MERGE_BASE,
 
 	/* Keep this entry _last_ in the list */
 	NB_CONFLICT_TYPES,
@@ -570,6 +574,8 @@ static const char *type_short_descriptions[] = {
 		"CONFLICT (submodule history not available)",
 	[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
 		"CONFLICT (submodule may have rewinds)",
+	[CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
+		"CONFLICT (submodule no merge base)"
 };
 
 struct logical_conflict_info {
@@ -686,6 +692,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 
 	mem_pool_discard(&opti->pool, 0);
 
+	string_list_clear(&opti->conflicted_submodules, 1);
+
 	/* Clean out callback_data as well. */
 	FREE_AND_NULL(renames->callback_data);
 	renames->callback_data_nr = renames->callback_data_alloc = 0;
@@ -1744,26 +1752,40 @@ static int merge_submodule(struct merge_options *opt,
 
 	int i;
 	int search = !opt->priv->call_depth;
+	int sub_initialized = 1;
 
 	/* store fallback answer in result in case we fail */
 	oidcpy(result, opt->priv->call_depth ? o : a);
 
 	/* we can not handle deletion conflicts */
-	if (is_null_oid(o))
-		return 0;
 	if (is_null_oid(a))
-		return 0;
+		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()"); 
 	if (is_null_oid(b))
-		return 0;
+		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
 
-	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
+	if ((sub_initialized = repo_submodule_init(&subrepo,
+									opt->repo, path, null_oid()))) {
 		path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
 			 path, NULL, NULL, NULL,
 			 _("Failed to merge submodule %s (not checked out)"),
 			 path);
+		/*
+		 * NEEDSWORK: Since the steps to resolve this error are
+		 * more involved than what is currently in 
+		 * print_submodule_conflict_suggestion(), we return
+		 * immediately rather than generating an error message
+		 */
 		return 0;
 	}
 
+	if (is_null_oid(o)) {
+		path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
+			 path, NULL, NULL, NULL,
+			 _("Failed to merge submodule %s (no merge base)"),
+			 path);
+		goto cleanup;
+	}
+
 	if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
 	    !(commit_a = lookup_commit_reference(&subrepo, a)) ||
 	    !(commit_b = lookup_commit_reference(&subrepo, b))) {
@@ -1849,7 +1871,15 @@ static int merge_submodule(struct merge_options *opt,
 
 	object_array_clear(&merges);
 cleanup:
-	repo_clear(&subrepo);
+	if (!opt->priv->call_depth && !ret) {
+		struct string_list *csub = &opt->priv->conflicted_submodules;
+
+		string_list_append(csub, path)->util =
+				xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
+	}
+
+	if (!sub_initialized)
+		repo_clear(&subrepo);
 	return ret;
 }
 
@@ -4412,6 +4442,73 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+static void print_submodule_conflict_suggestion(struct string_list *csub) {
+	if (csub->nr > 0) {
+		struct string_list_item *item;
+		struct strbuf msg = STRBUF_INIT;
+		struct strbuf tmp = STRBUF_INIT;
+
+		strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
+		strbuf_addf(&msg, "%s\n", tmp.buf);
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, _("Please manually handle the merging of each conflicted submodule."));
+		strbuf_addf(&msg, "%s\n", tmp.buf);
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, _("This can be accomplished with the following steps:"));
+		strbuf_addf(&msg, "%s\n", tmp.buf);
+		strbuf_release(&tmp);
+
+		for_each_string_list_item(item, csub) {
+			const char *abbrev= item->util;
+			/*
+			 * TRANSLATORS: This is a line of advice to resolve a merge conflict
+			 * in a submodule. The second argument is the abbreviated id of the
+			 * commit that needs to be merged.
+			 * E.g. - go to submodule (sub), and either merge commit abc1234"
+			 */
+			strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s"),
+													item->string, abbrev);
+			strbuf_addf(&msg, _(" - %s"), tmp.buf);
+			strbuf_addf(&msg, "\n");
+			strbuf_release(&tmp);
+			strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));
+			strbuf_addf(&msg, _("   %s"), tmp.buf);
+			strbuf_addf(&msg, "\n");
+			strbuf_release(&tmp);
+		}
+		strbuf_addf(&tmp, _("come back to superproject and run:"));
+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
+		strbuf_addf(&msg, "\n\n");
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, "git add ");
+		strbuf_add_separated_string_list(&tmp, " ", csub);
+		strbuf_addf(&msg, _("       %s"), tmp.buf);
+		strbuf_addf(&msg, "\n\n");
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, _("to record the above merge or update"));
+		strbuf_addf(&msg, _("   %s"), tmp.buf);
+		strbuf_addf(&msg, "\n");
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, _("resolve any other conflicts in the superproject"));
+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
+		strbuf_addf(&msg, "\n");
+		strbuf_release(&tmp);
+
+		strbuf_addf(&tmp, _("commit the resulting index in the superproject"));
+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
+		strbuf_addf(&msg, "\n");
+		strbuf_release(&tmp);
+
+		printf("%s", msg.buf);
+		strbuf_release(&msg);
+	}
+}
+
 void merge_display_update_messages(struct merge_options *opt,
 				   int detailed,
 				   struct merge_result *result)
@@ -4461,6 +4558,8 @@ void merge_display_update_messages(struct merge_options *opt,
 	}
 	string_list_clear(&olist, 0);
 
+	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
 			       opti->renames.needed_limit, 0);
@@ -4657,6 +4756,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	trace2_region_enter("merge", "allocate/init", opt->repo);
 	if (opt->priv) {
 		clear_or_reinit_internal_opts(opt->priv, 1);
+		string_list_init_nodup(&opt->priv->conflicted_submodules);
 		trace2_region_leave("merge", "allocate/init", opt->repo);
 		return;
 	}
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index c253bf759a..414597a420 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
 
+test_expect_success 'merging should conflict for non fast-forward' '
+	test_when_finished "git -C merge-search reset --hard" &&
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi)
+'
+
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse --short sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
@@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 ) &&
 	 if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	 then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	 else
 		test_must_fail git merge c 2> actual
 	 fi &&
@@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+		grep "$sub_expect" actual
+	fi)
 '
 
 
@@ -476,4 +502,44 @@ test_expect_failure 'directory/submodule conflict; merge --abort works afterward
 	)
 '
 
+# Setup:
+#   - Submodule has 2 commits: a and b
+#   - Superproject branch 'a' adds and commits submodule pointing to 'commit a'
+#   - Superproject branch 'b' adds and commits submodule pointing to 'commit b'
+# If these two branches are now merged, there is no merge base
+test_expect_success 'setup for null merge base' '
+	mkdir no-merge-base &&
+	(cd no-merge-base &&
+	git init &&
+	mkdir sub &&
+	(cd sub &&
+	 git init &&
+	 echo "file-a" > file-a &&
+	 git add file-a &&
+	 git commit -m "commit a") &&
+	git commit --allow-empty -m init &&
+	git branch init &&
+	git checkout -b a init &&
+	git add sub &&
+	git commit -m "a" &&
+	git switch main &&
+	(cd sub &&
+	 echo "file-b" > file-b &&
+	 git add file-b &&
+	 git commit -m "commit b"))
+'
+
+test_expect_success 'merging should fail with no merge base' '
+	(cd no-merge-base &&
+	git checkout -b b init &&
+	git add sub &&
+	git commit -m "b" &&
+	test_must_fail git merge a >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short HEAD^1)" &&
+		grep "$sub_expect" actual
+	fi)
+'
+
 test_done
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..ebeca12a71 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
+		grep "$sub_expect" actual_output
+	fi
 '
 
 test_done

base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
-- 
2.37.1.455.g008518b4e5-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
@ 2022-07-28 23:22             ` Ævar Arnfjörð Bjarmason
  2022-07-29  0:24             ` Elijah Newren
                               ` (3 subsequent siblings)
  4 siblings, 0 replies; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-07-28 23:22 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, gitster, newren, Johannes.Schindelin


On Thu, Jul 28 2022, Calvin Wan wrote:

> For Elijah: Cleaned up the small nits and updated resolutions for
> those 4 cases we discussed.
>
> For Ævar: Apologies for misunderstanding your suggestions to make
> my messages easier for translators to work with. I have reformatted
> all of the messages to separate text vs formatting translations. Let
> me know if this is what you were expecting.

Let's take a look, and thanks for sticking with this...

>
>  merge-ort.c                 | 112 ++++++++++++++++++++++++++++++++++--
>  t/t6437-submodule-merge.sh  |  78 +++++++++++++++++++++++--
>  t/t7402-submodule-rebase.sh |   9 ++-
>  3 files changed, 185 insertions(+), 14 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 01f150ef3b..4302e900ee 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -387,6 +387,9 @@ struct merge_options_internal {
>  
>  	/* call_depth: recursion level counter for merging merge bases */
>  	int call_depth;
> +
> +	/* field that holds submodule conflict information */
> +	struct string_list conflicted_submodules;
>  };
>  
>  struct version_info {
> @@ -517,6 +520,7 @@ enum conflict_and_info_types {
>  	CONFLICT_SUBMODULE_NOT_INITIALIZED,
>  	CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
>  	CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
> +	CONFLICT_SUBMODULE_NULL_MERGE_BASE,
>  
>  	/* Keep this entry _last_ in the list */
>  	NB_CONFLICT_TYPES,
> @@ -570,6 +574,8 @@ static const char *type_short_descriptions[] = {
>  		"CONFLICT (submodule history not available)",
>  	[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
>  		"CONFLICT (submodule may have rewinds)",
> +	[CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
> +		"CONFLICT (submodule no merge base)"
>  };
>  
>  struct logical_conflict_info {
> @@ -686,6 +692,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
>  
>  	mem_pool_discard(&opti->pool, 0);
>  
> +	string_list_clear(&opti->conflicted_submodules, 1);
> +
>  	/* Clean out callback_data as well. */
>  	FREE_AND_NULL(renames->callback_data);
>  	renames->callback_data_nr = renames->callback_data_alloc = 0;
> @@ -1744,26 +1752,40 @@ static int merge_submodule(struct merge_options *opt,
>  
>  	int i;
>  	int search = !opt->priv->call_depth;
> +	int sub_initialized = 1;
>  
>  	/* store fallback answer in result in case we fail */
>  	oidcpy(result, opt->priv->call_depth ? o : a);
>  
>  	/* we can not handle deletion conflicts */
> -	if (is_null_oid(o))
> -		return 0;
>  	if (is_null_oid(a))
> -		return 0;
> +		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()"); 
>  	if (is_null_oid(b))
> -		return 0;
> +		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
>  
> -	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
> +	if ((sub_initialized = repo_submodule_init(&subrepo,
> +									opt->repo, path, null_oid()))) {
>  		path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
>  			 path, NULL, NULL, NULL,
>  			 _("Failed to merge submodule %s (not checked out)"),
>  			 path);
> +		/*
> +		 * NEEDSWORK: Since the steps to resolve this error are
> +		 * more involved than what is currently in 
> +		 * print_submodule_conflict_suggestion(), we return
> +		 * immediately rather than generating an error message
> +		 */
>  		return 0;
>  	}
>  
> +	if (is_null_oid(o)) {
> +		path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
> +			 path, NULL, NULL, NULL,
> +			 _("Failed to merge submodule %s (no merge base)"),
> +			 path);
> +		goto cleanup;
> +	}
> +
>  	if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
>  	    !(commit_a = lookup_commit_reference(&subrepo, a)) ||
>  	    !(commit_b = lookup_commit_reference(&subrepo, b))) {
> @@ -1849,7 +1871,15 @@ static int merge_submodule(struct merge_options *opt,
>  
>  	object_array_clear(&merges);
>  cleanup:
> -	repo_clear(&subrepo);
> +	if (!opt->priv->call_depth && !ret) {
> +		struct string_list *csub = &opt->priv->conflicted_submodules;
> +
> +		string_list_append(csub, path)->util =
> +				xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));

FWIW (since you may have changed this due to my comment) I meant (but
maybe didn't make clear enough) in
https://lore.kernel.org/git/220727.86ilnilxfv.gmgdl@evledraar.gmail.com/
that just getting rid of the "util" variable could trivially be done, so:

	struct string_list *csub = &opt->priv->conflicted_submodules;
	const char *abbrev;

	abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);

	string_list_append(csub, path)->util = xstrdup(abbrev);

What you have here is also just fine, but in case you traded the line
variable for line wrapping I think it's just fine (and actually
preferable) to just make an intermediate variable to avoid this sort of
line wrapping.

Anyway, just clarifying...

> +static void print_submodule_conflict_suggestion(struct string_list *csub) {
> +	if (csub->nr > 0) {

Since the entire body of the function is guarded by this maybe avoid the
indentation and:

	if (!csub->nr)
		return;


> +		struct string_list_item *item;
> +		struct strbuf msg = STRBUF_INIT;
> +		struct strbuf tmp = STRBUF_INIT;
> +
> +		strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("Please manually handle the merging of each conflicted submodule."));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("This can be accomplished with the following steps:"));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);

This was *almost* correct in your v6, i.e.:

	printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
		 "Please manually handle the merging of each conflicted submodule.\n"
		 "This can be accomplished with the following steps:\n"));

What I meant with "We can add \n\n unconditionally, no need to put it in
the translation." in
https://lore.kernel.org/git/220727.86ilnilxfv.gmgdl@evledraar.gmail.com/
is that you should avoid adding formatting to translations themselves if
possible.

i.e. what we're translating here is a full paragraph, so to a translator
it doesn't matter if it ends with ":\n" or ":", so we can add the last
"\n" unconditionalyl.

But we should *not* add the \n's within a single paragraph
unconditionally, a translator needs to be able to translate that entire
string.

Different languages split sentences differently, and different
conventions in different languages & SVO v.s. OVS or wahtever (see
https://en.wikipedia.org/wiki/Word_order) means that your last sentence
might need to come first in some languages.

So I think this should just be:

	printf(_("Recursive merging with submodules currently only supports trivial cases.\n"
		 "Please manually handle the merging of each conflicted submodule.\n"
		 "This can be accomplished with the following steps:"));
	putchar('\n');

I.e. the "\n" within the pargraph are imporant, and translators need to
be able to amend those, and perhaps some languages will have 2x, some 4x
or whatever.

Whereas the "\n" at the end is something we can always add, because it's
just a matter of how we're interpolating the paragraph into other output
(think *nix terminal v.s. say HTML, just as an example).

> +
> +		for_each_string_list_item(item, csub) {
> +			const char *abbrev= item->util;
> +			/*
> +			 * TRANSLATORS: This is a line of advice to resolve a merge conflict
> +			 * in a submodule. The second argument is the abbreviated id of the
> +			 * commit that needs to be merged.
> +			 * E.g. - go to submodule (sub), and either merge commit abc1234"
> +			 */
> +			strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s"),
> +													item->string, abbrev);

...

> +			strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +			strbuf_addf(&msg, "\n");
> +			strbuf_release(&tmp);
> +			strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));
> +			strbuf_addf(&msg, _("   %s"), tmp.buf);
> +			strbuf_addf(&msg, "\n");
> +			strbuf_release(&tmp);

Similarly this is worse, because we're now splitting up up a single
sentence or paragraph into multiple translations.

FWIW I meant that you could write some helper function that would do
something like this (in Perl, too lazy to write C now):

	perl -wE '
		my @lines = split /\n/, $ARGV[0];
		for (my $i = 0; $i < @lines; $i++) {
			my $pfx = !$i ? "- " : "  "; say "$pfx$lines[$i]";
	}' "$(printf "some multi\nline paragraph\nhere")"
	- some multi
	  line paragraph
	  here

I.e. the translator would see:

	_("some multi\nline paragraph\nhere")

Which would allow them to insert any amount of newlines, but you'd just
have a utility function that:

 * Splits those lines by \n
 * Formats the first line by _("- %s\n")
 * Formats subsequent lines with _("  %s\n") 
 * The %s in those is a _()'d string.

I.e. what's happening at the bottom of show_ambiguous_object(). It's:

 * A relatively small amount of programming,
 * Lets translators focus only on these formatting questions for the
   translations that do the magic formatting, and those only need to be
   changed for RTL languages.

   I.e. German and English whill both want "- %s\n", but e.g. Hebrew
   will want "%s -\n".

 * Makes the code easier to read, because instead of adding formatting concerns to every string, you'd just:

	strbuf_addstr(&buf, add_fmt_list_item(i, _("blah bla\nblah blah\nblah.)));

> +		strbuf_release(&tmp);

When you use the strbuf API you should strbuf_reset() within a single
scope if you want to "clear" it, not strbuf_release().

The strbuf_release() also works, but then it's a malloc()/free(), each
time, with strbuf_reset() we don't free() it, we just set the len to
zero, and the content to "\0", but remember how long it was ("alloc" in
strbuf.h).

You then only do the strbuf_release() at the very end.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
  2022-07-28 23:22             ` Ævar Arnfjörð Bjarmason
@ 2022-07-29  0:24             ` Elijah Newren
  2022-08-01 22:24               ` Calvin Wan
  2022-08-01 12:06             ` Ævar Arnfjörð Bjarmason
                               ` (2 subsequent siblings)
  4 siblings, 1 reply; 60+ messages in thread
From: Elijah Newren @ 2022-07-29  0:24 UTC (permalink / raw)
  To: Calvin Wan
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

On Thu, Jul 28, 2022 at 2:12 PM Calvin Wan <calvinwan@google.com> wrote:
>
> When attempting to merge in a superproject with conflicting submodule
> pointers that cannot be fast-forwarded or trivially resolved, the merge
> fails and Git prints an error message that accurately describes the
> failure, but does not provide steps for the user to resolve the error.
>
> Git is left in a conflicted state, which requires the user to:
>  1. merge submodules or update submodules to an already existing
>         commit that reflects the merge
>  2. add submodules changes to the superproject
>  3. finish merging superproject
> These steps are non-obvious for newer submodule users to figure out
> based on the error message and neither `git submodule status` nor `git
> status` provide any useful pointers.
>
> Update error message to provide steps to resolve submodule merge
> conflict. Future work could involve adding an advice flag to the
> message. Although the message is long, it also has the id of the
> submodule commit that needs to be merged, which could be useful
> information for the user.
>
> Additionally, 4 merge failures that resulted in an early return have
> been updated to reflect the status of the merge.
> 1. Null merge base (null o): CONFLICT_SUBMODULE_NULL_MERGE_BASE added
>    as a new conflict type and will print updated error message.
> 2. Null merge side a (null a): BUG(). See [1] for discussion
> 3. Null merge side b (null b): BUG(). See [1] for discussion
> 4. Submodule not checked out: Still returns early, but added a
>    NEEDSWORK bit since current error message does not reflect the
>    correct resolution

s/does not reflect the correct resolution/also deserves a more
detailed explanation of how to resolve/ ?

>
> [1] https://lore.kernel.org/git/CABPp-BE0qGwUy80dmVszkJQ+tcpfLRW0OZyErymzhZ9+HWY1mw@mail.gmail.com/
>
> Signed-off-by: Calvin Wan <calvinwan@google.com>
> ---
>
> For Elijah: Cleaned up the small nits and updated resolutions for
> those 4 cases we discussed.

Thanks.  Including a range-diff in the cover letter would be really
helpful, for future reference.

>
> For Ævar: Apologies for misunderstanding your suggestions to make
> my messages easier for translators to work with. I have reformatted
> all of the messages to separate text vs formatting translations. Let
> me know if this is what you were expecting.
>
>  merge-ort.c                 | 112 ++++++++++++++++++++++++++++++++++--
>  t/t6437-submodule-merge.sh  |  78 +++++++++++++++++++++++--
>  t/t7402-submodule-rebase.sh |   9 ++-
>  3 files changed, 185 insertions(+), 14 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 01f150ef3b..4302e900ee 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -387,6 +387,9 @@ struct merge_options_internal {
>
>         /* call_depth: recursion level counter for merging merge bases */
>         int call_depth;
> +
> +       /* field that holds submodule conflict information */
> +       struct string_list conflicted_submodules;
>  };
>
>  struct version_info {
> @@ -517,6 +520,7 @@ enum conflict_and_info_types {
>         CONFLICT_SUBMODULE_NOT_INITIALIZED,
>         CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
>         CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
> +       CONFLICT_SUBMODULE_NULL_MERGE_BASE,
>
>         /* Keep this entry _last_ in the list */
>         NB_CONFLICT_TYPES,
> @@ -570,6 +574,8 @@ static const char *type_short_descriptions[] = {
>                 "CONFLICT (submodule history not available)",
>         [CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
>                 "CONFLICT (submodule may have rewinds)",
> +       [CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
> +               "CONFLICT (submodule no merge base)"
>  };
>
>  struct logical_conflict_info {
> @@ -686,6 +692,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
>
>         mem_pool_discard(&opti->pool, 0);
>
> +       string_list_clear(&opti->conflicted_submodules, 1);
> +
>         /* Clean out callback_data as well. */
>         FREE_AND_NULL(renames->callback_data);
>         renames->callback_data_nr = renames->callback_data_alloc = 0;
> @@ -1744,26 +1752,40 @@ static int merge_submodule(struct merge_options *opt,
>
>         int i;
>         int search = !opt->priv->call_depth;
> +       int sub_initialized = 1;
>
>         /* store fallback answer in result in case we fail */
>         oidcpy(result, opt->priv->call_depth ? o : a);
>
>         /* we can not handle deletion conflicts */
> -       if (is_null_oid(o))
> -               return 0;
>         if (is_null_oid(a))
> -               return 0;
> +               BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
>         if (is_null_oid(b))
> -               return 0;
> +               BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
>
> -       if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
> +       if ((sub_initialized = repo_submodule_init(&subrepo,
> +                                                                       opt->repo, path, null_oid()))) {

Um, repo_submodule_init() returns 0 on success, non-zero upon failure.
So !sub_initialized means "submodule IS initialized", though it
appears to read to mean the opposite.  Can you consider renaming your
variable (maybe just to "sub_not_initialized")?

>                 path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
>                          path, NULL, NULL, NULL,
>                          _("Failed to merge submodule %s (not checked out)"),
>                          path);
> +               /*
> +                * NEEDSWORK: Since the steps to resolve this error are
> +                * more involved than what is currently in
> +                * print_submodule_conflict_suggestion(), we return
> +                * immediately rather than generating an error message
> +                */

Technically, we just did generate an error message ("Failed to merge
submodule %s (not checked out)").

Maybe replace "immediately rather than generating an error message"
with "immediately.  It would be better to "goto cleanup" here, after
setting some flag requesting a more detailed message be saved for
print_submodule_conflict_suggetion()".  Or something like that.

>                 return 0;
>         }
>
> +       if (is_null_oid(o)) {
> +               path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
> +                        path, NULL, NULL, NULL,
> +                        _("Failed to merge submodule %s (no merge base)"),
> +                        path);
> +               goto cleanup;
> +       }

Does this need to be moved after initializing the submodule?  I
thought that was the point of introducing the sub_initialized
variable, and we're clearly not going to use it, so it would seem to
make more sense to not initialize it for this case.

> +
>         if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
>             !(commit_a = lookup_commit_reference(&subrepo, a)) ||
>             !(commit_b = lookup_commit_reference(&subrepo, b))) {
> @@ -1849,7 +1871,15 @@ static int merge_submodule(struct merge_options *opt,
>
>         object_array_clear(&merges);
>  cleanup:
> -       repo_clear(&subrepo);
> +       if (!opt->priv->call_depth && !ret) {
> +               struct string_list *csub = &opt->priv->conflicted_submodules;
> +
> +               string_list_append(csub, path)->util =
> +                               xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
> +       }
> +
> +       if (!sub_initialized)
> +               repo_clear(&subrepo);
>         return ret;
>  }
>
> @@ -4412,6 +4442,73 @@ static int record_conflicted_index_entries(struct merge_options *opt)
>         return errs;
>  }
>
> +static void print_submodule_conflict_suggestion(struct string_list *csub) {
> +       if (csub->nr > 0) {
> +               struct string_list_item *item;
> +               struct strbuf msg = STRBUF_INIT;
> +               struct strbuf tmp = STRBUF_INIT;
> +
> +               strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
> +               strbuf_addf(&msg, "%s\n", tmp.buf);
> +               strbuf_release(&tmp);

Instead of these three lines, why not just
    strbuf_addstr(&msg, "%s\n", _("Recursive merging with submodules
currently only supports trivial cases."));
?

Same applies for some of the messages below too.

> +               strbuf_addf(&tmp, _("Please manually handle the merging of each conflicted submodule."));
> +               strbuf_addf(&msg, "%s\n", tmp.buf);
> +               strbuf_release(&tmp);
> +
> +               strbuf_addf(&tmp, _("This can be accomplished with the following steps:"));
> +               strbuf_addf(&msg, "%s\n", tmp.buf);
> +               strbuf_release(&tmp);
> +
> +               for_each_string_list_item(item, csub) {
> +                       const char *abbrev= item->util;
> +                       /*
> +                        * TRANSLATORS: This is a line of advice to resolve a merge conflict
> +                        * in a submodule. The second argument is the abbreviated id of the
> +                        * commit that needs to be merged.
> +                        * E.g. - go to submodule (sub), and either merge commit abc1234"
> +                        */
> +                       strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s"),
> +                                                                                                       item->string, abbrev);
> +                       strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +                       strbuf_addf(&msg, "\n");
> +                       strbuf_release(&tmp);
> +                       strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));
> +                       strbuf_addf(&msg, _("   %s"), tmp.buf);
> +                       strbuf_addf(&msg, "\n");
> +                       strbuf_release(&tmp);
> +               }
> +               strbuf_addf(&tmp, _("come back to superproject and run:"));
> +               strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +               strbuf_addf(&msg, "\n\n");
> +               strbuf_release(&tmp);
> +
> +               strbuf_addf(&tmp, "git add ");
> +               strbuf_add_separated_string_list(&tmp, " ", csub);
> +               strbuf_addf(&msg, _("       %s"), tmp.buf);
> +               strbuf_addf(&msg, "\n\n");
> +               strbuf_release(&tmp);
> +
> +               strbuf_addf(&tmp, _("to record the above merge or update"));
> +               strbuf_addf(&msg, _("   %s"), tmp.buf);
> +               strbuf_addf(&msg, "\n");
> +               strbuf_release(&tmp);
> +
> +               strbuf_addf(&tmp, _("resolve any other conflicts in the superproject"));
> +               strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +               strbuf_addf(&msg, "\n");
> +               strbuf_release(&tmp);
> +
> +               strbuf_addf(&tmp, _("commit the resulting index in the superproject"));
> +               strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +               strbuf_addf(&msg, "\n");
> +               strbuf_release(&tmp);
> +
> +               printf("%s", msg.buf);
> +               strbuf_release(&msg);

Yuck.  I'm not a translator, so maybe what you are doing is preferred.
But wouldn't translators find it annoying to have to translate " - %s"
and "  %s" in all these places (and wouldn't there need to be a
TRANSLATORS comment before each and every one)?  Also, are we running
the risk of these short strings needing to be translated differently
here than in other places?  For example, what if someone is trying to
align output nicely:

    Name:   John Johnson
    Date:   2022-07-28
    Gender: Male

and uses _("  %s") when printing the name to get the indent right, and
expects other languages to add or remove spaces accordingly?  Or has
other built up messages without hyphens but uses _("  %s") for
whatever reason that may not match what you are doing here?

If there are multiple _("  %s") in the code, that string can only be
translated once.  Not once per codepath, but once total.  I'm afraid
this lego building of messages seems to risk needing
translation-per-codepath when that just isn't possible.

I also find the big block of code somewhat painful to read.  Could we
instead do something like (note, I have both a tmp and tmp2):

    strbuf_add_separated_string_list(&tmp2, " ", csub);

    for_each_string_list_item(item, csub) {
        const char *abbrev= item->util;
        /*
         * TRANSLATORS: This is a line of advice to resolve a merge conflict
         * in a submodule. The second argument is the abbreviated id of the
         * commit that needs to be merged.
         * E.g. - go to submodule (sub), and either merge commit abc1234
         */
        strbuf_addf(&tmp, _(" - go to submodule %s, and either merge
commit %s\n"
                            "   or update to an existing commit which
has merged those changes\n"),
                            item->string, abbrev);
    }

    strbuf_addf(&msg,
                _("Recursive merging with submodules currently only
supports trivial cases.\n"
                  "Please manually handle the merging of each
conflicted submodule.\n"
                  "This can be accomplished with the following steps:\n"
                  "%s"
                  " - come back to superproject and run:\n\n"
                  "      git add %s\n\n"
                  "   to record the above merge or update\n"
                  " - resolve any other conflicts in the superproject\n"
                  " - commit the resulting index in the superproject\n"),
                  tmp.buf, tmp2.buf);

This will give translators precisely two messages to translate (and we
can't drop it to one since one of the two is repeated a variable
number of times), and provide more built-in context about how to
translate since the whole message is involved.  If one of the messages
translates into something especially long, they can even add line
breaks and reflow the paragraph in ways that make sense for them,
which your current version just doesn't permit.


> +       }
> +}
> +
>  void merge_display_update_messages(struct merge_options *opt,
>                                    int detailed,
>                                    struct merge_result *result)
> @@ -4461,6 +4558,8 @@ void merge_display_update_messages(struct merge_options *opt,
>         }
>         string_list_clear(&olist, 0);
>
> +       print_submodule_conflict_suggestion(&opti->conflicted_submodules);
> +
>         /* Also include needed rename limit adjustment now */
>         diff_warn_rename_limit("merge.renamelimit",
>                                opti->renames.needed_limit, 0);
> @@ -4657,6 +4756,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
>         trace2_region_enter("merge", "allocate/init", opt->repo);
>         if (opt->priv) {
>                 clear_or_reinit_internal_opts(opt->priv, 1);
> +               string_list_init_nodup(&opt->priv->conflicted_submodules);
>                 trace2_region_leave("merge", "allocate/init", opt->repo);
>                 return;
>         }
> diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
> index c253bf759a..414597a420 100755
> --- a/t/t6437-submodule-merge.sh
> +++ b/t/t6437-submodule-merge.sh
> @@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
>          echo "file-c" > file-c &&
>          git add file-c &&
>          git commit -m "sub-c") &&
> -       git commit -a -m "c" &&
> +       git commit -a -m "c")
> +'
>
> +test_expect_success 'merging should conflict for non fast-forward' '
> +       test_when_finished "git -C merge-search reset --hard" &&
> +       (cd merge-search &&
> +        git checkout -b test-nonforward-a b &&
> +         if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +         then
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
> +         else
> +               test_must_fail git merge c 2> actual
> +         fi)
> +'
> +
> +test_expect_success 'finish setup for merge-search' '
> +       (cd merge-search &&
>         git checkout -b d a &&
>         (cd sub &&
>          git checkout -b sub-d sub-b &&
> @@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
>          test_cmp expect actual)
>  '
>
> -test_expect_success 'merging should conflict for non fast-forward' '
> +test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
>         (cd merge-search &&
> -        git checkout -b test-nonforward b &&
> +        git checkout -b test-nonforward-b b &&
>          (cd sub &&
>           git rev-parse --short sub-d > ../expect) &&
>           if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>           then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>           else
>                 test_must_fail git merge c 2> actual
>           fi &&
> @@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
>          ) &&
>          if test "$GIT_TEST_MERGE_ALGORITHM" = ort
>          then
> -               test_must_fail git merge c >actual
> +               test_must_fail git merge c >actual &&
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
> +               grep "$sub_expect" actual
>          else
>                 test_must_fail git merge c 2> actual
>          fi &&
> @@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
>         git commit -a -m "f" &&
>
>         git checkout -b test-backward e &&
> -       test_must_fail git merge f)
> +       test_must_fail git merge f >actual &&
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +    then
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
> +               grep "$sub_expect" actual
> +       fi)
>  '
>
>
> @@ -476,4 +502,44 @@ test_expect_failure 'directory/submodule conflict; merge --abort works afterward
>         )
>  '
>
> +# Setup:
> +#   - Submodule has 2 commits: a and b
> +#   - Superproject branch 'a' adds and commits submodule pointing to 'commit a'
> +#   - Superproject branch 'b' adds and commits submodule pointing to 'commit b'
> +# If these two branches are now merged, there is no merge base
> +test_expect_success 'setup for null merge base' '
> +       mkdir no-merge-base &&
> +       (cd no-merge-base &&
> +       git init &&
> +       mkdir sub &&
> +       (cd sub &&
> +        git init &&
> +        echo "file-a" > file-a &&
> +        git add file-a &&
> +        git commit -m "commit a") &&
> +       git commit --allow-empty -m init &&
> +       git branch init &&
> +       git checkout -b a init &&
> +       git add sub &&
> +       git commit -m "a" &&
> +       git switch main &&
> +       (cd sub &&
> +        echo "file-b" > file-b &&
> +        git add file-b &&
> +        git commit -m "commit b"))
> +'
> +
> +test_expect_success 'merging should fail with no merge base' '
> +       (cd no-merge-base &&
> +       git checkout -b b init &&
> +       git add sub &&
> +       git commit -m "b" &&
> +       test_must_fail git merge a >actual &&
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +    then
> +               sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short HEAD^1)" &&
> +               grep "$sub_expect" actual
> +       fi)
> +'
> +
>  test_done
> diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
> index 8e32f19007..ebeca12a71 100755
> --- a/t/t7402-submodule-rebase.sh
> +++ b/t/t7402-submodule-rebase.sh
> @@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
>         test_tick &&
>         git commit -m fourth &&
>
> -       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
> +       test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
>         git ls-files -s submodule >actual &&
>         (
>                 cd submodule &&
> @@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
>                 echo "160000 $(git rev-parse HEAD^^) 2  submodule" &&
>                 echo "160000 $(git rev-parse HEAD) 3    submodule"
>         ) >expect &&
> -       test_cmp expect actual
> +       test_cmp expect actual &&
> +       if test "$GIT_TEST_MERGE_ALGORITHM" = ort
> +    then
> +               sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
> +               grep "$sub_expect" actual_output
> +       fi
>  '
>
>  test_done
>
> base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
> --
> 2.37.1.455.g008518b4e5-goog
>

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
  2022-07-28 23:22             ` Ævar Arnfjörð Bjarmason
  2022-07-29  0:24             ` Elijah Newren
@ 2022-08-01 12:06             ` Ævar Arnfjörð Bjarmason
  2022-08-02  0:50             ` Junio C Hamano
  2022-08-04 19:51             ` [PATCH v8] " Calvin Wan
  4 siblings, 0 replies; 60+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-08-01 12:06 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, gitster, newren, Johannes.Schindelin


On Thu, Jul 28 2022, Calvin Wan wrote:

> +		strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);

Aside from anything else mentioned, please run the "static-analysis" job
in CI (or coccicheck locally) to sanity check patches. The "seen" branch
is currently failing on this & similar API use:
https://github.com/git/git/runs/7587382291?check_suite_focus=true

Per my previous feedback we should change this anyway, but that would
have shown that doing this particular thing is better done with
strbuf_addbuf() (and actually buggy with strbuf_addf(), if we had a
vector where format specifiers could be inserted).

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-07-29  0:24             ` Elijah Newren
@ 2022-08-01 22:24               ` Calvin Wan
  0 siblings, 0 replies; 60+ messages in thread
From: Calvin Wan @ 2022-08-01 22:24 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Git Mailing List, Junio C Hamano, Johannes Schindelin,
	Ævar Arnfjörð

> > 4. Submodule not checked out: Still returns early, but added a
> >    NEEDSWORK bit since current error message does not reflect the
> >    correct resolution
>
> s/does not reflect the correct resolution/also deserves a more
> detailed explanation of how to resolve/ ?

ack

> Thanks.  Including a range-diff in the cover letter would be really
> helpful, for future reference.

will do

> Um, repo_submodule_init() returns 0 on success, non-zero upon failure.
> So !sub_initialized means "submodule IS initialized", though it
> appears to read to mean the opposite.  Can you consider renaming your
> variable (maybe just to "sub_not_initialized")?

ack


> Technically, we just did generate an error message ("Failed to merge
> submodule %s (not checked out)").
>
> Maybe replace "immediately rather than generating an error message"
> with "immediately.  It would be better to "goto cleanup" here, after
> setting some flag requesting a more detailed message be saved for
> print_submodule_conflict_suggetion()".  Or something like that.

Sounds like a better place to put the NEEDSWORK bit.

>
> >                 return 0;
> >         }
> >
> > +       if (is_null_oid(o)) {
> > +               path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
> > +                        path, NULL, NULL, NULL,
> > +                        _("Failed to merge submodule %s (no merge base)"),
> > +                        path);
> > +               goto cleanup;
> > +       }
>
> Does this need to be moved after initializing the submodule?  I
> thought that was the point of introducing the sub_initialized
> variable, and we're clearly not going to use it, so it would seem to
> make more sense to not initialize it for this case.

The submodule needs to be initialized to generate part of the error
message. See the following in cleanup:

repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV)

> Yuck.  I'm not a translator, so maybe what you are doing is preferred.
> But wouldn't translators find it annoying to have to translate " - %s"
> and "  %s" in all these places (and wouldn't there need to be a
> TRANSLATORS comment before each and every one)?

As per Avar's suggestion, I made a helper function so translators
only have to translate " - %s" and "  %s" once. This change does
end up lining up the `git add ...` command, but I think that's fine

- come back to superproject and run:

   git add sub3 sub2 sub

   to record the above merge or update

> I also find the big block of code somewhat painful to read.  Could we
> instead do something like (note, I have both a tmp and tmp2):
>
>     strbuf_add_separated_string_list(&tmp2, " ", csub);
>
>     for_each_string_list_item(item, csub) {
>         const char *abbrev= item->util;
>         /*
>          * TRANSLATORS: This is a line of advice to resolve a merge conflict
>          * in a submodule. The second argument is the abbreviated id of the
>          * commit that needs to be merged.
>          * E.g. - go to submodule (sub), and either merge commit abc1234
>          */
>         strbuf_addf(&tmp, _(" - go to submodule %s, and either merge
> commit %s\n"
>                             "   or update to an existing commit which
> has merged those changes\n"),
>                             item->string, abbrev);
>     }
>
>     strbuf_addf(&msg,
>                 _("Recursive merging with submodules currently only
> supports trivial cases.\n"
>                   "Please manually handle the merging of each
> conflicted submodule.\n"
>                   "This can be accomplished with the following steps:\n"
>                   "%s"
>                   " - come back to superproject and run:\n\n"
>                   "      git add %s\n\n"
>                   "   to record the above merge or update\n"
>                   " - resolve any other conflicts in the superproject\n"
>                   " - commit the resulting index in the superproject\n"),
>                   tmp.buf, tmp2.buf);
>
> This will give translators precisely two messages to translate (and we
> can't drop it to one since one of the two is repeated a variable
> number of times), and provide more built-in context about how to
> translate since the whole message is involved.  If one of the messages
> translates into something especially long, they can even add line
> breaks and reflow the paragraph in ways that make sense for them,
> which your current version just doesn't permit.

I think Avar's suggestion makes translating the formatting easier than
your suggestion, at the cost of having a big block of code to setup it
all up.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
                               ` (2 preceding siblings ...)
  2022-08-01 12:06             ` Ævar Arnfjörð Bjarmason
@ 2022-08-02  0:50             ` Junio C Hamano
  2022-08-02 21:03               ` Calvin Wan
  2022-08-04 19:51             ` [PATCH v8] " Calvin Wan
  4 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-08-02  0:50 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Calvin Wan <calvinwan@google.com> writes:

>  merge-ort.c                 | 112 ++++++++++++++++++++++++++++++++++--
>  t/t6437-submodule-merge.sh  |  78 +++++++++++++++++++++++--
>  t/t7402-submodule-rebase.sh |   9 ++-
>  3 files changed, 185 insertions(+), 14 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 01f150ef3b..4302e900ee 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -387,6 +387,9 @@ struct merge_options_internal {
>  
>  	/* call_depth: recursion level counter for merging merge bases */
>  	int call_depth;
> +
> +	/* field that holds submodule conflict information */
> +	struct string_list conflicted_submodules;
>  };
>  
>  struct version_info {
> @@ -517,6 +520,7 @@ enum conflict_and_info_types {
>  	CONFLICT_SUBMODULE_NOT_INITIALIZED,
>  	CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
>  	CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
> +	CONFLICT_SUBMODULE_NULL_MERGE_BASE,
>  
>  	/* Keep this entry _last_ in the list */
>  	NB_CONFLICT_TYPES,
> @@ -570,6 +574,8 @@ static const char *type_short_descriptions[] = {
>  		"CONFLICT (submodule history not available)",
>  	[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
>  		"CONFLICT (submodule may have rewinds)",

The other ones are semi sentences ...

> +	[CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
> +		"CONFLICT (submodule no merge base)"

... and this wants to become one, too, e.g. "submodule lacks merge
base", perhaps?

>  };

> @@ -686,6 +692,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
>  
>  	mem_pool_discard(&opti->pool, 0);
>  
> +	string_list_clear(&opti->conflicted_submodules, 1);
> +
>  	/* Clean out callback_data as well. */
>  	FREE_AND_NULL(renames->callback_data);
>  	renames->callback_data_nr = renames->callback_data_alloc = 0;
> @@ -1744,26 +1752,40 @@ static int merge_submodule(struct merge_options *opt,
>  
>  	int i;
>  	int search = !opt->priv->call_depth;
> +	int sub_initialized = 1;
>  
>  	/* store fallback answer in result in case we fail */
>  	oidcpy(result, opt->priv->call_depth ? o : a);
>  
>  	/* we can not handle deletion conflicts */
> -	if (is_null_oid(o))
> -		return 0;
>  	if (is_null_oid(a))
> -		return 0;
> +		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()"); 
>  	if (is_null_oid(b))
> -		return 0;
> +		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");

OK.  It is interesting that the first condition (i.e. 'o' is
missing) we are removing is not about "we cannot handle deletion",
so this change corrects the code to match the comment.  As the BUG()
messages are the same on these sides,

	if (is_null_oid(a) || is_null_oid(b))
		BUG(...);

may be easier to read, perhaps?

> -	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
> +	if ((sub_initialized = repo_submodule_init(&subrepo,
> +									opt->repo, path, null_oid()))) {

I wonder where this overly deep indentation come from?  Can the
.editorconfig file we ship with the project help?

>  		path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
>  			 path, NULL, NULL, NULL,
>  			 _("Failed to merge submodule %s (not checked out)"),
>  			 path);
> +		/*
> +		 * NEEDSWORK: Since the steps to resolve this error are
> +		 * more involved than what is currently in 
> +		 * print_submodule_conflict_suggestion(), we return
> +		 * immediately rather than generating an error message
> +		 */

OK.

>  		return 0;
>  	}
>  
> +	if (is_null_oid(o)) {
> +		path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
> +			 path, NULL, NULL, NULL,
> +			 _("Failed to merge submodule %s (no merge base)"),
> +			 path);

OK.

> +		goto cleanup;
> +	}

> @@ -1849,7 +1871,15 @@ static int merge_submodule(struct merge_options *opt,
>  
>  	object_array_clear(&merges);
>  cleanup:
> -	repo_clear(&subrepo);
> +	if (!opt->priv->call_depth && !ret) {
> +		struct string_list *csub = &opt->priv->conflicted_submodules;
> +
> +		string_list_append(csub, path)->util =
> +				xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));

This line could also lose one level of indent.

We record 'b' in its abbreviated form here, because the assumption
is that when merging the superproject, 'a' (which is the assumed
fallback answer wet up at the beginning of the furnction) is the
commit recorded in our side of the superproject, and it is the
commit checked out in the submodule, so the task of making the
necessary merge in the submodule is to go to the submodule and then
merge 'b' into HEAD.  Makes sense.

> @@ -4412,6 +4442,73 @@ static int record_conflicted_index_entries(struct merge_options *opt)
>  	return errs;
>  }
>  
> +static void print_submodule_conflict_suggestion(struct string_list *csub) {
> +	if (csub->nr > 0) {
> +		struct string_list_item *item;
> +		struct strbuf msg = STRBUF_INIT;
> +		struct strbuf tmp = STRBUF_INIT;
> +
> +		strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("Please manually handle the merging of each conflicted submodule."));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("This can be accomplished with the following steps:"));
> +		strbuf_addf(&msg, "%s\n", tmp.buf);
> +		strbuf_release(&tmp);
> +
> +		for_each_string_list_item(item, csub) {
> +			const char *abbrev= item->util;
> +			/*
> +			 * TRANSLATORS: This is a line of advice to resolve a merge conflict
> +			 * in a submodule. The second argument is the abbreviated id of the
> +			 * commit that needs to be merged.
> +			 * E.g. - go to submodule (sub), and either merge commit abc1234"
> +			 */
> +			strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s"),
> +													item->string, abbrev);

Is this also an instance of overly deep indentation (it is so deep
that I cannot easily tell)?

When we say "either merge commit %s" (where %s is 'b'---what they
have as we saw earlier), do we need to mention what the value of 'a'
is to the user, or is it redundant because we are absolutely sure
that 'a' is what is checked out in the submodule?

> +			strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +			strbuf_addf(&msg, "\n");
> +			strbuf_release(&tmp);
> +			strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));

"those changes" means "a..b"?  Again, I just want to make sure that
the user in this situation knows "the other end" of the range when
they are told only about 'b'.

> +			strbuf_addf(&msg, _("   %s"), tmp.buf);
> +			strbuf_addf(&msg, "\n");
> +			strbuf_release(&tmp);
> +		}
> +		strbuf_addf(&tmp, _("come back to superproject and run:"));
> +		strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +		strbuf_addf(&msg, "\n\n");
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, "git add ");
> +		strbuf_add_separated_string_list(&tmp, " ", csub);
> +		strbuf_addf(&msg, _("       %s"), tmp.buf);
> +		strbuf_addf(&msg, "\n\n");
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("to record the above merge or update"));
> +		strbuf_addf(&msg, _("   %s"), tmp.buf);
> +		strbuf_addf(&msg, "\n");
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("resolve any other conflicts in the superproject"));
> +		strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +		strbuf_addf(&msg, "\n");
> +		strbuf_release(&tmp);
> +
> +		strbuf_addf(&tmp, _("commit the resulting index in the superproject"));
> +		strbuf_addf(&msg, _(" - %s"), tmp.buf);
> +		strbuf_addf(&msg, "\n");
> +		strbuf_release(&tmp);
> +
> +		printf("%s", msg.buf);
> +		strbuf_release(&msg);
> +	}
> +}

OK.  Makes sense.

Thanks.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-08-02  0:50             ` Junio C Hamano
@ 2022-08-02 21:03               ` Calvin Wan
  2022-08-02 21:11                 ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-08-02 21:03 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, newren, Johannes.Schindelin, avarab

> > @@ -570,6 +574,8 @@ static const char *type_short_descriptions[] = {
> >               "CONFLICT (submodule history not available)",
> >       [CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
> >               "CONFLICT (submodule may have rewinds)",
>
> The other ones are semi sentences ...
>
> > +     [CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
> > +             "CONFLICT (submodule no merge base)"
>
> ... and this wants to become one, too, e.g. "submodule lacks merge
> base", perhaps?

lacks/missing both SGTM


>         if (is_null_oid(a) || is_null_oid(b))
>                 BUG(...);
>
> may be easier to read, perhaps?

ack

> I wonder where this overly deep indentation come from?  Can the
> .editorconfig file we ship with the project help?

It comes from me not properly indenting the line. I'll take a look at
the .editorconfig file.

> When we say "either merge commit %s" (where %s is 'b'---what they
> have as we saw earlier), do we need to mention what the value of 'a'
> is to the user, or is it redundant because we are absolutely sure
> that 'a' is what is checked out in the submodule?

We are absolutely sure that 'a' is the GITLINK to the submodule, but
if the user changes their local submodule without updating the reference,
it can result in errors like "not checked out" and "commits not present"
during the merge.

> > +                     strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));
>
> "those changes" means "a..b"?  Again, I just want to make sure that
> the user in this situation knows "the other end" of the range when
> they are told only about 'b'.

Correct. Based on what I said above, it might be worthwhile to mention
'a' as well since that information could also help the user in those cases.
I'm thinking something like this:

"go to submodule ('sub' : 'a'), and either merge commit 'b'\n"
"go to submodule ('sub', 'a'), and either merge commit 'b'\n"
"go to submodule 'sub', commit 'a', and either merge commit 'b'\n"

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-08-02 21:03               ` Calvin Wan
@ 2022-08-02 21:11                 ` Junio C Hamano
  2022-08-02 21:55                   ` Calvin Wan
  0 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-08-02 21:11 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Calvin Wan <calvinwan@google.com> writes:

> I'm thinking something like this:
>
> "go to submodule ('sub' : 'a'), and either merge commit 'b'\n"
> "go to submodule ('sub', 'a'), and either merge commit 'b'\n"
> "go to submodule 'sub', commit 'a', and either merge commit 'b'\n"

In the first two, I suspect that it may not be quite clear what 'a'
means to the user.  In the third one, the first "commit" might be
mistaken as a verb.  I am tempted to say

    cd to <sub>, run "checkout --detach <a>" then "merge <b>"

but that may be a bit too prescriptive.  I dunno.




^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-08-02 21:11                 ` Junio C Hamano
@ 2022-08-02 21:55                   ` Calvin Wan
  2022-08-02 22:22                     ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-08-02 21:55 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, newren, Johannes.Schindelin, avarab

I'm starting to think this is getting out of scope for my patch.
For the errors, "not checked out" and "commits not present",
I will have a NEEDSWORK bit attached to them in
print_submodule_conflict(), and if any of the submodules has
those errors, then my message won't print out. That way,
we are guaranteed to have 'a' checked out when my message
prints, rendering it redundant.

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v7] submodule merge: update conflict error message
  2022-08-02 21:55                   ` Calvin Wan
@ 2022-08-02 22:22                     ` Junio C Hamano
  0 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-08-02 22:22 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Calvin Wan <calvinwan@google.com> writes:

> I'm starting to think this is getting out of scope for my patch.
> For the errors, "not checked out" and "commits not present",
> I will have a NEEDSWORK bit attached to them in
> print_submodule_conflict(), and if any of the submodules has
> those errors, then my message won't print out. That way,
> we are guaranteed to have 'a' checked out when my message
> prints, rendering it redundant.

That's fine, then.

Thanks for thinking it through.


^ permalink raw reply	[flat|nested] 60+ messages in thread

* [PATCH v8] submodule merge: update conflict error message
  2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
                               ` (3 preceding siblings ...)
  2022-08-02  0:50             ` Junio C Hamano
@ 2022-08-04 19:51             ` Calvin Wan
  2022-08-16 15:58               ` Junio C Hamano
  4 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-08-04 19:51 UTC (permalink / raw)
  To: git; +Cc: Calvin Wan, gitster, newren, Johannes.Schindelin, avarab

When attempting to merge in a superproject with conflicting submodule
pointers that cannot be fast-forwarded or trivially resolved, the merge
fails and Git prints an error message that accurately describes the
failure, but does not provide steps for the user to resolve the error.

Git is left in a conflicted state, which requires the user to:
 1. merge submodules or update submodules to an already existing
	commit that reflects the merge
 2. add submodules changes to the superproject
 3. finish merging superproject
These steps are non-obvious for newer submodule users to figure out
based on the error message and neither `git submodule status` nor `git
status` provide any useful pointers.

Update error message to provide steps to resolve submodule merge
conflict. Future work could involve adding an advice flag to the
message. Although the message is long, it also has the id of the 
submodule commit that needs to be merged, which could be useful
information for the user.

Additionally, 5 merge failures that resulted in an early return have
been updated to reflect the status of the merge.
1. Null merge base (null o): CONFLICT_SUBMODULE_NULL_MERGE_BASE added
   as a new conflict type and will print updated error message.
2. Null merge side a (null a): BUG(). See [1] for discussion
3. Null merge side b (null b): BUG(). See [1] for discussion
4. Submodule not checked out: added NEEDSWORK bit
5. Submodule commits not present: added NEEDSWORK bit
The errors with a NEEDSWORK bit deserve a more detailed explanation of
how to resolve them. See [2] for more context.

[1] https://lore.kernel.org/git/CABPp-BE0qGwUy80dmVszkJQ+tcpfLRW0OZyErymzhZ9+HWY1mw@mail.gmail.com/
[2] https://lore.kernel.org/git/xmqqpmhjjwo9.fsf@gitster.g/

Signed-off-by: Calvin Wan <calvinwan@google.com>
---
Range-diff against v7:
1:  0640133250 ! 1:  24a4652b25 submodule merge: update conflict error message
    @@ merge-ort.c: struct merge_options_internal {
     +
     +	/* field that holds submodule conflict information */
     +	struct string_list conflicted_submodules;
    ++};
    ++
    ++struct conflicted_submodule_item {
    ++	char *abbrev;
    ++	int flag;
      };
      
    ++static void conflicted_submodule_item_free(void *util, const char *str)
    ++{
    ++	struct conflicted_submodule_item *item = util;
    ++
    ++	free(item->abbrev);
    ++	free(item);
    ++}
    ++
      struct version_info {
    + 	struct object_id oid;
    + 	unsigned short mode;
     @@ merge-ort.c: enum conflict_and_info_types {
      	CONFLICT_SUBMODULE_NOT_INITIALIZED,
      	CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
    @@ merge-ort.c: static const char *type_short_descriptions[] = {
      	[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
      		"CONFLICT (submodule may have rewinds)",
     +	[CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
    -+		"CONFLICT (submodule no merge base)"
    ++		"CONFLICT (submodule lacks merge base)"
      };
      
      struct logical_conflict_info {
    @@ merge-ort.c: static void clear_or_reinit_internal_opts(struct merge_options_inte
      
      	mem_pool_discard(&opti->pool, 0);
      
    -+	string_list_clear(&opti->conflicted_submodules, 1);
    ++	string_list_clear_func(&opti->conflicted_submodules,
    ++					conflicted_submodule_item_free);
     +
      	/* Clean out callback_data as well. */
      	FREE_AND_NULL(renames->callback_data);
    @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
      
      	int i;
      	int search = !opt->priv->call_depth;
    -+	int sub_initialized = 1;
    ++	int sub_not_initialized = 1;
    ++	int sub_flag = -1;
      
      	/* store fallback answer in result in case we fail */
      	oidcpy(result, opt->priv->call_depth ? o : a);
    @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
      	/* we can not handle deletion conflicts */
     -	if (is_null_oid(o))
     -		return 0;
    - 	if (is_null_oid(a))
    +-	if (is_null_oid(a))
     -		return 0;
    -+		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()"); 
    - 	if (is_null_oid(b))
    +-	if (is_null_oid(b))
     -		return 0;
    ++	if (is_null_oid(a) || is_null_oid(b))
     +		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
      
     -	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
    -+	if ((sub_initialized = repo_submodule_init(&subrepo,
    -+									opt->repo, path, null_oid()))) {
    ++	if ((sub_not_initialized = repo_submodule_init(&subrepo,
    ++		opt->repo, path, null_oid()))) {
      		path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
      			 path, NULL, NULL, NULL,
      			 _("Failed to merge submodule %s (not checked out)"),
      			 path);
    -+		/*
    -+		 * NEEDSWORK: Since the steps to resolve this error are
    -+		 * more involved than what is currently in 
    -+		 * print_submodule_conflict_suggestion(), we return
    -+		 * immediately rather than generating an error message
    -+		 */
    - 		return 0;
    - 	}
    - 
    +-		return 0;
    ++		sub_flag = CONFLICT_SUBMODULE_NOT_INITIALIZED;
    ++		goto cleanup;
    ++	}
    ++
     +	if (is_null_oid(o)) {
     +		path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
     +			 path, NULL, NULL, NULL,
     +			 _("Failed to merge submodule %s (no merge base)"),
     +			 path);
     +		goto cleanup;
    -+	}
    -+
    + 	}
    + 
      	if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
    - 	    !(commit_a = lookup_commit_reference(&subrepo, a)) ||
    - 	    !(commit_b = lookup_commit_reference(&subrepo, b))) {
    +@@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
    + 			 path, NULL, NULL, NULL,
    + 			 _("Failed to merge submodule %s (commits not present)"),
    + 			 path);
    ++		sub_flag = CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE;
    + 		goto cleanup;
    + 	}
    + 
     @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
      
      	object_array_clear(&merges);
    @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
     -	repo_clear(&subrepo);
     +	if (!opt->priv->call_depth && !ret) {
     +		struct string_list *csub = &opt->priv->conflicted_submodules;
    ++		struct conflicted_submodule_item *util;
    ++		const char *abbrev;
     +
    -+		string_list_append(csub, path)->util =
    -+				xstrdup(repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV));
    ++		util = xmalloc(sizeof(*util));
    ++		util->flag = sub_flag;
    ++		util->abbrev = NULL;
    ++		if (!sub_not_initialized) {
    ++			abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);
    ++			util->abbrev = xstrdup(abbrev);
    ++		}
    ++		string_list_append(csub, path)->util = util;
     +	}
     +
    -+	if (!sub_initialized)
    ++	if (!sub_not_initialized)
     +		repo_clear(&subrepo);
      	return ret;
      }
    @@ merge-ort.c: static int record_conflicted_index_entries(struct merge_options *op
      	return errs;
      }
      
    -+static void print_submodule_conflict_suggestion(struct string_list *csub) {
    -+	if (csub->nr > 0) {
    -+		struct string_list_item *item;
    -+		struct strbuf msg = STRBUF_INIT;
    -+		struct strbuf tmp = STRBUF_INIT;
    -+
    -+		strbuf_addf(&tmp, _("Recursive merging with submodules currently only supports trivial cases."));
    -+		strbuf_addf(&msg, "%s\n", tmp.buf);
    -+		strbuf_release(&tmp);
    ++static void format_submodule_conflict_suggestion(struct strbuf *msg) {
    ++	struct strbuf tmp = STRBUF_INIT;
    ++	struct string_list msg_list = STRING_LIST_INIT_DUP;
    ++	int i;
     +
    -+		strbuf_addf(&tmp, _("Please manually handle the merging of each conflicted submodule."));
    -+		strbuf_addf(&msg, "%s\n", tmp.buf);
    -+		strbuf_release(&tmp);
    -+
    -+		strbuf_addf(&tmp, _("This can be accomplished with the following steps:"));
    -+		strbuf_addf(&msg, "%s\n", tmp.buf);
    -+		strbuf_release(&tmp);
    -+
    -+		for_each_string_list_item(item, csub) {
    -+			const char *abbrev= item->util;
    ++	string_list_split(&msg_list, msg->buf, '\n', -1);
    ++	for (i = 0; i < msg_list.nr; i++) {
    ++		if (!i)
     +			/*
    -+			 * TRANSLATORS: This is a line of advice to resolve a merge conflict
    -+			 * in a submodule. The second argument is the abbreviated id of the
    -+			 * commit that needs to be merged.
    -+			 * E.g. - go to submodule (sub), and either merge commit abc1234"
    ++			 * TRANSLATORS: This is line item of submodule conflict message
    ++			 * from print_submodule_conflict_suggestion() below. For RTL
    ++			 * languages, the following swap is suggested:
    ++			 *      " - %s\n" -> "%s - \n"
     +			 */
    -+			strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s"),
    -+													item->string, abbrev);
    -+			strbuf_addf(&msg, _(" - %s"), tmp.buf);
    -+			strbuf_addf(&msg, "\n");
    -+			strbuf_release(&tmp);
    -+			strbuf_addf(&tmp, _("or update to an existing commit which has merged those changes"));
    -+			strbuf_addf(&msg, _("   %s"), tmp.buf);
    -+			strbuf_addf(&msg, "\n");
    -+			strbuf_release(&tmp);
    -+		}
    -+		strbuf_addf(&tmp, _("come back to superproject and run:"));
    -+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
    -+		strbuf_addf(&msg, "\n\n");
    -+		strbuf_release(&tmp);
    ++			strbuf_addf(&tmp, _(" - %s\n"), msg_list.items[i].string);
    ++		else
    ++			/*
    ++			 * TRANSLATORS: This is line item of submodule conflict message
    ++			 * from print_submodule_conflict_suggestion() below. For RTL
    ++			 * languages, the following swap is suggested:
    ++			 *      "   %s\n" -> "%s   \n"
    ++			 */
    ++			strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
    ++	}
    ++	strbuf_reset(msg);
    ++	strbuf_add(msg, tmp.buf, tmp.len);
    ++}
     +
    -+		strbuf_addf(&tmp, "git add ");
    -+		strbuf_add_separated_string_list(&tmp, " ", csub);
    -+		strbuf_addf(&msg, _("       %s"), tmp.buf);
    -+		strbuf_addf(&msg, "\n\n");
    -+		strbuf_release(&tmp);
    ++static void print_submodule_conflict_suggestion(struct string_list *csub) {
    ++	struct string_list_item *item;
    ++	struct strbuf msg = STRBUF_INIT;
    ++	struct strbuf tmp = STRBUF_INIT;
    ++	struct strbuf subs = STRBUF_INIT;
     +
    -+		strbuf_addf(&tmp, _("to record the above merge or update"));
    -+		strbuf_addf(&msg, _("   %s"), tmp.buf);
    -+		strbuf_addf(&msg, "\n");
    -+		strbuf_release(&tmp);
    ++	if (!csub->nr)
    ++		return;
    ++	
    ++	/*
    ++	 * NEEDSWORK: The steps to resolve these errors deserve a more
    ++	 * detailed explanation than what is currently printed below.
    ++	 */
    ++	for_each_string_list_item(item, csub) {
    ++		struct conflicted_submodule_item *util = item->util;
     +
    -+		strbuf_addf(&tmp, _("resolve any other conflicts in the superproject"));
    -+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
    -+		strbuf_addf(&msg, "\n");
    -+		strbuf_release(&tmp);
    ++		if (util->flag == CONFLICT_SUBMODULE_NOT_INITIALIZED ||
    ++			util->flag == CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE)
    ++			return;
    ++	}
     +
    -+		strbuf_addf(&tmp, _("commit the resulting index in the superproject"));
    -+		strbuf_addf(&msg, _(" - %s"), tmp.buf);
    -+		strbuf_addf(&msg, "\n");
    -+		strbuf_release(&tmp);
    ++	printf(_("Recursive merging with submodules currently only supports "
    ++		"trivial cases.\nPlease manually handle the merging of each "
    ++		"conflicted submodule.\nThis can be accomplished with the following "
    ++		"steps:"));
    ++	putchar('\n');
     +
    -+		printf("%s", msg.buf);
    -+		strbuf_release(&msg);
    ++	for_each_string_list_item(item, csub) {
    ++		struct conflicted_submodule_item *util = item->util;
    ++		/*
    ++		 * TRANSLATORS: This is a line of advice to resolve a merge conflict
    ++		 * in a submodule. The second argument is the abbreviated id of the
    ++		 * commit that needs to be merged.
    ++		 * E.g. - go to submodule (sub), and either merge commit abc1234"
    ++		 */
    ++		strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s\n"
    ++			"or update to an existing commit which has merged those changes"),
    ++			item->string, util->abbrev);
    ++		format_submodule_conflict_suggestion(&tmp);
    ++		strbuf_add(&msg, tmp.buf, tmp.len);
    ++		strbuf_reset(&tmp);
     +	}
    ++	strbuf_add_separated_string_list(&subs, " ", csub);
    ++	strbuf_addstr(&tmp, _("come back to superproject and run:"));
    ++	strbuf_addf(&tmp, "\n\ngit add %s\n\n", subs.buf);
    ++	strbuf_addstr(&tmp, _("to record the above merge or update"));
    ++	format_submodule_conflict_suggestion(&tmp);
    ++	strbuf_add(&msg, tmp.buf, tmp.len);
    ++	strbuf_reset(&tmp);
    ++
    ++	strbuf_addstr(&tmp, _("resolve any other conflicts in the superproject"));
    ++	format_submodule_conflict_suggestion(&tmp);
    ++	strbuf_add(&msg, tmp.buf, tmp.len);
    ++	strbuf_reset(&tmp);
    ++
    ++	strbuf_addstr(&tmp, _("commit the resulting index in the superproject"));
    ++	format_submodule_conflict_suggestion(&tmp);
    ++	strbuf_add(&msg, tmp.buf, tmp.len);
    ++
    ++	printf("%s", msg.buf);
    ++	strbuf_release(&subs);
    ++	strbuf_release(&tmp);
    ++	strbuf_release(&msg);
     +}
     +
      void merge_display_update_messages(struct merge_options *opt,

 merge-ort.c                 | 161 ++++++++++++++++++++++++++++++++++--
 t/t6437-submodule-merge.sh  |  78 +++++++++++++++--
 t/t7402-submodule-rebase.sh |   9 +-
 3 files changed, 231 insertions(+), 17 deletions(-)

diff --git a/merge-ort.c b/merge-ort.c
index 01f150ef3b..86985d5c61 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -387,8 +387,24 @@ struct merge_options_internal {
 
 	/* call_depth: recursion level counter for merging merge bases */
 	int call_depth;
+
+	/* field that holds submodule conflict information */
+	struct string_list conflicted_submodules;
+};
+
+struct conflicted_submodule_item {
+	char *abbrev;
+	int flag;
 };
 
+static void conflicted_submodule_item_free(void *util, const char *str)
+{
+	struct conflicted_submodule_item *item = util;
+
+	free(item->abbrev);
+	free(item);
+}
+
 struct version_info {
 	struct object_id oid;
 	unsigned short mode;
@@ -517,6 +533,7 @@ enum conflict_and_info_types {
 	CONFLICT_SUBMODULE_NOT_INITIALIZED,
 	CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
 	CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
+	CONFLICT_SUBMODULE_NULL_MERGE_BASE,
 
 	/* Keep this entry _last_ in the list */
 	NB_CONFLICT_TYPES,
@@ -570,6 +587,8 @@ static const char *type_short_descriptions[] = {
 		"CONFLICT (submodule history not available)",
 	[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
 		"CONFLICT (submodule may have rewinds)",
+	[CONFLICT_SUBMODULE_NULL_MERGE_BASE] =
+		"CONFLICT (submodule lacks merge base)"
 };
 
 struct logical_conflict_info {
@@ -686,6 +705,9 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 
 	mem_pool_discard(&opti->pool, 0);
 
+	string_list_clear_func(&opti->conflicted_submodules,
+					conflicted_submodule_item_free);
+
 	/* Clean out callback_data as well. */
 	FREE_AND_NULL(renames->callback_data);
 	renames->callback_data_nr = renames->callback_data_alloc = 0;
@@ -1744,24 +1766,32 @@ static int merge_submodule(struct merge_options *opt,
 
 	int i;
 	int search = !opt->priv->call_depth;
+	int sub_not_initialized = 1;
+	int sub_flag = -1;
 
 	/* store fallback answer in result in case we fail */
 	oidcpy(result, opt->priv->call_depth ? o : a);
 
 	/* we can not handle deletion conflicts */
-	if (is_null_oid(o))
-		return 0;
-	if (is_null_oid(a))
-		return 0;
-	if (is_null_oid(b))
-		return 0;
+	if (is_null_oid(a) || is_null_oid(b))
+		BUG("submodule deleted on one side; this should be handled outside of merge_submodule()");
 
-	if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
+	if ((sub_not_initialized = repo_submodule_init(&subrepo,
+		opt->repo, path, null_oid()))) {
 		path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
 			 path, NULL, NULL, NULL,
 			 _("Failed to merge submodule %s (not checked out)"),
 			 path);
-		return 0;
+		sub_flag = CONFLICT_SUBMODULE_NOT_INITIALIZED;
+		goto cleanup;
+	}
+
+	if (is_null_oid(o)) {
+		path_msg(opt, CONFLICT_SUBMODULE_NULL_MERGE_BASE, 0,
+			 path, NULL, NULL, NULL,
+			 _("Failed to merge submodule %s (no merge base)"),
+			 path);
+		goto cleanup;
 	}
 
 	if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
@@ -1771,6 +1801,7 @@ static int merge_submodule(struct merge_options *opt,
 			 path, NULL, NULL, NULL,
 			 _("Failed to merge submodule %s (commits not present)"),
 			 path);
+		sub_flag = CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE;
 		goto cleanup;
 	}
 
@@ -1849,7 +1880,23 @@ static int merge_submodule(struct merge_options *opt,
 
 	object_array_clear(&merges);
 cleanup:
-	repo_clear(&subrepo);
+	if (!opt->priv->call_depth && !ret) {
+		struct string_list *csub = &opt->priv->conflicted_submodules;
+		struct conflicted_submodule_item *util;
+		const char *abbrev;
+
+		util = xmalloc(sizeof(*util));
+		util->flag = sub_flag;
+		util->abbrev = NULL;
+		if (!sub_not_initialized) {
+			abbrev = repo_find_unique_abbrev(&subrepo, b, DEFAULT_ABBREV);
+			util->abbrev = xstrdup(abbrev);
+		}
+		string_list_append(csub, path)->util = util;
+	}
+
+	if (!sub_not_initialized)
+		repo_clear(&subrepo);
 	return ret;
 }
 
@@ -4412,6 +4459,99 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+static void format_submodule_conflict_suggestion(struct strbuf *msg) {
+	struct strbuf tmp = STRBUF_INIT;
+	struct string_list msg_list = STRING_LIST_INIT_DUP;
+	int i;
+
+	string_list_split(&msg_list, msg->buf, '\n', -1);
+	for (i = 0; i < msg_list.nr; i++) {
+		if (!i)
+			/*
+			 * TRANSLATORS: This is line item of submodule conflict message
+			 * from print_submodule_conflict_suggestion() below. For RTL
+			 * languages, the following swap is suggested:
+			 *      " - %s\n" -> "%s - \n"
+			 */
+			strbuf_addf(&tmp, _(" - %s\n"), msg_list.items[i].string);
+		else
+			/*
+			 * TRANSLATORS: This is line item of submodule conflict message
+			 * from print_submodule_conflict_suggestion() below. For RTL
+			 * languages, the following swap is suggested:
+			 *      "   %s\n" -> "%s   \n"
+			 */
+			strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
+	}
+	strbuf_reset(msg);
+	strbuf_add(msg, tmp.buf, tmp.len);
+}
+
+static void print_submodule_conflict_suggestion(struct string_list *csub) {
+	struct string_list_item *item;
+	struct strbuf msg = STRBUF_INIT;
+	struct strbuf tmp = STRBUF_INIT;
+	struct strbuf subs = STRBUF_INIT;
+
+	if (!csub->nr)
+		return;
+	
+	/*
+	 * NEEDSWORK: The steps to resolve these errors deserve a more
+	 * detailed explanation than what is currently printed below.
+	 */
+	for_each_string_list_item(item, csub) {
+		struct conflicted_submodule_item *util = item->util;
+
+		if (util->flag == CONFLICT_SUBMODULE_NOT_INITIALIZED ||
+			util->flag == CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE)
+			return;
+	}
+
+	printf(_("Recursive merging with submodules currently only supports "
+		"trivial cases.\nPlease manually handle the merging of each "
+		"conflicted submodule.\nThis can be accomplished with the following "
+		"steps:"));
+	putchar('\n');
+
+	for_each_string_list_item(item, csub) {
+		struct conflicted_submodule_item *util = item->util;
+		/*
+		 * TRANSLATORS: This is a line of advice to resolve a merge conflict
+		 * in a submodule. The second argument is the abbreviated id of the
+		 * commit that needs to be merged.
+		 * E.g. - go to submodule (sub), and either merge commit abc1234"
+		 */
+		strbuf_addf(&tmp, _("go to submodule (%s), and either merge commit %s\n"
+			"or update to an existing commit which has merged those changes"),
+			item->string, util->abbrev);
+		format_submodule_conflict_suggestion(&tmp);
+		strbuf_add(&msg, tmp.buf, tmp.len);
+		strbuf_reset(&tmp);
+	}
+	strbuf_add_separated_string_list(&subs, " ", csub);
+	strbuf_addstr(&tmp, _("come back to superproject and run:"));
+	strbuf_addf(&tmp, "\n\ngit add %s\n\n", subs.buf);
+	strbuf_addstr(&tmp, _("to record the above merge or update"));
+	format_submodule_conflict_suggestion(&tmp);
+	strbuf_add(&msg, tmp.buf, tmp.len);
+	strbuf_reset(&tmp);
+
+	strbuf_addstr(&tmp, _("resolve any other conflicts in the superproject"));
+	format_submodule_conflict_suggestion(&tmp);
+	strbuf_add(&msg, tmp.buf, tmp.len);
+	strbuf_reset(&tmp);
+
+	strbuf_addstr(&tmp, _("commit the resulting index in the superproject"));
+	format_submodule_conflict_suggestion(&tmp);
+	strbuf_add(&msg, tmp.buf, tmp.len);
+
+	printf("%s", msg.buf);
+	strbuf_release(&subs);
+	strbuf_release(&tmp);
+	strbuf_release(&msg);
+}
+
 void merge_display_update_messages(struct merge_options *opt,
 				   int detailed,
 				   struct merge_result *result)
@@ -4461,6 +4601,8 @@ void merge_display_update_messages(struct merge_options *opt,
 	}
 	string_list_clear(&olist, 0);
 
+	print_submodule_conflict_suggestion(&opti->conflicted_submodules);
+
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
 			       opti->renames.needed_limit, 0);
@@ -4657,6 +4799,7 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	trace2_region_enter("merge", "allocate/init", opt->repo);
 	if (opt->priv) {
 		clear_or_reinit_internal_opts(opt->priv, 1);
+		string_list_init_nodup(&opt->priv->conflicted_submodules);
 		trace2_region_leave("merge", "allocate/init", opt->repo);
 		return;
 	}
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index c253bf759a..414597a420 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -103,8 +103,25 @@ test_expect_success 'setup for merge search' '
 	 echo "file-c" > file-c &&
 	 git add file-c &&
 	 git commit -m "sub-c") &&
-	git commit -a -m "c" &&
+	git commit -a -m "c")
+'
 
+test_expect_success 'merging should conflict for non fast-forward' '
+	test_when_finished "git -C merge-search reset --hard" &&
+	(cd merge-search &&
+	 git checkout -b test-nonforward-a b &&
+	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+	  then
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
+	  else
+		test_must_fail git merge c 2> actual
+	  fi)
+'
+
+test_expect_success 'finish setup for merge-search' '
+	(cd merge-search &&
 	git checkout -b d a &&
 	(cd sub &&
 	 git checkout -b sub-d sub-b &&
@@ -129,14 +146,16 @@ test_expect_success 'merge with one side as a fast-forward of the other' '
 	 test_cmp expect actual)
 '
 
-test_expect_success 'merging should conflict for non fast-forward' '
+test_expect_success 'merging should conflict for non fast-forward (resolution exists)' '
 	(cd merge-search &&
-	 git checkout -b test-nonforward b &&
+	 git checkout -b test-nonforward-b b &&
 	 (cd sub &&
 	  git rev-parse --short sub-d > ../expect) &&
 	  if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	  then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+	 	grep "$sub_expect" actual
 	  else
 		test_must_fail git merge c 2> actual
 	  fi &&
@@ -161,7 +180,9 @@ test_expect_success 'merging should fail for ambiguous common parent' '
 	 ) &&
 	 if test "$GIT_TEST_MERGE_ALGORITHM" = ort
 	 then
-		test_must_fail git merge c >actual
+		test_must_fail git merge c >actual &&
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
+		grep "$sub_expect" actual
 	 else
 		test_must_fail git merge c 2> actual
 	 fi &&
@@ -205,7 +226,12 @@ test_expect_success 'merging should fail for changes that are backwards' '
 	git commit -a -m "f" &&
 
 	git checkout -b test-backward e &&
-	test_must_fail git merge f)
+	test_must_fail git merge f >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
+		grep "$sub_expect" actual
+	fi)
 '
 
 
@@ -476,4 +502,44 @@ test_expect_failure 'directory/submodule conflict; merge --abort works afterward
 	)
 '
 
+# Setup:
+#   - Submodule has 2 commits: a and b
+#   - Superproject branch 'a' adds and commits submodule pointing to 'commit a'
+#   - Superproject branch 'b' adds and commits submodule pointing to 'commit b'
+# If these two branches are now merged, there is no merge base
+test_expect_success 'setup for null merge base' '
+	mkdir no-merge-base &&
+	(cd no-merge-base &&
+	git init &&
+	mkdir sub &&
+	(cd sub &&
+	 git init &&
+	 echo "file-a" > file-a &&
+	 git add file-a &&
+	 git commit -m "commit a") &&
+	git commit --allow-empty -m init &&
+	git branch init &&
+	git checkout -b a init &&
+	git add sub &&
+	git commit -m "a" &&
+	git switch main &&
+	(cd sub &&
+	 echo "file-b" > file-b &&
+	 git add file-b &&
+	 git commit -m "commit b"))
+'
+
+test_expect_success 'merging should fail with no merge base' '
+	(cd no-merge-base &&
+	git checkout -b b init &&
+	git add sub &&
+	git commit -m "b" &&
+	test_must_fail git merge a >actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short HEAD^1)" &&
+		grep "$sub_expect" actual
+	fi)
+'
+
 test_done
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 8e32f19007..ebeca12a71 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -104,7 +104,7 @@ test_expect_success 'rebasing submodule that should conflict' '
 	test_tick &&
 	git commit -m fourth &&
 
-	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 &&
+	test_must_fail git rebase --onto HEAD^^ HEAD^ HEAD^0 >actual_output &&
 	git ls-files -s submodule >actual &&
 	(
 		cd submodule &&
@@ -112,7 +112,12 @@ test_expect_success 'rebasing submodule that should conflict' '
 		echo "160000 $(git rev-parse HEAD^^) 2	submodule" &&
 		echo "160000 $(git rev-parse HEAD) 3	submodule"
 	) >expect &&
-	test_cmp expect actual
+	test_cmp expect actual &&
+	if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+    then
+		sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
+		grep "$sub_expect" actual_output
+	fi
 '
 
 test_done

base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a
-- 
2.37.1.559.g78731f0fdb-goog


^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v8] submodule merge: update conflict error message
  2022-08-04 19:51             ` [PATCH v8] " Calvin Wan
@ 2022-08-16 15:58               ` Junio C Hamano
  2022-08-16 18:58                 ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-08-16 15:58 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Calvin Wan <calvinwan@google.com> writes:

> @@ -4412,6 +4459,99 @@ static int record_conflicted_index_entries(struct merge_options *opt)
>  	return errs;
>  }
>  
> +static void format_submodule_conflict_suggestion(struct strbuf *msg) {
> +	struct strbuf tmp = STRBUF_INIT;
> +	struct string_list msg_list = STRING_LIST_INIT_DUP;
> +	int i;
> +
> +	string_list_split(&msg_list, msg->buf, '\n', -1);
> +	for (i = 0; i < msg_list.nr; i++) {
> +		if (!i)
> +			/*
> +			 * TRANSLATORS: This is line item of submodule conflict message
> +			 * from print_submodule_conflict_suggestion() below. For RTL
> +			 * languages, the following swap is suggested:
> +			 *      " - %s\n" -> "%s - \n"
> +			 */
> +			strbuf_addf(&tmp, _(" - %s\n"), msg_list.items[i].string);
> +		else
> +			/*
> +			 * TRANSLATORS: This is line item of submodule conflict message
> +			 * from print_submodule_conflict_suggestion() below. For RTL
> +			 * languages, the following swap is suggested:
> +			 *      "   %s\n" -> "%s   \n"
> +			 */
> +			strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
> +	}
> +	strbuf_reset(msg);
> +	strbuf_add(msg, tmp.buf, tmp.len);
> +}

Here, tmp is not released, and mst_list holds the words split out of msg.

 merge-ort.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git i/merge-ort.c w/merge-ort.c
index 4cb92bdff8..cdb53770be 100644
--- i/merge-ort.c
+++ w/merge-ort.c
@@ -4507,6 +4507,8 @@ static void format_submodule_conflict_suggestion(struct strbuf *msg) {
 	}
 	strbuf_reset(msg);
 	strbuf_add(msg, tmp.buf, tmp.len);
+	string_list_clear(&msg_list, 0);
+	strbuf_release(&tmp);
 }
 
 static void print_submodule_conflict_suggestion(struct string_list *csub) {

^ permalink raw reply related	[flat|nested] 60+ messages in thread

* Re: [PATCH v8] submodule merge: update conflict error message
  2022-08-16 15:58               ` Junio C Hamano
@ 2022-08-16 18:58                 ` Junio C Hamano
  2022-08-16 19:34                   ` Calvin Wan
  0 siblings, 1 reply; 60+ messages in thread
From: Junio C Hamano @ 2022-08-16 18:58 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Junio C Hamano <gitster@pobox.com> writes:

>> +			 * languages, the following swap is suggested:
>> +			 *      "   %s\n" -> "%s   \n"
>> +			 */
>> +			strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
>> +	}
>> +	strbuf_reset(msg);
>> +	strbuf_add(msg, tmp.buf, tmp.len);
>> +}
>
> Here, tmp is not released, and mst_list holds the words split out of msg.

FWIW, with this fixed, the tip of 'seen' passes the linux-leaks CI
job.

>  merge-ort.c | 2 ++
>  1 file changed, 2 insertions(+)
>
> diff --git i/merge-ort.c w/merge-ort.c
> index 4cb92bdff8..cdb53770be 100644
> --- i/merge-ort.c
> +++ w/merge-ort.c
> @@ -4507,6 +4507,8 @@ static void format_submodule_conflict_suggestion(struct strbuf *msg) {
>  	}
>  	strbuf_reset(msg);
>  	strbuf_add(msg, tmp.buf, tmp.len);
> +	string_list_clear(&msg_list, 0);
> +	strbuf_release(&tmp);
>  }
>  
>  static void print_submodule_conflict_suggestion(struct string_list *csub) {

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v8] submodule merge: update conflict error message
  2022-08-16 18:58                 ` Junio C Hamano
@ 2022-08-16 19:34                   ` Calvin Wan
  2022-08-16 19:39                     ` Junio C Hamano
  0 siblings, 1 reply; 60+ messages in thread
From: Calvin Wan @ 2022-08-16 19:34 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, newren, Johannes.Schindelin, avarab

Should I re-roll with just that change?

On Tue, Aug 16, 2022 at 11:58 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Junio C Hamano <gitster@pobox.com> writes:
>
> >> +                     * languages, the following swap is suggested:
> >> +                     *      "   %s\n" -> "%s   \n"
> >> +                     */
> >> +                    strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
> >> +    }
> >> +    strbuf_reset(msg);
> >> +    strbuf_add(msg, tmp.buf, tmp.len);
> >> +}
> >
> > Here, tmp is not released, and mst_list holds the words split out of msg.
>
> FWIW, with this fixed, the tip of 'seen' passes the linux-leaks CI
> job.
>
> >  merge-ort.c | 2 ++
> >  1 file changed, 2 insertions(+)
> >
> > diff --git i/merge-ort.c w/merge-ort.c
> > index 4cb92bdff8..cdb53770be 100644
> > --- i/merge-ort.c
> > +++ w/merge-ort.c
> > @@ -4507,6 +4507,8 @@ static void format_submodule_conflict_suggestion(struct strbuf *msg) {
> >       }
> >       strbuf_reset(msg);
> >       strbuf_add(msg, tmp.buf, tmp.len);
> > +     string_list_clear(&msg_list, 0);
> > +     strbuf_release(&tmp);
> >  }
> >
> >  static void print_submodule_conflict_suggestion(struct string_list *csub) {

^ permalink raw reply	[flat|nested] 60+ messages in thread

* Re: [PATCH v8] submodule merge: update conflict error message
  2022-08-16 19:34                   ` Calvin Wan
@ 2022-08-16 19:39                     ` Junio C Hamano
  0 siblings, 0 replies; 60+ messages in thread
From: Junio C Hamano @ 2022-08-16 19:39 UTC (permalink / raw)
  To: Calvin Wan; +Cc: git, newren, Johannes.Schindelin, avarab

Calvin Wan <calvinwan@google.com> writes:

> On Tue, Aug 16, 2022 at 11:58 AM Junio C Hamano <gitster@pobox.com> wrote:
>>
>> Junio C Hamano <gitster@pobox.com> writes:
>>
>> >> +                     * languages, the following swap is suggested:
>> >> +                     *      "   %s\n" -> "%s   \n"
>> >> +                     */
>> >> +                    strbuf_addf(&tmp, _("   %s\n"), msg_list.items[i].string);
>> >> +    }
>> >> +    strbuf_reset(msg);
>> >> +    strbuf_add(msg, tmp.buf, tmp.len);
>> >> +}
>> >
>> > Here, tmp is not released, and mst_list holds the words split out of msg.
>>
>> FWIW, with this fixed, the tip of 'seen' passes the linux-leaks CI
>> job.
>>
>> >  merge-ort.c | 2 ++
>> >  1 file changed, 2 insertions(+)
>> ...
>
> Should I re-roll with just that change?

I think the topic is already in 'next', so no, it is too late for
that.

I queued the attached on top but haven't merged to 'next' yet.
Making sure that there is no silly mistakes (like "oh, no, that
temporary variable is still alive and it shouldn't be released there
just yet") by lending an extra pair of eyes is what is the most
useful at this point ;-)

--- >8 ---
Subject: [PATCH] merge-ort: plug leaks in "submodule conflict suggestion" code

The helper function uses two temporary variables that it forgets to
release after it is done with them.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 merge-ort.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/merge-ort.c b/merge-ort.c
index a52faf6e21..f33df3ff65 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -4507,6 +4507,8 @@ static void format_submodule_conflict_suggestion(struct strbuf *msg) {
 	}
 	strbuf_reset(msg);
 	strbuf_add(msg, tmp.buf, tmp.len);
+	string_list_clear(&msg_list, 0);
+	strbuf_release(&tmp);
 }
 
 static void print_submodule_conflict_suggestion(struct string_list *csub) {
-- 
2.37.2-492-g20f88697f3



^ permalink raw reply related	[flat|nested] 60+ messages in thread

end of thread, other threads:[~2022-08-16 19:41 UTC | newest]

Thread overview: 60+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-06-06 23:54 [PATCH] submodule merge: update conflict error message Calvin Wan
2022-06-07  0:48 ` Junio C Hamano
2022-06-08 17:19   ` Calvin Wan
2022-06-08 17:34     ` Glen Choo
2022-06-08 18:01       ` Calvin Wan
2022-06-08 19:13         ` Junio C Hamano
2022-06-10 23:11 ` [PATCH v2] " Calvin Wan
2022-06-11  4:53   ` Elijah Newren
2022-06-11 17:08     ` Philippe Blain
2022-06-12  6:56       ` Elijah Newren
2022-06-13  2:03       ` Calvin Wan
2022-06-12 23:30     ` Junio C Hamano
2022-06-13  1:54     ` Calvin Wan
2022-06-29 22:40   ` [PATCH v3] " Calvin Wan
2022-06-30  2:40     ` Elijah Newren
2022-06-30 19:48       ` Calvin Wan
2022-07-01  4:27         ` Elijah Newren
2022-06-30 20:35     ` Glen Choo
2022-06-30 20:45       ` Glen Choo
2022-06-30 21:08       ` Calvin Wan
2022-07-12 23:19     ` [PATCH v4] " Calvin Wan
2022-07-13 18:11       ` Junio C Hamano
2022-07-17  2:46         ` Elijah Newren
2022-07-15 12:57       ` Johannes Schindelin
2022-07-16  6:22         ` Junio C Hamano
2022-07-17  2:44         ` Elijah Newren
2022-07-18 17:03         ` Calvin Wan
2022-07-18 21:43       ` [PATCH v5] " Calvin Wan
2022-07-19  6:39         ` Junio C Hamano
2022-07-19 19:30           ` Calvin Wan
2022-07-19 20:16             ` Junio C Hamano
2022-07-19  7:13         ` Junio C Hamano
2022-07-19 19:07           ` Calvin Wan
2022-07-19 20:30             ` Junio C Hamano
2022-07-25  6:05         ` Ævar Arnfjörð Bjarmason
2022-07-25 12:11           ` Ævar Arnfjörð Bjarmason
2022-07-25 22:03             ` Calvin Wan
2022-07-25 12:31         ` Ævar Arnfjörð Bjarmason
2022-07-25 21:27           ` Calvin Wan
2022-07-26 21:00         ` [PATCH v6] " Calvin Wan
2022-07-27  1:13           ` Elijah Newren
2022-07-27 22:00             ` Calvin Wan
2022-07-28  0:41               ` Elijah Newren
2022-07-28 19:06                 ` Calvin Wan
2022-07-27  9:20           ` Ævar Arnfjörð Bjarmason
2022-07-28 21:12           ` [PATCH v7] " Calvin Wan
2022-07-28 23:22             ` Ævar Arnfjörð Bjarmason
2022-07-29  0:24             ` Elijah Newren
2022-08-01 22:24               ` Calvin Wan
2022-08-01 12:06             ` Ævar Arnfjörð Bjarmason
2022-08-02  0:50             ` Junio C Hamano
2022-08-02 21:03               ` Calvin Wan
2022-08-02 21:11                 ` Junio C Hamano
2022-08-02 21:55                   ` Calvin Wan
2022-08-02 22:22                     ` Junio C Hamano
2022-08-04 19:51             ` [PATCH v8] " Calvin Wan
2022-08-16 15:58               ` Junio C Hamano
2022-08-16 18:58                 ` Junio C Hamano
2022-08-16 19:34                   ` Calvin Wan
2022-08-16 19:39                     ` Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).