{"thread":{"id":"57114","subject":"[PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","startedAt":"2021-12-18T00:57:04Z","lastAt":"2021-12-21T13:52:46Z","messageCount":8,"participants":["Junio C Hamano","Sergey Organov","Ævar Arnfjörð Bjarmason","Derrick Stolee"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"444411","messageId":"xmqqbl1ezq5j.fsf@gitster.g","threadId":"57114","inReplyTo":null,"subject":"[PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-18T00:56:56Z","receivedAt":"2021-12-18T00:57:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"When a series of patches for a topic-B depends on having topic-A,\nthe workflow to prepare the topic-B branch would look like this:\n\n    $ git checkout -b topic-B main\n    $ git merge --no-ff --no-edit topic-A\n    $ git am <mbox-for-topic-B\n\nWhen topic-A gets updated, recreating the first merge and rebasing\nthe rest of the topic-B, all on detached HEAD, is a useful\ntechnique.  After updating topic-A with its new round of patches:\n\n    $ git checkout topic-B\n    $ prev=$(git rev-parse 'HEAD^{/^Merge branch .topic-A. into}')\n    $ git checkout --detach $prev^1\n    $ git merge --no-ff --no-edit topic-A\n    $ git rebase --onto HEAD $prev @{-1}^0\n    $ git checkout -B @{-1}\n\nThis will\n\n (0) check out the current topic-B.\n (1) find the previous merge of topic-A into topic-B.\n (2) detach the HEAD to the parent of the previous merge.\n (3) merge the updated topic-A to it.\n (4) reapply the patches to rebuild the rest of topic-B.\n (5) update topic-B with the result.\n\nwithout contaminating the reflog of topic-B too much.  topic-B@{1}\nis the \"logically previous\" state before topic-A got updated, for\nexample.  At (4), comparison (e.g. range-diff) between HEAD and\n@{-1} is a meaningful way to sanity check the result, and the same\ncan be done at (5) by comparing topic-B and topic-B@{1}.\n\nBut there is one glitch.  The merge into the detached HEAD done in\nthe step (3) above gives us \"Merge branch 'topic-A' into HEAD\", and\ndoes not say \"into topic-B\".\n\nTeach the \"--detached-head-name=<branch>\" option to \"git merge\" and\nits underlying \"git fmt-merge-message\", to pretend as if we were\nmerging into <branch> (instead of HEAD) when they prepare the merge\nmessage.  The pretend name honors the usual \"into <target>\"\nsuppression mechanism, which can be seen in the tests added here.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-fmt-merge-msg.txt |  6 ++++-\n Documentation/git-merge.txt         |  7 +++++-\n builtin/fmt-merge-msg.c             |  4 ++++\n builtin/merge.c                     |  4 ++++\n fmt-merge-msg.c                     |  2 ++\n fmt-merge-msg.h                     |  1 +\n t/t6200-fmt-merge-msg.sh            | 35 ++++++++++++++++++++++++++++-\n 7 files changed, 56 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt\nindex 6793d8fc05..235276752f 100644\n--- a/Documentation/git-fmt-merge-msg.txt\n+++ b/Documentation/git-fmt-merge-msg.txt\n@@ -9,7 +9,7 @@ git-fmt-merge-msg - Produce a merge commit message\n SYNOPSIS\n --------\n [verse]\n-'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log]\n+'git fmt-merge-msg' [-m <message>] [--detached-head-name <branch>] [--log[=<n>] | --no-log]\n 'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] -F <file>\n \n DESCRIPTION\n@@ -44,6 +44,10 @@ OPTIONS\n \tUse <message> instead of the branch names for the first line\n \tof the log message.  For use with `--log`.\n \n+--detached-head-name <branch>::\n+\tWhen merging into a detached HEAD, prepare the merge\n+\tmessage as if merging to the branch `<branch>` instead.\n+\n -F <file>::\n --file <file>::\n \tTake the list of merged objects from <file> instead of\ndiff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt\nindex e8cecf5a51..c369c1776a 100644\n--- a/Documentation/git-merge.txt\n+++ b/Documentation/git-merge.txt\n@@ -12,7 +12,8 @@ SYNOPSIS\n 'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]\n \t[--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]\n \t[--[no-]allow-unrelated-histories]\n-\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]\n+\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>]\n+\t[--detached-head-name <branch>] [<commit>...]\n 'git merge' (--continue | --abort | --quit)\n \n DESCRIPTION\n@@ -76,6 +77,10 @@ The 'git fmt-merge-msg' command can be\n used to give a good default for automated 'git merge'\n invocations. The automated message can include the branch description.\n \n+--detached-head-name <branch>::\n+\tWhen merging into a detached HEAD, prepare the default merge\n+\tmessage as if merging to the branch `<branch>` instead.\n+\n -F <file>::\n --file=<file>::\n \tRead the commit message to be used for the merge commit (in\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 48a8699de7..255c45b17a 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -12,6 +12,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n {\n \tconst char *inpath = NULL;\n \tconst char *message = NULL;\n+\tchar *detached_head_name = NULL;\n \tint shortlog_len = -1;\n \tstruct option options[] = {\n \t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n@@ -23,6 +24,8 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n \t\t  DEFAULT_MERGE_LOG_LEN },\n \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n \t\t\tN_(\"use <text> as start of message\")),\n+\t\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of detached HEAD\")),\n \t\tOPT_FILENAME('F', \"file\", &inpath, N_(\"file to read from\")),\n \t\tOPT_END()\n \t};\n@@ -56,6 +59,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n \topts.add_title = !message;\n \topts.credit_people = 1;\n \topts.shortlog_len = shortlog_len;\n+\topts.detached_head_name = detached_head_name;\n \n \tret = fmt_merge_msg(&input, &output, &opts);\n \tif (ret)\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5f0476b0b7..d0b624bd65 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -87,6 +87,7 @@ static int signoff;\n static const char *sign_commit;\n static int autostash;\n static int no_verify;\n+static char *detached_head_name;\n \n static struct strategy all_strategy[] = {\n \t{ \"recursive\",  NO_TRIVIAL },\n@@ -286,6 +287,8 @@ static struct option builtin_merge_options[] = {\n \t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n \t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n \t\tNULL, 0, option_read_message },\n+\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n+\t\t   N_(\"use <name> instead of detached HEAD\")),\n \tOPT__VERBOSITY(&verbosity),\n \tOPT_BOOL(0, \"abort\", &abort_current_merge,\n \t\tN_(\"abort the current in-progress merge\")),\n@@ -1121,6 +1124,7 @@ static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *mer\n \topts.add_title = !have_message;\n \topts.shortlog_len = shortlog_len;\n \topts.credit_people = (0 < option_edit);\n+\topts.detached_head_name = detached_head_name;\n \n \tfmt_merge_msg(merge_names, merge_msg, &opts);\n \tif (merge_msg->len)\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 5216191488..72b96d45e6 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -656,6 +656,8 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,\n \t\tdie(\"No current branch\");\n \tif (starts_with(current_branch, \"refs/heads/\"))\n \t\tcurrent_branch += 11;\n+\telse if (!strcmp(current_branch, \"HEAD\") && opts->detached_head_name)\n+\t\tcurrent_branch = opts->detached_head_name;\n \n \tfind_merge_parents(&merge_parents, in, &head_oid);\n \ndiff --git a/fmt-merge-msg.h b/fmt-merge-msg.h\nindex f2ab0e0085..b5f3a5ec54 100644\n--- a/fmt-merge-msg.h\n+++ b/fmt-merge-msg.h\n@@ -9,6 +9,7 @@ struct fmt_merge_msg_opts {\n \tunsigned add_title:1,\n \t\tcredit_people:1;\n \tint shortlog_len;\n+\tconst char *detached_head_name;\n };\n \n extern int merge_log_config;\ndiff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh\nindex 06c5fb5615..b6fa76189e 100755\n--- a/t/t6200-fmt-merge-msg.sh\n+++ b/t/t6200-fmt-merge-msg.sh\n@@ -573,7 +573,28 @@ test_expect_success 'merge-msg with \"merging\" an annotated tag' '\n \ttest_cmp expected .git/MERGE_MSG\n '\n \n+test_expect_success 'merge --detached=<name>' '\n+\ttest_when_finished \"git checkout main\" &&\n+\tgit checkout -B side main &&\n+\tgit commit --allow-empty -m \"One step ahead\" &&\n+\n+\tgit checkout --detach main &&\n+\tgit merge --no-ff side &&\n+\tgit show -s --format=\"%s\" >full.0 &&\n+\thead -n1 full.0 >actual &&\n+\t# expect that HEAD is shown as-is\n+\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n+\n+\tgit reset --hard main &&\n+\tgit merge --no-ff --detached=main side &&\n+\tgit show -s --format=\"%s\" >full.1 &&\n+\thead -n1 full.1 >actual &&\n+\t# expect that we pretend to be merging to main, that is suppressed\n+\tgrep -e \"Merge branch .side.$\" actual\n+'\n+\n test_expect_success 'merge.suppressDest configuration' '\n+\ttest_when_finished \"git checkout main\" &&\n \tgit checkout -B side main &&\n \tgit commit --allow-empty -m \"One step ahead\" &&\n \tgit checkout main &&\n@@ -590,7 +611,19 @@ test_expect_success 'merge.suppressDest configuration' '\n \tgit -c merge.suppressDest=\"ma?*[rn]\" fmt-merge-msg <.git/FETCH_HEAD >full.3 &&\n \thead -n1 full.3 >actual &&\n \tgrep -e \"Merge branch .side.\" actual &&\n-\t! grep -e \" into main$\" actual\n+\t! grep -e \" into main$\" actual &&\n+\n+\tgit checkout --detach HEAD &&\n+\tgit -c merge.suppressDest=\"main\" fmt-merge-msg <.git/FETCH_HEAD >full.4 &&\n+\thead -n1 full.4 >actual &&\n+\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n+\n+\tgit -c merge.suppressDest=\"main\" fmt-merge-msg \\\n+\t\t--detached=main <.git/FETCH_HEAD >full.5 &&\n+\thead -n1 full.5 >actual &&\n+\tgrep -e \"Merge branch .side.\" actual &&\n+\t! grep -e \" into main$\" actual &&\n+\t! grep -e \" into HEAD$\" actual\n '\n \n test_done\n-- \n2.34.1-472-g213ab46be7\n\n"},{"id":"444417","messageId":"87a6gyiamm.fsf@osv.gnss.ru","threadId":"57114","inReplyTo":"xmqqbl1ezq5j.fsf@gitster.g","subject":"Re: [PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Sergey Organov","fromEmail":"sorganov@gmail.com","sentAt":"2021-12-18T08:24:17Z","receivedAt":"2021-12-18T08:24:22Z","isPatch":true,"sender":{"key":"sorganov@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8501568?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n[...]\n\n> Teach the \"--detached-head-name=<branch>\" option to \"git merge\" and\n> its underlying \"git fmt-merge-message\", to pretend as if we were\n> merging into <branch> (instead of HEAD) when they prepare the merge\n> message.  The pretend name honors the usual \"into <target>\"\n> suppression mechanism, which can be seen in the tests added here.\n\nI think renaming destination branch in the commit message is useful in\ngeneral, and should not be limited to merging to detached head. Please\nconsider to introduce something like \"--into-name=<name>\" instead, that\nwill do this renaming unconditionally.\n\nAlso, being an advocate for eventual abandoning of \"detached head\" from\nGit terminology, I'd prefer to see more neutral name for the option\nanyway.\n\nThanks,\n-- Sergey Organov\n"},{"id":"444424","messageId":"211218.86sfuqw6ii.gmgdl@evledraar.gmail.com","threadId":"57114","inReplyTo":"xmqqbl1ezq5j.fsf@gitster.g","subject":"Re: [PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-18T10:27:34Z","receivedAt":"2021-12-18T10:29:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Dec 17 2021, Junio C Hamano wrote:\n\n> +\t\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n> +\t\t\t   N_(\"use <name> instead of detached HEAD\")),\n> [...]\n> +\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n> +\t\t   N_(\"use <name> instead of detached HEAD\")),\n\nMaybe OPT_STRING_F and PARSE_OPT_NONEG, or does --no-detached make\nsense?\n"},{"id":"444487","messageId":"b52be977-f03c-7915-68c8-f1842e920184@gmail.com","threadId":"57114","inReplyTo":"211218.86sfuqw6ii.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-12-20T15:36:28Z","receivedAt":"2021-12-20T15:36:35Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 12/18/2021 5:27 AM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Dec 17 2021, Junio C Hamano wrote:\n> \n>> +\t\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n>> +\t\t\t   N_(\"use <name> instead of detached HEAD\")),\n>> [...]\n>> +\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n>> +\t\t   N_(\"use <name> instead of detached HEAD\")),\n> \n> Maybe OPT_STRING_F and PARSE_OPT_NONEG, or does --no-detached make\n> sense?\n\nWhile sometimes undocumented, these \"--no-\" options allows calling an\nalias that might have the positive version included while disabling the\noption.\n\n\tgit alias merge-main merge <lots-of-custom-things> --detached-head-name=main\n\tgit merge-main --no-detached-head-name\n\nSo, I think using the standard OPT_STRING here is fine.\n\nThanks,\n-Stolee\n"},{"id":"444488","messageId":"xmqq35mnz2x6.fsf@gitster.g","threadId":"57114","inReplyTo":"87a6gyiamm.fsf@osv.gnss.ru","subject":"Re: [PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-20T15:55:33Z","receivedAt":"2021-12-20T15:55:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Sergey Organov <sorganov@gmail.com> writes:\n\n> ...\n> consider to introduce something like \"--into-name=<name>\" instead, that\n> will do this renaming unconditionally.\n\nExcellent.\n\nIn the scenario I illustrated in the proposed log message, I used a\nthrow-away temporary working area to prepare the history for a real\nbranch, and detached HEAD is the perfect match for such a temporary\nworking area.  But it should not be a crime to use a real temporary\nbranch for such use case.\n\nThanks.\n"},{"id":"444489","messageId":"xmqqwnjzxo9x.fsf@gitster.g","threadId":"57114","inReplyTo":"b52be977-f03c-7915-68c8-f1842e920184@gmail.com","subject":"Re: [PATCH] merge: allow to pretend a merge into detached HEAD is made into a branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-20T15:57:14Z","receivedAt":"2021-12-20T15:57:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n> While sometimes undocumented, these \"--no-\" options allows calling an\n> alias that might have the positive version included while disabling the\n> option.\n>\n> \tgit alias merge-main merge <lots-of-custom-things> --detached-head-name=main\n> \tgit merge-main --no-detached-head-name\n>\n> So, I think using the standard OPT_STRING here is fine.\n\nExactly.  Not just 'is fine', but is more correct.  Thanks.\n\n"},{"id":"444554","messageId":"xmqqee66ubuw.fsf_-_@gitster.g","threadId":"57114","inReplyTo":"xmqq35mnz2x6.fsf@gitster.g","subject":"[PATCH v2] merge: allow to pretend a merge is made into a different branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-12-20T22:53:43Z","receivedAt":"2021-12-20T22:53:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"When a series of patches for a topic-B depends on having topic-A,\nthe workflow to prepare the topic-B branch would look like this:\n\n    $ git checkout -b topic-B main\n    $ git merge --no-ff --no-edit topic-A\n    $ git am <mbox-for-topic-B\n\nWhen topic-A gets updated, recreating the first merge and rebasing\nthe rest of the topic-B, all on detached HEAD, is a useful\ntechnique.  After updating topic-A with its new round of patches:\n\n    $ git checkout topic-B\n    $ prev=$(git rev-parse 'HEAD^{/^Merge branch .topic-A. into}')\n    $ git checkout --detach $prev^1\n    $ git merge --no-ff --no-edit topic-A\n    $ git rebase --onto HEAD $prev @{-1}^0\n    $ git checkout -B @{-1}\n\nThis will\n\n (0) check out the current topic-B.\n (1) find the previous merge of topic-A into topic-B.\n (2) detach the HEAD to the parent of the previous merge.\n (3) merge the updated topic-A to it.\n (4) reapply the patches to rebuild the rest of topic-B.\n (5) update topic-B with the result.\n\nwithout contaminating the reflog of topic-B too much.  topic-B@{1}\nis the \"logically previous\" state before topic-A got updated, for\nexample.  At (4), comparison (e.g. range-diff) between HEAD and\n@{-1} is a meaningful way to sanity check the result, and the same\ncan be done at (5) by comparing topic-B and topic-B@{1}.\n\nBut there is one glitch.  The merge into the detached HEAD done in\nthe step (3) above gives us \"Merge branch 'topic-A' into HEAD\", and\ndoes not say \"into topic-B\".\n\nTeach the \"--into-name=<branch>\" option to \"git merge\" and its\nunderlying \"git fmt-merge-message\", to pretend as if we were merging\ninto <branch>, no matter what branch we are actually merging into,\nwhen they prepare the merge message.  The pretend name honors the\nusual \"into <target>\" suppression mechanism, which can be seen in\nthe tests added here.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n \nRange-diff:\n1:  0ada838311 ! 1:  273df9ed2a merge: allow to pretend a merge into detached HEAD is made into a branch\n    @@ Metadata\n     Author: Junio C Hamano <gitster@pobox.com>\n     \n      ## Commit message ##\n    -    merge: allow to pretend a merge into detached HEAD is made into a branch\n    +    merge: allow to pretend a merge is made into a different branch\n     \n         When a series of patches for a topic-B depends on having topic-A,\n         the workflow to prepare the topic-B branch would look like this:\n    @@ Commit message\n         the step (3) above gives us \"Merge branch 'topic-A' into HEAD\", and\n         does not say \"into topic-B\".\n     \n    -    Teach the \"--detached-head-name=<branch>\" option to \"git merge\" and\n    -    its underlying \"git fmt-merge-message\", to pretend as if we were\n    -    merging into <branch> (instead of HEAD) when they prepare the merge\n    -    message.  The pretend name honors the usual \"into <target>\"\n    -    suppression mechanism, which can be seen in the tests added here.\n    +    Teach the \"--into-name=<branch>\" option to \"git merge\" and its\n    +    underlying \"git fmt-merge-message\", to pretend as if we were merging\n    +    into <branch>, no matter what branch we are actually merging into,\n    +    when they prepare the merge message.  The pretend name honors the\n    +    usual \"into <target>\" suppression mechanism, which can be seen in\n    +    the tests added here.\n     \n         Signed-off-by: Junio C Hamano <gitster@pobox.com>\n     \n    @@ Documentation/git-fmt-merge-msg.txt: git-fmt-merge-msg - Produce a merge commit\n      --------\n      [verse]\n     -'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log]\n    -+'git fmt-merge-msg' [-m <message>] [--detached-head-name <branch>] [--log[=<n>] | --no-log]\n    ++'git fmt-merge-msg' [-m <message>] [--into-name <branch>] [--log[=<n>] | --no-log]\n      'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] -F <file>\n      \n      DESCRIPTION\n    @@ Documentation/git-fmt-merge-msg.txt: OPTIONS\n      \tUse <message> instead of the branch names for the first line\n      \tof the log message.  For use with `--log`.\n      \n    -+--detached-head-name <branch>::\n    -+\tWhen merging into a detached HEAD, prepare the merge\n    -+\tmessage as if merging to the branch `<branch>` instead.\n    ++--into-name <branch>::\n    ++\tPrepare the merge message as if merging to the branch `<branch>`,\n    ++\tinstead of the name of the real branch to which the merge is made.\n     +\n      -F <file>::\n      --file <file>::\n    @@ Documentation/git-merge.txt: SYNOPSIS\n      \t[--[no-]allow-unrelated-histories]\n     -\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]\n     +\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>]\n    -+\t[--detached-head-name <branch>] [<commit>...]\n    ++\t[--into-name <branch>] [<commit>...]\n      'git merge' (--continue | --abort | --quit)\n      \n      DESCRIPTION\n    @@ Documentation/git-merge.txt: The 'git fmt-merge-msg' command can be\n      used to give a good default for automated 'git merge'\n      invocations. The automated message can include the branch description.\n      \n    -+--detached-head-name <branch>::\n    -+\tWhen merging into a detached HEAD, prepare the default merge\n    -+\tmessage as if merging to the branch `<branch>` instead.\n    ++--into-name <branch>::\n    ++\tPrepare the default merge message as if merging to the branch\n    ++\t`<branch>`, instead of the name of the real branch to which\n    ++\tthe merge is made.\n     +\n      -F <file>::\n      --file=<file>::\n    @@ builtin/fmt-merge-msg.c: int cmd_fmt_merge_msg(int argc, const char **argv, cons\n      {\n      \tconst char *inpath = NULL;\n      \tconst char *message = NULL;\n    -+\tchar *detached_head_name = NULL;\n    ++\tchar *into_name = NULL;\n      \tint shortlog_len = -1;\n      \tstruct option options[] = {\n      \t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n    @@ builtin/fmt-merge-msg.c: int cmd_fmt_merge_msg(int argc, const char **argv, cons\n      \t\t  DEFAULT_MERGE_LOG_LEN },\n      \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n      \t\t\tN_(\"use <text> as start of message\")),\n    -+\t\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n    -+\t\t\t   N_(\"use <name> instead of detached HEAD\")),\n    ++\t\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n    ++\t\t\t   N_(\"use <name> instead of the real target branch\")),\n      \t\tOPT_FILENAME('F', \"file\", &inpath, N_(\"file to read from\")),\n      \t\tOPT_END()\n      \t};\n    @@ builtin/fmt-merge-msg.c: int cmd_fmt_merge_msg(int argc, const char **argv, cons\n      \topts.add_title = !message;\n      \topts.credit_people = 1;\n      \topts.shortlog_len = shortlog_len;\n    -+\topts.detached_head_name = detached_head_name;\n    ++\topts.into_name = into_name;\n      \n      \tret = fmt_merge_msg(&input, &output, &opts);\n      \tif (ret)\n    @@ builtin/merge.c: static int signoff;\n      static const char *sign_commit;\n      static int autostash;\n      static int no_verify;\n    -+static char *detached_head_name;\n    ++static char *into_name;\n      \n      static struct strategy all_strategy[] = {\n      \t{ \"recursive\",  NO_TRIVIAL },\n    @@ builtin/merge.c: static struct option builtin_merge_options[] = {\n      \t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n      \t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n      \t\tNULL, 0, option_read_message },\n    -+\tOPT_STRING(0, \"detached\", &detached_head_name, N_(\"name\"),\n    -+\t\t   N_(\"use <name> instead of detached HEAD\")),\n    ++\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n    ++\t\t   N_(\"use <name> instead of the real target\")),\n      \tOPT__VERBOSITY(&verbosity),\n      \tOPT_BOOL(0, \"abort\", &abort_current_merge,\n      \t\tN_(\"abort the current in-progress merge\")),\n    @@ builtin/merge.c: static void prepare_merge_message(struct strbuf *merge_names, s\n      \topts.add_title = !have_message;\n      \topts.shortlog_len = shortlog_len;\n      \topts.credit_people = (0 < option_edit);\n    -+\topts.detached_head_name = detached_head_name;\n    ++\topts.into_name = into_name;\n      \n      \tfmt_merge_msg(merge_names, merge_msg, &opts);\n      \tif (merge_msg->len)\n     \n      ## fmt-merge-msg.c ##\n     @@ fmt-merge-msg.c: int fmt_merge_msg(struct strbuf *in, struct strbuf *out,\n    + \n    + \tmemset(&merge_parents, 0, sizeof(merge_parents));\n    + \n    +-\t/* get current branch */\n    ++\t/* learn the commit that we merge into and the current branch name */\n    + \tcurrent_branch = current_branch_to_free =\n    + \t\tresolve_refdup(\"HEAD\", RESOLVE_REF_READING, &head_oid, NULL);\n    + \tif (!current_branch)\n      \t\tdie(\"No current branch\");\n    - \tif (starts_with(current_branch, \"refs/heads/\"))\n    +-\tif (starts_with(current_branch, \"refs/heads/\"))\n    ++\n    ++\tif (opts->into_name)\n    ++\t\tcurrent_branch = opts->into_name;\n    ++\telse if (starts_with(current_branch, \"refs/heads/\"))\n      \t\tcurrent_branch += 11;\n    -+\telse if (!strcmp(current_branch, \"HEAD\") && opts->detached_head_name)\n    -+\t\tcurrent_branch = opts->detached_head_name;\n      \n      \tfind_merge_parents(&merge_parents, in, &head_oid);\n    - \n     \n      ## fmt-merge-msg.h ##\n     @@ fmt-merge-msg.h: struct fmt_merge_msg_opts {\n      \tunsigned add_title:1,\n      \t\tcredit_people:1;\n      \tint shortlog_len;\n    -+\tconst char *detached_head_name;\n    ++\tconst char *into_name;\n      };\n      \n      extern int merge_log_config;\n    @@ t/t6200-fmt-merge-msg.sh: test_expect_success 'merge-msg with \"merging\" an annot\n      \ttest_cmp expected .git/MERGE_MSG\n      '\n      \n    -+test_expect_success 'merge --detached=<name>' '\n    ++test_expect_success 'merge --into-name=<name>' '\n     +\ttest_when_finished \"git checkout main\" &&\n     +\tgit checkout -B side main &&\n     +\tgit commit --allow-empty -m \"One step ahead\" &&\n    @@ t/t6200-fmt-merge-msg.sh: test_expect_success 'merge-msg with \"merging\" an annot\n     +\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n     +\n     +\tgit reset --hard main &&\n    -+\tgit merge --no-ff --detached=main side &&\n    ++\tgit merge --no-ff --into-name=main side &&\n     +\tgit show -s --format=\"%s\" >full.1 &&\n     +\thead -n1 full.1 >actual &&\n     +\t# expect that we pretend to be merging to main, that is suppressed\n    ++\tgrep -e \"Merge branch .side.$\" actual &&\n    ++\n    ++\tgit checkout -b throwaway main &&\n    ++\tgit merge --no-ff --into-name=main side &&\n    ++\tgit show -s --format=\"%s\" >full.2 &&\n    ++\thead -n1 full.2 >actual &&\n    ++\t# expect that we pretend to be merging to main, that is suppressed\n     +\tgrep -e \"Merge branch .side.$\" actual\n     +'\n     +\n    @@ t/t6200-fmt-merge-msg.sh: test_expect_success 'merge.suppressDest configuration'\n     +\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n     +\n     +\tgit -c merge.suppressDest=\"main\" fmt-merge-msg \\\n    -+\t\t--detached=main <.git/FETCH_HEAD >full.5 &&\n    ++\t\t--into-name=main <.git/FETCH_HEAD >full.5 &&\n     +\thead -n1 full.5 >actual &&\n     +\tgrep -e \"Merge branch .side.\" actual &&\n     +\t! grep -e \" into main$\" actual &&\n\n Documentation/git-fmt-merge-msg.txt |  6 ++++-\n Documentation/git-merge.txt         |  8 +++++-\n builtin/fmt-merge-msg.c             |  4 +++\n builtin/merge.c                     |  4 +++\n fmt-merge-msg.c                     |  7 +++--\n fmt-merge-msg.h                     |  1 +\n t/t6200-fmt-merge-msg.sh            | 42 ++++++++++++++++++++++++++++-\n 7 files changed, 67 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt\nindex 6793d8fc05..6f28812f38 100644\n--- a/Documentation/git-fmt-merge-msg.txt\n+++ b/Documentation/git-fmt-merge-msg.txt\n@@ -9,7 +9,7 @@ git-fmt-merge-msg - Produce a merge commit message\n SYNOPSIS\n --------\n [verse]\n-'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log]\n+'git fmt-merge-msg' [-m <message>] [--into-name <branch>] [--log[=<n>] | --no-log]\n 'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] -F <file>\n \n DESCRIPTION\n@@ -44,6 +44,10 @@ OPTIONS\n \tUse <message> instead of the branch names for the first line\n \tof the log message.  For use with `--log`.\n \n+--into-name <branch>::\n+\tPrepare the merge message as if merging to the branch `<branch>`,\n+\tinstead of the name of the real branch to which the merge is made.\n+\n -F <file>::\n --file <file>::\n \tTake the list of merged objects from <file> instead of\ndiff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt\nindex e4f3352eb5..ed0990621f 100644\n--- a/Documentation/git-merge.txt\n+++ b/Documentation/git-merge.txt\n@@ -12,7 +12,8 @@ SYNOPSIS\n 'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]\n \t[--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]\n \t[--[no-]allow-unrelated-histories]\n-\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]\n+\t[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>]\n+\t[--into-name <branch>] [<commit>...]\n 'git merge' (--continue | --abort | --quit)\n \n DESCRIPTION\n@@ -76,6 +77,11 @@ The 'git fmt-merge-msg' command can be\n used to give a good default for automated 'git merge'\n invocations. The automated message can include the branch description.\n \n+--into-name <branch>::\n+\tPrepare the default merge message as if merging to the branch\n+\t`<branch>`, instead of the name of the real branch to which\n+\tthe merge is made.\n+\n -F <file>::\n --file=<file>::\n \tRead the commit message to be used for the merge commit (in\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 48a8699de7..8d8fd393f8 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -12,6 +12,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n {\n \tconst char *inpath = NULL;\n \tconst char *message = NULL;\n+\tchar *into_name = NULL;\n \tint shortlog_len = -1;\n \tstruct option options[] = {\n \t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n@@ -23,6 +24,8 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n \t\t  DEFAULT_MERGE_LOG_LEN },\n \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n \t\t\tN_(\"use <text> as start of message\")),\n+\t\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n+\t\t\t   N_(\"use <name> instead of the real target branch\")),\n \t\tOPT_FILENAME('F', \"file\", &inpath, N_(\"file to read from\")),\n \t\tOPT_END()\n \t};\n@@ -56,6 +59,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)\n \topts.add_title = !message;\n \topts.credit_people = 1;\n \topts.shortlog_len = shortlog_len;\n+\topts.into_name = into_name;\n \n \tret = fmt_merge_msg(&input, &output, &opts);\n \tif (ret)\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex ea3112e0c0..1ba5951d49 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -87,6 +87,7 @@ static int signoff;\n static const char *sign_commit;\n static int autostash;\n static int no_verify;\n+static char *into_name;\n \n static struct strategy all_strategy[] = {\n \t{ \"recursive\",  NO_TRIVIAL },\n@@ -286,6 +287,8 @@ static struct option builtin_merge_options[] = {\n \t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n \t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n \t\tNULL, 0, option_read_message },\n+\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n+\t\t   N_(\"use <name> instead of the real target\")),\n \tOPT__VERBOSITY(&verbosity),\n \tOPT_BOOL(0, \"abort\", &abort_current_merge,\n \t\tN_(\"abort the current in-progress merge\")),\n@@ -1122,6 +1125,7 @@ static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *mer\n \topts.add_title = !have_message;\n \topts.shortlog_len = shortlog_len;\n \topts.credit_people = (0 < option_edit);\n+\topts.into_name = into_name;\n \n \tfmt_merge_msg(merge_names, merge_msg, &opts);\n \tif (merge_msg->len)\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 5216191488..d25594545c 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -649,12 +649,15 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out,\n \n \tmemset(&merge_parents, 0, sizeof(merge_parents));\n \n-\t/* get current branch */\n+\t/* learn the commit that we merge into and the current branch name */\n \tcurrent_branch = current_branch_to_free =\n \t\tresolve_refdup(\"HEAD\", RESOLVE_REF_READING, &head_oid, NULL);\n \tif (!current_branch)\n \t\tdie(\"No current branch\");\n-\tif (starts_with(current_branch, \"refs/heads/\"))\n+\n+\tif (opts->into_name)\n+\t\tcurrent_branch = opts->into_name;\n+\telse if (starts_with(current_branch, \"refs/heads/\"))\n \t\tcurrent_branch += 11;\n \n \tfind_merge_parents(&merge_parents, in, &head_oid);\ndiff --git a/fmt-merge-msg.h b/fmt-merge-msg.h\nindex f2ab0e0085..99054042dc 100644\n--- a/fmt-merge-msg.h\n+++ b/fmt-merge-msg.h\n@@ -9,6 +9,7 @@ struct fmt_merge_msg_opts {\n \tunsigned add_title:1,\n \t\tcredit_people:1;\n \tint shortlog_len;\n+\tconst char *into_name;\n };\n \n extern int merge_log_config;\ndiff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh\nindex 06c5fb5615..d861d7ca28 100755\n--- a/t/t6200-fmt-merge-msg.sh\n+++ b/t/t6200-fmt-merge-msg.sh\n@@ -573,7 +573,35 @@ test_expect_success 'merge-msg with \"merging\" an annotated tag' '\n \ttest_cmp expected .git/MERGE_MSG\n '\n \n+test_expect_success 'merge --into-name=<name>' '\n+\ttest_when_finished \"git checkout main\" &&\n+\tgit checkout -B side main &&\n+\tgit commit --allow-empty -m \"One step ahead\" &&\n+\n+\tgit checkout --detach main &&\n+\tgit merge --no-ff side &&\n+\tgit show -s --format=\"%s\" >full.0 &&\n+\thead -n1 full.0 >actual &&\n+\t# expect that HEAD is shown as-is\n+\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n+\n+\tgit reset --hard main &&\n+\tgit merge --no-ff --into-name=main side &&\n+\tgit show -s --format=\"%s\" >full.1 &&\n+\thead -n1 full.1 >actual &&\n+\t# expect that we pretend to be merging to main, that is suppressed\n+\tgrep -e \"Merge branch .side.$\" actual &&\n+\n+\tgit checkout -b throwaway main &&\n+\tgit merge --no-ff --into-name=main side &&\n+\tgit show -s --format=\"%s\" >full.2 &&\n+\thead -n1 full.2 >actual &&\n+\t# expect that we pretend to be merging to main, that is suppressed\n+\tgrep -e \"Merge branch .side.$\" actual\n+'\n+\n test_expect_success 'merge.suppressDest configuration' '\n+\ttest_when_finished \"git checkout main\" &&\n \tgit checkout -B side main &&\n \tgit commit --allow-empty -m \"One step ahead\" &&\n \tgit checkout main &&\n@@ -590,7 +618,19 @@ test_expect_success 'merge.suppressDest configuration' '\n \tgit -c merge.suppressDest=\"ma?*[rn]\" fmt-merge-msg <.git/FETCH_HEAD >full.3 &&\n \thead -n1 full.3 >actual &&\n \tgrep -e \"Merge branch .side.\" actual &&\n-\t! grep -e \" into main$\" actual\n+\t! grep -e \" into main$\" actual &&\n+\n+\tgit checkout --detach HEAD &&\n+\tgit -c merge.suppressDest=\"main\" fmt-merge-msg <.git/FETCH_HEAD >full.4 &&\n+\thead -n1 full.4 >actual &&\n+\tgrep -e \"Merge branch .side. into HEAD$\" actual &&\n+\n+\tgit -c merge.suppressDest=\"main\" fmt-merge-msg \\\n+\t\t--into-name=main <.git/FETCH_HEAD >full.5 &&\n+\thead -n1 full.5 >actual &&\n+\tgrep -e \"Merge branch .side.\" actual &&\n+\t! grep -e \" into main$\" actual &&\n+\t! grep -e \" into HEAD$\" actual\n '\n \n test_done\n-- \n2.34.1-472-g213ab46be7\n\n"},{"id":"444610","messageId":"c09a2e85-1c27-ed19-eb6b-c4daece49133@gmail.com","threadId":"57114","inReplyTo":"xmqqee66ubuw.fsf_-_@gitster.g","subject":"Re: [PATCH v2] merge: allow to pretend a merge is made into a different branch","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-12-21T13:52:42Z","receivedAt":"2021-12-21T13:52:46Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 12/20/2021 5:53 PM, Junio C Hamano wrote:\n\n> Teach the \"--into-name=<branch>\" option to \"git merge\" and its\n> underlying \"git fmt-merge-message\", to pretend as if we were merging\n> into <branch>, no matter what branch we are actually merging into,\n> when they prepare the merge message.  The pretend name honors the\n> usual \"into <target>\" suppression mechanism, which can be seen in\n> the tests added here.\n\nI like the change to \"--into-name\".\n \n> -\tif (starts_with(current_branch, \"refs/heads/\"))\n> +\n> +\tif (opts->into_name)\n> +\t\tcurrent_branch = opts->into_name;\n> +\telse if (starts_with(current_branch, \"refs/heads/\"))\n>  \t\tcurrent_branch += 11;\n\nI briefly considered whether this 'else if' should just be an\n'if' to allow stripping \"revs/heads/\" from the --into-name option,\nbut I think it is better as-is, so we use the literal value that\nthe user supplied.\n\nThis patch LGTM.\n\nThanks,\n-Stolee\n"}]}