diff mbox series

[v6] submodule merge: update conflict error message

Message ID 20220726210020.3397249-1-calvinwan@google.com (mailing list archive)
State Superseded
Headers show
Series [v6] submodule merge: update conflict error message | expand

Commit Message

Calvin Wan July 26, 2022, 9 p.m. UTC
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(-)


base-commit: 9dd64cb4d310986dd7b8ca7fff92f9b61e0bd21a

Comments

Elijah Newren July 27, 2022, 1:13 a.m. UTC | #1
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
Ævar Arnfjörð Bjarmason July 27, 2022, 9:20 a.m. UTC | #2
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.
Calvin Wan July 27, 2022, 10 p.m. UTC | #3
> 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 :)
Elijah Newren July 28, 2022, 12:41 a.m. UTC | #4
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?
Calvin Wan July 28, 2022, 7:06 p.m. UTC | #5
> 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.
diff mbox series

Patch

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