diff mbox series

[v3] submodule merge: update conflict error message

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

Commit Message

Calvin Wan June 29, 2022, 10:40 p.m. UTC
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(-)


base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085

Comments

Elijah Newren June 30, 2022, 2:40 a.m. UTC | #1
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.
Calvin Wan June 30, 2022, 7:48 p.m. UTC | #2
> > 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!
Glen Choo June 30, 2022, 8:35 p.m. UTC | #3
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?
Glen Choo June 30, 2022, 8:45 p.m. UTC | #4
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.
Calvin Wan June 30, 2022, 9:08 p.m. UTC | #5
> 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?
Elijah Newren July 1, 2022, 4:27 a.m. UTC | #6
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!
diff mbox series

Patch

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