]> git.ipfire.org Git - thirdparty/git.git/commitdiff
merge: allow to pretend a merge is made into a different branch
authorJunio C Hamano <gitster@pobox.com>
Mon, 20 Dec 2021 22:53:43 +0000 (14:53 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 20 Dec 2021 22:55:02 +0000 (14:55 -0800)
When a series of patches for a topic-B depends on having topic-A,
the workflow to prepare the topic-B branch would look like this:

    $ git checkout -b topic-B main
    $ git merge --no-ff --no-edit topic-A
    $ git am <mbox-for-topic-B

When topic-A gets updated, recreating the first merge and rebasing
the rest of the topic-B, all on detached HEAD, is a useful
technique.  After updating topic-A with its new round of patches:

    $ git checkout topic-B
    $ prev=$(git rev-parse 'HEAD^{/^Merge branch .topic-A. into}')
    $ git checkout --detach $prev^1
    $ git merge --no-ff --no-edit topic-A
    $ git rebase --onto HEAD $prev @{-1}^0
    $ git checkout -B @{-1}

This will

 (0) check out the current topic-B.
 (1) find the previous merge of topic-A into topic-B.
 (2) detach the HEAD to the parent of the previous merge.
 (3) merge the updated topic-A to it.
 (4) reapply the patches to rebuild the rest of topic-B.
 (5) update topic-B with the result.

without contaminating the reflog of topic-B too much.  topic-B@{1}
is the "logically previous" state before topic-A got updated, for
example.  At (4), comparison (e.g. range-diff) between HEAD and
@{-1} is a meaningful way to sanity check the result, and the same
can be done at (5) by comparing topic-B and topic-B@{1}.

But there is one glitch.  The merge into the detached HEAD done in
the step (3) above gives us "Merge branch 'topic-A' into HEAD", and
does not say "into topic-B".

Teach the "--into-name=<branch>" option to "git merge" and its
underlying "git fmt-merge-message", to pretend as if we were merging
into <branch>, no matter what branch we are actually merging into,
when they prepare the merge message.  The pretend name honors the
usual "into <target>" suppression mechanism, which can be seen in
the tests added here.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-fmt-merge-msg.txt
Documentation/git-merge.txt
builtin/fmt-merge-msg.c
builtin/merge.c
fmt-merge-msg.c
fmt-merge-msg.h
t/t6200-fmt-merge-msg.sh

index 6793d8fc05218f61ebc399b9ac910f27832bc58b..6f28812f38defec0047f9c47ca554231a6adfd8e 100644 (file)
@@ -9,7 +9,7 @@ git-fmt-merge-msg - Produce a merge commit message
 SYNOPSIS
 --------
 [verse]
-'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log]
+'git fmt-merge-msg' [-m <message>] [--into-name <branch>] [--log[=<n>] | --no-log]
 'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] -F <file>
 
 DESCRIPTION
@@ -44,6 +44,10 @@ OPTIONS
        Use <message> instead of the branch names for the first line
        of the log message.  For use with `--log`.
 
+--into-name <branch>::
+       Prepare the merge message as if merging to the branch `<branch>`,
+       instead of the name of the real branch to which the merge is made.
+
 -F <file>::
 --file <file>::
        Take the list of merged objects from <file> instead of
index e4f3352eb584539b75235fc304431646df9415f4..ed0990621f65d19e0d5dc338aae824989f466048 100644 (file)
@@ -12,7 +12,8 @@ SYNOPSIS
 'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
        [--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
        [--[no-]allow-unrelated-histories]
-       [--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]
+       [--[no-]rerere-autoupdate] [-m <msg>] [-F <file>]
+       [--into-name <branch>] [<commit>...]
 'git merge' (--continue | --abort | --quit)
 
 DESCRIPTION
@@ -76,6 +77,11 @@ The 'git fmt-merge-msg' command can be
 used to give a good default for automated 'git merge'
 invocations. The automated message can include the branch description.
 
+--into-name <branch>::
+       Prepare the default merge message as if merging to the branch
+       `<branch>`, instead of the name of the real branch to which
+       the merge is made.
+
 -F <file>::
 --file=<file>::
        Read the commit message to be used for the merge commit (in
index 48a8699de728a950053b182fae23e73b14fba6a6..8d8fd393f8925c93155091db4f0959b7f03c31bb 100644 (file)
@@ -12,6 +12,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)
 {
        const char *inpath = NULL;
        const char *message = NULL;
+       char *into_name = NULL;
        int shortlog_len = -1;
        struct option options[] = {
                { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"),
@@ -23,6 +24,8 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)
                  DEFAULT_MERGE_LOG_LEN },
                OPT_STRING('m', "message", &message, N_("text"),
                        N_("use <text> as start of message")),
+               OPT_STRING(0, "into-name", &into_name, N_("name"),
+                          N_("use <name> instead of the real target branch")),
                OPT_FILENAME('F', "file", &inpath, N_("file to read from")),
                OPT_END()
        };
@@ -56,6 +59,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)
        opts.add_title = !message;
        opts.credit_people = 1;
        opts.shortlog_len = shortlog_len;
+       opts.into_name = into_name;
 
        ret = fmt_merge_msg(&input, &output, &opts);
        if (ret)
index ea3112e0c0b3acc9f4439430fb19e596a9fc9093..1ba5951d49816feec29a1df765f4495b5948ae51 100644 (file)
@@ -87,6 +87,7 @@ static int signoff;
 static const char *sign_commit;
 static int autostash;
 static int no_verify;
+static char *into_name;
 
 static struct strategy all_strategy[] = {
        { "recursive",  NO_TRIVIAL },
@@ -286,6 +287,8 @@ static struct option builtin_merge_options[] = {
        { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"),
                N_("read message from file"), PARSE_OPT_NONEG,
                NULL, 0, option_read_message },
+       OPT_STRING(0, "into-name", &into_name, N_("name"),
+                  N_("use <name> instead of the real target")),
        OPT__VERBOSITY(&verbosity),
        OPT_BOOL(0, "abort", &abort_current_merge,
                N_("abort the current in-progress merge")),
@@ -1122,6 +1125,7 @@ static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *mer
        opts.add_title = !have_message;
        opts.shortlog_len = shortlog_len;
        opts.credit_people = (0 < option_edit);
+       opts.into_name = into_name;
 
        fmt_merge_msg(merge_names, merge_msg, &opts);
        if (merge_msg->len)
index 5216191488e20115949418aab527988ba7b81e7c..d25594545ce1ec1efe2ec52dcd1b190b3d4c1690 100644 (file)
@@ -649,12 +649,15 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
 
        memset(&merge_parents, 0, sizeof(merge_parents));
 
-       /* get current branch */
+       /* learn the commit that we merge into and the current branch name */
        current_branch = current_branch_to_free =
                resolve_refdup("HEAD", RESOLVE_REF_READING, &head_oid, NULL);
        if (!current_branch)
                die("No current branch");
-       if (starts_with(current_branch, "refs/heads/"))
+
+       if (opts->into_name)
+               current_branch = opts->into_name;
+       else if (starts_with(current_branch, "refs/heads/"))
                current_branch += 11;
 
        find_merge_parents(&merge_parents, in, &head_oid);
index f2ab0e0085ada6509c02427503b1ea04b18951e0..99054042dc5e574b2ef1c00394331955239e0324 100644 (file)
@@ -9,6 +9,7 @@ struct fmt_merge_msg_opts {
        unsigned add_title:1,
                credit_people:1;
        int shortlog_len;
+       const char *into_name;
 };
 
 extern int merge_log_config;
index 06c5fb56157f28359b47c04818e2a51f9098ee36..d861d7ca282f1f5c8fd4d59aa78005d902a812b5 100755 (executable)
@@ -573,7 +573,35 @@ test_expect_success 'merge-msg with "merging" an annotated tag' '
        test_cmp expected .git/MERGE_MSG
 '
 
+test_expect_success 'merge --into-name=<name>' '
+       test_when_finished "git checkout main" &&
+       git checkout -B side main &&
+       git commit --allow-empty -m "One step ahead" &&
+
+       git checkout --detach main &&
+       git merge --no-ff side &&
+       git show -s --format="%s" >full.0 &&
+       head -n1 full.0 >actual &&
+       # expect that HEAD is shown as-is
+       grep -e "Merge branch .side. into HEAD$" actual &&
+
+       git reset --hard main &&
+       git merge --no-ff --into-name=main side &&
+       git show -s --format="%s" >full.1 &&
+       head -n1 full.1 >actual &&
+       # expect that we pretend to be merging to main, that is suppressed
+       grep -e "Merge branch .side.$" actual &&
+
+       git checkout -b throwaway main &&
+       git merge --no-ff --into-name=main side &&
+       git show -s --format="%s" >full.2 &&
+       head -n1 full.2 >actual &&
+       # expect that we pretend to be merging to main, that is suppressed
+       grep -e "Merge branch .side.$" actual
+'
+
 test_expect_success 'merge.suppressDest configuration' '
+       test_when_finished "git checkout main" &&
        git checkout -B side main &&
        git commit --allow-empty -m "One step ahead" &&
        git checkout main &&
@@ -590,7 +618,19 @@ test_expect_success 'merge.suppressDest configuration' '
        git -c merge.suppressDest="ma?*[rn]" fmt-merge-msg <.git/FETCH_HEAD >full.3 &&
        head -n1 full.3 >actual &&
        grep -e "Merge branch .side." actual &&
-       ! grep -e " into main$" actual
+       ! grep -e " into main$" actual &&
+
+       git checkout --detach HEAD &&
+       git -c merge.suppressDest="main" fmt-merge-msg <.git/FETCH_HEAD >full.4 &&
+       head -n1 full.4 >actual &&
+       grep -e "Merge branch .side. into HEAD$" actual &&
+
+       git -c merge.suppressDest="main" fmt-merge-msg \
+               --into-name=main <.git/FETCH_HEAD >full.5 &&
+       head -n1 full.5 >actual &&
+       grep -e "Merge branch .side." actual &&
+       ! grep -e " into main$" actual &&
+       ! grep -e " into HEAD$" actual
 '
 
 test_done