{"thread":{"id":"66426","subject":"[PATCH 0/2] checkout -m: recreate conflict labels","startedAt":"2026-09-30T09:49:07Z","lastAt":"2026-10-05T16:31:22Z","messageCount":18,"participants":["Phillip Wood","Junio C Hamano","Johannes Sixt"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"553677","messageId":"cover.1790761727.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":null,"subject":"[PATCH 0/2] checkout -m: recreate conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-30T09:48:47Z","receivedAt":"2026-09-30T09:49:07Z","isPatch":true,"body":"When \"git checkout -m <path>\" recreates a merge conflict, it uses\nthe labels \"base\", \"ours\", \"theirs\", rather than the labels used by\nthe original merge. This short series teaches the ort machinery to\nwrite the labels to \".git/MERGE_LABELS\" when it switches to a merge\nresult containing conflicts, so that \"git checkout -m\" can then read\nthat file and use the same labels.\n\nAs \"git checkout -m\" is recreating the original conflict I wonder\nif we should remember the conflict style as well so that\n\n    git -c merge.conflictStyle=diff3 git merge topic\n    git checkout -m <unmerged-path>\n\nwould recreate diff3 style conflicts, instead of using the default\nconfig. I cannot decide if that would be convenient or confusing and\nam interested to hear what others think.\n\nbase-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Fconflict-labels%2Fv1\nView-Changes-At: https://github.com/phillipwood/git/compare/3bc034112...fdaf3da99\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/conflict-labels/v1\n\n\nPhillip Wood (2):\n  remove_branch_state: convert boolean argument to flags\n  merge: remember conflict labels\n\n branch.c           | 17 +++++++++----\n branch.h           |  4 ++-\n builtin/checkout.c | 30 ++++++++++++++++++----\n builtin/commit.c   |  1 +\n merge-ort.c        | 19 ++++++++++++++\n merge.c            | 63 ++++++++++++++++++++++++++++++++++++++++++++++\n merge.h            |  4 +++\n path.c             |  1 +\n path.h             |  1 +\n repository.c       |  1 +\n repository.h       |  1 +\n sequencer.c        |  1 +\n t/t7201-co.sh      | 21 ++++++++++++++++\n 13 files changed, 153 insertions(+), 11 deletions(-)\n\n-- \n2.56.0.rc2.84.gaf8b4f0d381\n\n"},{"id":"553678","messageId":"86ef0f848a35c66b2d68f96c5f307a6c80f74c78.1790761727.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":"cover.1790761727.git.phillip.wood@dunelm.org.uk","subject":"[PATCH 1/2] remove_branch_state: convert boolean argument to flags","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-30T09:48:48Z","receivedAt":"2026-09-30T09:49:07Z","isPatch":true,"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nConvert the \"verbose\" boolean argument to a flag so that we can add\nmore flags in a future commit.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n branch.c           | 4 ++--\n branch.h           | 3 ++-\n builtin/checkout.c | 6 +++++-\n 3 files changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/branch.c b/branch.c\nindex 22f4f46b96..8bc7a395a7 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -871,9 +871,9 @@ void remove_merge_branch_state(struct repository *r)\n \tsave_autostash_ref(r, \"MERGE_AUTOSTASH\");\n }\n \n-void remove_branch_state(struct repository *r, int verbose)\n+void remove_branch_state(struct repository *r, unsigned flags)\n {\n-\tsequencer_post_commit_cleanup(r, verbose);\n+\tsequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE);\n \tunlink(git_path_squash_msg(r));\n \tremove_merge_branch_state(r);\n }\ndiff --git a/branch.h b/branch.h\nindex e9b1f7b37d..42d1b12918 100644\n--- a/branch.h\n+++ b/branch.h\n@@ -127,6 +127,7 @@ int validate_branchname(const char *name, struct strbuf *ref);\n  */\n int validate_new_branchname(const char *name, struct strbuf *ref, int force);\n \n+#define REMOVE_BRANCH_STATE_VERBOSE (1u << 0)\n /*\n  * Remove information about the merge state on the current\n  * branch. (E.g., MERGE_HEAD)\n@@ -137,7 +138,7 @@ void remove_merge_branch_state(struct repository *r);\n  * Remove information about the state of working on the current\n  * branch. (E.g., MERGE_HEAD)\n  */\n-void remove_branch_state(struct repository *r, int verbose);\n+void remove_branch_state(struct repository *r, unsigned flags);\n \n /*\n  * Configure local branch \"local\" as downstream to branch \"remote\"\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex c0f0d2c700..bdd2d816b6 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -950,6 +950,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *old_desc, *reflog_msg;\n+\tunsigned flags = 0;\n+\n \tif (opts->new_branch) {\n \t\tif (opts->new_orphan_branch) {\n \t\t\tenum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;\n@@ -1044,7 +1046,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\t\t\t\t   old_branch_info->path);\n \t\t}\n \t}\n-\tremove_branch_state(the_repository, !opts->quiet);\n+\tif (!opts->quiet)\n+\t\tflags |= REMOVE_BRANCH_STATE_VERBOSE;\n+\tremove_branch_state(the_repository, flags);\n \tstrbuf_release(&msg);\n \tif (!opts->quiet &&\n \t    !opts->force_detach &&\n-- \n2.56.0.rc2.84.gaf8b4f0d381\n\n"},{"id":"553679","messageId":"fdaf3da993366878b51bd0b2a950888710cafb8a.1790761727.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":"cover.1790761727.git.phillip.wood@dunelm.org.uk","subject":"[PATCH 2/2] merge: remember conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-30T09:48:49Z","receivedAt":"2026-09-30T09:49:09Z","isPatch":true,"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nWhen recreating merge conflicts with \"git checkout -m <path>\" the\noriginal conflict labels are lost. For commands like \"git merge\" and\n\"git cherry-pick\" we could use the presence of the related root\nref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the\nlabels. However, if the conflicts are from \"git stash pop\" or \"git\ncheckout -m <branch>\", then there is no ref to deduce the labels from. To\nensure the labels are always available, the merge machinery is updated to\nwrite \".git/MERGE_LABELS\" when it updates the worktree and\nthere are conflicts. The labels are then read from that file by \"git\ncheckout -m <path>\" when recreating the conflicts.\n\nAs \"git checkout -m <branch>\" calls remove_branch_state() which\nordinarily removes the labels file, we need to pass a flag down\nto optionally prevent that so that the labels are available for any\nsubsequent \"git checkout -m <path>\". Note that merge_switch_to_result()\nwe assign \"result->priv\" to \"opt->priv\" and later clear \"opt->priv\" in\norder to get a pointer to the private struct as result->priv is void*.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n branch.c           | 11 ++++++--\n branch.h           |  1 +\n builtin/checkout.c | 24 +++++++++++++++---\n builtin/commit.c   |  1 +\n merge-ort.c        | 19 ++++++++++++++\n merge.c            | 63 ++++++++++++++++++++++++++++++++++++++++++++++\n merge.h            |  4 +++\n path.c             |  1 +\n path.h             |  1 +\n repository.c       |  1 +\n repository.h       |  1 +\n sequencer.c        |  1 +\n t/t7201-co.sh      | 21 ++++++++++++++++\n 13 files changed, 143 insertions(+), 6 deletions(-)\n\ndiff --git a/branch.c b/branch.c\nindex 8bc7a395a7..5bb1c28915 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -860,9 +860,11 @@ void create_branches_recursively(struct repository *r, const char *name,\n \tfree(branch_point);\n }\n \n-void remove_merge_branch_state(struct repository *r)\n+static void do_remove_merge_branch_state(struct repository *r, unsigned flags)\n {\n \tunlink(git_path_merge_head(r));\n+\tif (!(flags & REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS))\n+\t\tunlink(git_path_merge_labels(r));\n \tunlink(git_path_merge_rr(r));\n \tunlink(git_path_merge_msg(r));\n \tunlink(git_path_merge_mode(r));\n@@ -871,11 +873,16 @@ void remove_merge_branch_state(struct repository *r)\n \tsave_autostash_ref(r, \"MERGE_AUTOSTASH\");\n }\n \n+void remove_merge_branch_state(struct repository *r)\n+{\n+\tdo_remove_merge_branch_state(r, 0);\n+}\n+\n void remove_branch_state(struct repository *r, unsigned flags)\n {\n \tsequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE);\n \tunlink(git_path_squash_msg(r));\n-\tremove_merge_branch_state(r);\n+\tdo_remove_merge_branch_state(r, flags);\n }\n \n void die_if_checked_out(const char *branch, int ignore_current_worktree)\ndiff --git a/branch.h b/branch.h\nindex 42d1b12918..95b2431f24 100644\n--- a/branch.h\n+++ b/branch.h\n@@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref);\n int validate_new_branchname(const char *name, struct strbuf *ref, int force);\n \n #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0)\n+#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1)\n /*\n  * Remove information about the merge state on the current\n  * branch. (E.g., MERGE_HEAD)\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex bdd2d816b6..295fe0e9fa 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -15,6 +15,7 @@\n #include \"hex.h\"\n #include \"hook.h\"\n #include \"merge-ll.h\"\n+#include \"merge.h\"\n #include \"lockfile.h\"\n #include \"mem-pool.h\"\n #include \"object-file.h\"\n@@ -317,6 +318,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = the_repository->index->cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tchar *base_label, *ours_label, *theirs_label;\n \tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n@@ -347,10 +349,19 @@ static int checkout_merged(int pos, const struct checkout *state,\n \n \trepo_config_get_bool(the_repository, \"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n+\tif (read_merge_labels(the_repository, &base_label, &ours_label,\n+\t\t\t      &theirs_label)) {\n+\t\tbase_label = xstrdup(\"base\");\n+\t\tours_label = xstrdup(\"ours\");\n+\t\ttheirs_label = xstrdup(\"theirs\");\n+\t}\n \tll_opts.conflict_style = conflict_style;\n-\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, base_label,\n+\t\t\t\t&ours, ours_label, &theirs, theirs_label,\n \t\t\t\tstate->istate, &ll_opts);\n+\tfree(base_label);\n+\tfree(ours_label);\n+\tfree(theirs_label);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n@@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info)\n \n static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\t\t   struct branch_info *old_branch_info,\n-\t\t\t\t   struct branch_info *new_branch_info)\n+\t\t\t\t   struct branch_info *new_branch_info,\n+\t\t\t\t   bool merge_conflicts)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *old_desc, *reflog_msg;\n@@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t}\n \tif (!opts->quiet)\n \t\tflags |= REMOVE_BRANCH_STATE_VERBOSE;\n+\tif (merge_conflicts)\n+\t\tflags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS;\n \tremove_branch_state(the_repository, flags);\n \tstrbuf_release(&msg);\n \tif (!opts->quiet &&\n@@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts,\n \n \tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n \t\tfputc('\\n', stderr);\n-\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n+\n+\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info,\n+\t\t\t       autostash_res == STASH_APPLY_CONFLICT);\n \n \tif (created_autostash) {\n \t\tdiscard_index(the_repository->index);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 205fbd57e3..c374d5e0d5 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1977,6 +1977,7 @@ int cmd_commit(int argc,\n \n \tsequencer_post_commit_cleanup(the_repository, 0);\n \tunlink(git_path_merge_head(the_repository));\n+\tunlink(git_path_merge_labels(the_repository));\n \tunlink(git_path_merge_msg(the_repository));\n \tunlink(git_path_merge_mode(the_repository));\n \tunlink(git_path_squash_msg(the_repository));\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c410a5d353..783e74c6e3 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -35,6 +35,7 @@\n #include \"hex.h\"\n #include \"entry.h\"\n #include \"merge-ll.h\"\n+#include \"merge.h\"\n #include \"match-trees.h\"\n #include \"mem-pool.h\"\n #include \"object-file.h\"\n@@ -418,6 +419,9 @@ struct merge_options_internal {\n \n \t/* field that holds submodule conflict information */\n \tstruct string_list conflicted_submodules;\n+\n+\t/* Copies of the labels used for conflict markers */\n+\tchar *labels[3];\n };\n \n struct conflicted_submodule_item {\n@@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\t\treturn;\n \t\t}\n \t\ttrace2_region_leave(\"merge\", \"write_auto_merge\", opt->repo);\n+\n+\t\ttrace2_region_enter(\"merge\", \"write_merge_labels\", opt->repo);\n+\t\topt->priv = result->priv;\n+\t\twrite_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1],\n+\t\t\t\t   opt->priv->labels[2]);\n+\t\topt->priv = NULL;\n+\t\ttrace2_region_leave(\"merge\", \"write_merge_labels\", opt->repo);\n \t}\n \tif (display_update_msgs)\n \t\tmerge_display_update_messages(opt, /* detailed */ 0, result);\n@@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt,\n \t * to move it.\n \t */\n \tassert(opt->priv && !result->priv);\n+\tif (!result->clean) {\n+\t\topt->priv->labels[0] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->ancestor);\n+\t\topt->priv->labels[1] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch1);\n+\t\topt->priv->labels[2] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch2);\n+\t}\n \tresult->priv = opt->priv;\n \tresult->_properly_initialized = RESULT_INITIALIZED;\n \topt->priv = NULL;\ndiff --git a/merge.c b/merge.c\nindex 0f5e823e63..95495ae1ba 100644\n--- a/merge.c\n+++ b/merge.c\n@@ -8,6 +8,7 @@\n #include \"merge.h\"\n #include \"commit.h\"\n #include \"repository.h\"\n+#include \"path.h\"\n #include \"run-command.h\"\n #include \"resolve-undo.h\"\n #include \"tree.h\"\n@@ -111,3 +112,65 @@ int checkout_fast_forward(struct repository *r,\n \t\treturn error(_(\"unable to write new index file\"));\n \treturn 0;\n }\n+\n+int write_merge_labels(struct repository *r, const char *base,\n+\t\t\t  const char *ours, const char *theirs)\n+{\n+\tFILE *f = fopen_or_warn(git_path_merge_labels(r), \"w\");\n+\n+\tif (!f)\n+\t\treturn -1;\n+\n+\tfprintf(f, \"%s\\n%s\\n%s\\n\", base, ours, theirs);\n+\tif (fclose(f))\n+\t\treturn error_errno(\"could not write '%s'\",\n+\t\t\t\t   git_path_merge_labels(r));\n+\n+\treturn 0;\n+}\n+\n+static int parse_merge_label_line(const char **p, char **line)\n+{\n+\tconst char *eol = strchr(*p, '\\n');\n+\n+\tif (!eol)\n+\t\treturn -1;\n+\n+\t*line = xmemdupz(*p, eol - *p);\n+\t*p = eol + 1;\n+\n+\treturn 0;\n+}\n+\n+int read_merge_labels(struct repository *r,\n+\t\t      char **pbase, char** pours, char** ptheirs)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *p;\n+\tchar *base = NULL, *ours = NULL, *theirs = NULL;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0)\n+\t\treturn -1;\n+\n+\tp = buf.buf;\n+\tif (parse_merge_label_line(&p, &base))\n+\t\tgoto out;\n+\tif (parse_merge_label_line(&p, &ours))\n+\t\tgoto out;\n+\tif (parse_merge_label_line(&p, &theirs))\n+\t\tgoto out;\n+\tret = 0;\n+\t*pbase = base;\n+\t*pours = ours;\n+\t*ptheirs = theirs;\n+out:\n+\tif (ret) {\n+\t\tfree(base);\n+\t\tfree(ours);\n+\t\tfree(theirs);\n+\t}\n+\tstrbuf_release(&buf);\n+\n+\treturn ret;\n+}\ndiff --git a/merge.h b/merge.h\nindex 21ac7ef2f1..0772737a87 100644\n--- a/merge.h\n+++ b/merge.h\n@@ -13,5 +13,9 @@ int checkout_fast_forward(struct repository *r,\n \t\t\t  const struct object_id *from,\n \t\t\t  const struct object_id *to,\n \t\t\t  int overwrite_ignore);\n+int write_merge_labels(struct repository *r,\n+\t\t       const char *base, const char *ours, const char *theirs);\n+int read_merge_labels(struct repository *r,\n+\t\t      char **base, char **ours, char **theirs);\n \n #endif /* MERGE_H */\ndiff --git a/path.c b/path.c\nindex c3a709a928..7965762602 100644\n--- a/path.c\n+++ b/path.c\n@@ -1655,3 +1655,4 @@ REPO_GIT_PATH_FUNC(merge_mode, \"MERGE_MODE\")\n REPO_GIT_PATH_FUNC(merge_head, \"MERGE_HEAD\")\n REPO_GIT_PATH_FUNC(fetch_head, \"FETCH_HEAD\")\n REPO_GIT_PATH_FUNC(shallow, \"shallow\")\n+REPO_GIT_PATH_FUNC(merge_labels, \"MERGE_LABELS\")\ndiff --git a/path.h b/path.h\nindex 7e7408dd05..8cd12ccfde 100644\n--- a/path.h\n+++ b/path.h\n@@ -142,6 +142,7 @@ const char *git_path_merge_mode(struct repository *r);\n const char *git_path_merge_head(struct repository *r);\n const char *git_path_fetch_head(struct repository *r);\n const char *git_path_shallow(struct repository *r);\n+const char *git_path_merge_labels(struct repository *r);\n \n int ends_with_path_components(const char *path, const char *components);\n \ndiff --git a/repository.c b/repository.c\nindex b857e1c580..210fb819b0 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -367,6 +367,7 @@ static void repo_clear_path_cache(struct repo_path_cache *cache)\n \tFREE_AND_NULL(cache->merge_head);\n \tFREE_AND_NULL(cache->fetch_head);\n \tFREE_AND_NULL(cache->shallow);\n+\tFREE_AND_NULL(cache->merge_labels);\n }\n \n void repo_clear(struct repository *repo)\ndiff --git a/repository.h b/repository.h\nindex 11f5c2ed10..91b1f57db7 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -36,6 +36,7 @@ struct repo_path_cache {\n \tchar *merge_head;\n \tchar *fetch_head;\n \tchar *shallow;\n+\tchar *merge_labels;\n };\n \n struct repository {\ndiff --git a/sequencer.c b/sequencer.c\nindex e25ef5eb61..0710aa6400 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -5145,6 +5145,7 @@ static int pick_commits(struct repository *r,\n \tunlink(rebase_path_stopped_sha());\n \tunlink(rebase_path_amend());\n \tunlink(rebase_path_patch());\n+\tunlink(git_path_merge_labels(r));\n \n \twhile (todo_list->current < todo_list->nr) {\n \t\tstruct todo_item *item = todo_list->items + todo_list->current;\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 9ea9462914..7d0dcf8c8b 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -183,6 +183,27 @@ test_expect_success 'format of merge conflict from checkout -m' '\n \td\n \t>>>>>>> local\n \tEOF\n+\ttest_cmp expect two &&\n+\n+\ttest_path_is_file .git/MERGE_LABELS &&\n+\n+\tgit checkout --conflict=diff3 two &&\n+\tcat >expect <<-\\EOF &&\n+\t<<<<<<< simple\n+\ta\n+\tc\n+\te\n+\t||||||| main\n+\ta\n+\tb\n+\tc\n+\td\n+\te\n+\t=======\n+\tb\n+\td\n+\t>>>>>>> local\n+\tEOF\n \ttest_cmp expect two\n '\n \n-- \n2.56.0.rc2.84.gaf8b4f0d381\n\n"},{"id":"553728","messageId":"xmqq1paad71z.fsf@gitster.g","threadId":"66426","inReplyTo":"fdaf3da993366878b51bd0b2a950888710cafb8a.1790761727.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH 2/2] merge: remember conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-30T16:42:48Z","receivedAt":"2026-09-30T16:42:50Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>\n> When recreating merge conflicts with \"git checkout -m <path>\" the\n> original conflict labels are lost. For commands like \"git merge\" and\n> \"git cherry-pick\" we could use the presence of the related root\n> ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the\n> labels. However, if the conflicts are from \"git stash pop\" or \"git\n> checkout -m <branch>\", then there is no ref to deduce the labels from. To\n> ensure the labels are always available, the merge machinery is updated to\n> write \".git/MERGE_LABELS\" when it updates the worktree and\n> there are conflicts. The labels are then read from that file by \"git\n> checkout -m <path>\" when recreating the conflicts.\n>\n> As \"git checkout -m <branch>\" calls remove_branch_state() which\n> ordinarily removes the labels file, we need to pass a flag down\n> to optionally prevent that so that the labels are available for any\n> subsequent \"git checkout -m <path>\". Note that merge_switch_to_result()\n> we assign \"result->priv\" to \"opt->priv\" and later clear \"opt->priv\" in\n> order to get a pointer to the private struct as result->priv is void*.\n>\n> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n> ---\n>  branch.c           | 11 ++++++--\n>  branch.h           |  1 +\n>  builtin/checkout.c | 24 +++++++++++++++---\n>  builtin/commit.c   |  1 +\n>  merge-ort.c        | 19 ++++++++++++++\n>  merge.c            | 63 ++++++++++++++++++++++++++++++++++++++++++++++\n>  merge.h            |  4 +++\n>  path.c             |  1 +\n>  path.h             |  1 +\n>  repository.c       |  1 +\n>  repository.h       |  1 +\n>  sequencer.c        |  1 +\n>  t/t7201-co.sh      | 21 ++++++++++++++++\n>  13 files changed, 143 insertions(+), 6 deletions(-)\n\nWhere do we talk about MERGE_HEAD and CHERRY_PICK_HEAD in the\ncurrent documentation set?  Do we want to mention MERGE_LABELS\nalongside them?\n\n> +\n> +int write_merge_labels(struct repository *r, const char *base,\n> +\t\t\t  const char *ours, const char *theirs)\n> +{\n> +\tFILE *f = fopen_or_warn(git_path_merge_labels(r), \"w\");\n> +\n> +\tif (!f)\n> +\t\treturn -1;\n> +\n> +\tfprintf(f, \"%s\\n%s\\n%s\\n\", base, ours, theirs);\n> +\tif (fclose(f))\n> +\t\treturn error_errno(\"could not write '%s'\",\n> +\t\t\t\t   git_path_merge_labels(r));\n> +\n> +\treturn 0;\n> +}\n> +\n\nWe write three items, one per line, delimited by LF.  As this goes\nthrough stdio, wouldn't Windows write CRLF-delimited lines?  I guess\nif we read this back through stdio, that will cancel out and we get\nthe LF-delimited lines back?\n\nWait.  Do we want to read this file via stdio, one line at a time,\nusing three calls to fgets()?  No, we do not give a strict upper\nlimit to the length of these labels.  So if we read with\nstrbuf_read_line() or something, we would be safe, I guess, but alas\nthere is no such helper function X-<.\n\n> +static int parse_merge_label_line(const char **p, char **line)\n> +{\n> +\tconst char *eol = strchr(*p, '\\n');\n> +\n> +\tif (!eol)\n> +\t\treturn -1;\n> +\n> +\t*line = xmemdupz(*p, eol - *p);\n> +\t*p = eol + 1;\n> +\n> +\treturn 0;\n> +}\n\n\nOK, this reads one line at a time from the file contents already\nfully read by strbuf_read_file(), as seen below.\n\nWhich means that the CRLF fprintf() may have written in\nwrite_merge_labels() will come back to this function, and our 'ours'\nmay become 'ours\\015' after stripping only the LF at the end?\n\n> +int read_merge_labels(struct repository *r,\n> +\t\t      char **pbase, char** pours, char** ptheirs)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tconst char *p;\n> +\tchar *base = NULL, *ours = NULL, *theirs = NULL;\n> +\tint ret = -1;\n> +\n> +\tif (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0)\n> +\t\treturn -1;\n\nCan strbuf_read_file() fill '.buf' halfway and return a failure, or\ndoes it ensure that it frees '.buf' before returning failure?  Just\ndouble-checking.\n\n    ... goes and checks ...\n\nstrbuf_read_file() calls strbuf_read(), which calls read_in_full() to\nfill a sufficiently large buffer, and a failure from there results in\nstrbuf_release() or strbuf_setlen() resetting back to the '.len'\nbefore strbuf_read() was called (i.e., 0 in this case), so we do not\nleak anything on the error path and this code is safe, I think.\n\n> +\n> +\tp = buf.buf;\n> +\tif (parse_merge_label_line(&p, &base))\n> +\t\tgoto out;\n> +\tif (parse_merge_label_line(&p, &ours))\n> +\t\tgoto out;\n> +\tif (parse_merge_label_line(&p, &theirs))\n> +\t\tgoto out;\n\nOK, we read three things.\n\n> +\tret = 0;\n> +\t*pbase = base;\n> +\t*pours = ours;\n> +\t*ptheirs = theirs;\n> +out:\n> +\tif (ret) {\n> +\t\tfree(base);\n> +\t\tfree(ours);\n> +\t\tfree(theirs);\n> +\t}\n> +\tstrbuf_release(&buf);\n\n\nOK, so the contract is that we will not touch p{base,ours,theirs}\nif we return failure, and we will not leak anything when doing so.\n\nWhich is very sensible.\n\n> +\treturn ret;\n> +}\n\nLooking good so far, modulo a small worry about writing via stdio\nand reading back while bypassing stdio.  But perhaps CRLF is so\nannoying that the compat/mingw layer takes care of all of the above\nworries by passing the 'binary' bit down to the msvcrt/ucrt layer,\nin which case we should not have to worry about it.  I dunno.\n\nThanks for working on these patches.\n"},{"id":"553753","messageId":"223c99ea-64d9-46da-9631-ed8035f1a062@kdbg.org","threadId":"66426","inReplyTo":"cover.1790761727.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH 0/2] checkout -m: recreate conflict labels","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-09-30T20:24:33Z","receivedAt":"2026-09-30T20:24:51Z","isPatch":true,"body":"Am 30.09.26 um 11:48 schrieb Phillip Wood:\n> When \"git checkout -m <path>\" recreates a merge conflict, it uses\n> the labels \"base\", \"ours\", \"theirs\", rather than the labels used by\n> the original merge. This short series teaches the ort machinery to\n> write the labels to \".git/MERGE_LABELS\" when it switches to a merge\n> result containing conflicts, so that \"git checkout -m\" can then read\n> that file and use the same labels.\n\nWould an index extension not be a better place to store auxiliary\ninformation about merges?\n\n-- Hannes\n\n"},{"id":"553756","messageId":"xmqqmrsy8ocd.fsf@gitster.g","threadId":"66426","inReplyTo":"223c99ea-64d9-46da-9631-ed8035f1a062@kdbg.org","subject":"Re: [PATCH 0/2] checkout -m: recreate conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-30T20:40:34Z","receivedAt":"2026-09-30T20:40:36Z","isPatch":true,"body":"Johannes Sixt <j6t@kdbg.org> writes:\n\n> Am 30.09.26 um 11:48 schrieb Phillip Wood:\n>> When \"git checkout -m <path>\" recreates a merge conflict, it uses\n>> the labels \"base\", \"ours\", \"theirs\", rather than the labels used by\n>> the original merge. This short series teaches the ort machinery to\n>> write the labels to \".git/MERGE_LABELS\" when it switches to a merge\n>> result containing conflicts, so that \"git checkout -m\" can then read\n>> that file and use the same labels.\n>\n> Would an index extension not be a better place to store auxiliary\n> information about merges?\n\nWow.  MERGE_HEAD, CHERRY_PICK_HEAD, and all others replaced with\nindex extensions?  That would unclutter $GIT_DIR/ quite a lot (for\nsome reason, I find ORIG_HEAD is a bit of eyesore).  It makes the\ninformation less accessible, so I am not sure how I feel about the\nproposal, but it is an interesting thought.\n\nThanks.\n\n"},{"id":"553762","messageId":"35417989-d31e-470c-b366-f6632246112b@kdbg.org","threadId":"66426","inReplyTo":"xmqqmrsy8ocd.fsf@gitster.g","subject":"Re: [PATCH 0/2] checkout -m: recreate conflict labels","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-09-30T21:20:22Z","receivedAt":"2026-09-30T21:20:26Z","isPatch":true,"body":"Am 30.09.26 um 22:40 schrieb Junio C Hamano:\n> Johannes Sixt <j6t@kdbg.org> writes:\n> \n>> Am 30.09.26 um 11:48 schrieb Phillip Wood:\n>>> When \"git checkout -m <path>\" recreates a merge conflict, it uses\n>>> the labels \"base\", \"ours\", \"theirs\", rather than the labels used by\n>>> the original merge. This short series teaches the ort machinery to\n>>> write the labels to \".git/MERGE_LABELS\" when it switches to a merge\n>>> result containing conflicts, so that \"git checkout -m\" can then read\n>>> that file and use the same labels.\n>>\n>> Would an index extension not be a better place to store auxiliary\n>> information about merges?\n> \n> Wow.  MERGE_HEAD, CHERRY_PICK_HEAD, and all others replaced with\n> index extensions?\n\n\nAbsolutely not. IIUC, MERGE_LABELS should not be a pseudo ref, but a\nfile carrying auxiliary information.\n\n\n>  That would unclutter $GIT_DIR/ quite a lot (for\n> some reason, I find ORIG_HEAD is a bit of eyesore).  It makes the\n> information less accessible, so I am not sure how I feel about the\n> proposal, but it is an interesting thought.\n\nI don't know how accessible data in an index extension is. But if it's\nprohibitively difficult to access, then the idea is dead on arrival.\n\n-- Hannes\n\n"},{"id":"553818","messageId":"f5397a5c-3482-4207-9501-fec431fa34a0@gmail.com","threadId":"66426","inReplyTo":"223c99ea-64d9-46da-9631-ed8035f1a062@kdbg.org","subject":"Re: [PATCH 0/2] checkout -m: recreate conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-01T08:45:34Z","receivedAt":"2026-10-01T08:45:43Z","isPatch":true,"body":"On 30/09/2026 21:24, Johannes Sixt wrote:\n> Am 30.09.26 um 11:48 schrieb Phillip Wood:\n>> When \"git checkout -m <path>\" recreates a merge conflict, it uses\n>> the labels \"base\", \"ours\", \"theirs\", rather than the labels used by\n>> the original merge. This short series teaches the ort machinery to\n>> write the labels to \".git/MERGE_LABELS\" when it switches to a merge\n>> result containing conflicts, so that \"git checkout -m\" can then read\n>> that file and use the same labels.\n> \n> Would an index extension not be a better place to store auxiliary\n> information about merges?\nI did briefly consider that, but it makes it much harder for other merge \nstrategies such as git-merge-octopus (which I should probably update to \nwrite MERGE_LABELS) to store the labels. We already have MERGE_MODE, \nMERGE_RR and MERGE_MSG storing various bits of merge-related information \nso this series just follows existing practice.\n\nThanks\n\nPhillip\n"},{"id":"553820","messageId":"6e028692-447c-4a21-a9bb-739e42f1c9aa@gmail.com","threadId":"66426","inReplyTo":"xmqq1paad71z.fsf@gitster.g","subject":"Re: [PATCH 2/2] merge: remember conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-01T08:54:52Z","receivedAt":"2026-10-01T08:55:00Z","isPatch":true,"body":"Hi Junio\n\nOn 30/09/2026 17:42, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>> From: Phillip Wood <phillip.wood@dunelm.org.uk>\n>>\n>> When recreating merge conflicts with \"git checkout -m <path>\" the\n>> original conflict labels are lost. For commands like \"git merge\" and\n>> \"git cherry-pick\" we could use the presence of the related root\n>> ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the\n>> labels. However, if the conflicts are from \"git stash pop\" or \"git\n>> checkout -m <branch>\", then there is no ref to deduce the labels from. To\n>> ensure the labels are always available, the merge machinery is updated to\n>> write \".git/MERGE_LABELS\" when it updates the worktree and\n>> there are conflicts. The labels are then read from that file by \"git\n>> checkout -m <path>\" when recreating the conflicts.\n>>\n>> As \"git checkout -m <branch>\" calls remove_branch_state() which\n>> ordinarily removes the labels file, we need to pass a flag down\n>> to optionally prevent that so that the labels are available for any\n>> subsequent \"git checkout -m <path>\". Note that merge_switch_to_result()\n>> we assign \"result->priv\" to \"opt->priv\" and later clear \"opt->priv\" in\n>> order to get a pointer to the private struct as result->priv is void*.\n>>\n>> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n>> ---\n>>   branch.c           | 11 ++++++--\n>>   branch.h           |  1 +\n>>   builtin/checkout.c | 24 +++++++++++++++---\n>>   builtin/commit.c   |  1 +\n>>   merge-ort.c        | 19 ++++++++++++++\n>>   merge.c            | 63 ++++++++++++++++++++++++++++++++++++++++++++++\n>>   merge.h            |  4 +++\n>>   path.c             |  1 +\n>>   path.h             |  1 +\n>>   repository.c       |  1 +\n>>   repository.h       |  1 +\n>>   sequencer.c        |  1 +\n>>   t/t7201-co.sh      | 21 ++++++++++++++++\n>>   13 files changed, 143 insertions(+), 6 deletions(-)\n> \n> Where do we talk about MERGE_HEAD and CHERRY_PICK_HEAD in the\n> current documentation set?  Do we want to mention MERGE_LABELS\n> alongside them?\n\nWe talk about those in gitrevisions, the \"refs\" section of gitglossary \nand in the merge documentation. As this is not a ref I don't think it \nfits with MERGE_HEAD, it is more like MERGE_MSG, or MERGE_MODE. The \nmerge man page mentions MERGE_MSG in passing but never explicitly says \nwhat it contains and MERGE_MODE is undocumented as far as I can see. We \nwould perhaps benefit from documenting the common files like \nCOMMIT_EDITMSG, MERGE_MSG, SQUASH_MSG, MERGE_HEAD, FETCH_HEAD and \nMERGE_LABELS somewhere in gitrepository briefly explaining what they \ncontain and how they are used as a separate series.\n\n>> +static int parse_merge_label_line(const char **p, char **line)\n>> +{\n>> +\tconst char *eol = strchr(*p, '\\n');\n>> +\n>> +\tif (!eol)\n>> +\t\treturn -1;\n>> +\n>> +\t*line = xmemdupz(*p, eol - *p);\n>> +\t*p = eol + 1;\n>> +\n>> +\treturn 0;\n>> +}\n> \n> \n> OK, this reads one line at a time from the file contents already\n> fully read by strbuf_read_file(), as seen below.\n> \n> Which means that the CRLF fprintf() may have written in\n> write_merge_labels() will come back to this function, and our 'ours'\n> may become 'ours\\015' after stripping only the LF at the end?\n\nThat's a good point, I've changed it to use strbuf_getline() instead.\n\n> Thanks for working on these patches.\n\nThanks for reviewing them, I'll send a re-roll in a couple of days\n\nPhillip\n\n"},{"id":"553851","messageId":"xmqqy0ch5one.fsf@gitster.g","threadId":"66426","inReplyTo":"6e028692-447c-4a21-a9bb-739e42f1c9aa@gmail.com","subject":"Re: [PATCH 2/2] merge: remember conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-01T17:14:29Z","receivedAt":"2026-10-01T17:14:33Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> We\n> would perhaps benefit from documenting the common files like \n> COMMIT_EDITMSG, MERGE_MSG, SQUASH_MSG, MERGE_HEAD, FETCH_HEAD and \n> MERGE_LABELS somewhere in gitrepository briefly explaining what they \n> contain and how they are used as a separate series.\n\nSounds good.\n\nThanks.\n"},{"id":"554175","messageId":"86ef0f848a35c66b2d68f96c5f307a6c80f74c78.1791206658.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":"cover.1791206658.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v2 1/2] remove_branch_state: convert boolean argument to flags","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-05T13:24:48Z","receivedAt":"2026-10-05T13:25:24Z","isPatch":true,"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nConvert the \"verbose\" boolean argument to a flag so that we can add\nmore flags in a future commit.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n branch.c           | 4 ++--\n branch.h           | 3 ++-\n builtin/checkout.c | 6 +++++-\n 3 files changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/branch.c b/branch.c\nindex 22f4f46b96..8bc7a395a7 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -871,9 +871,9 @@ void remove_merge_branch_state(struct repository *r)\n \tsave_autostash_ref(r, \"MERGE_AUTOSTASH\");\n }\n \n-void remove_branch_state(struct repository *r, int verbose)\n+void remove_branch_state(struct repository *r, unsigned flags)\n {\n-\tsequencer_post_commit_cleanup(r, verbose);\n+\tsequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE);\n \tunlink(git_path_squash_msg(r));\n \tremove_merge_branch_state(r);\n }\ndiff --git a/branch.h b/branch.h\nindex e9b1f7b37d..42d1b12918 100644\n--- a/branch.h\n+++ b/branch.h\n@@ -127,6 +127,7 @@ int validate_branchname(const char *name, struct strbuf *ref);\n  */\n int validate_new_branchname(const char *name, struct strbuf *ref, int force);\n \n+#define REMOVE_BRANCH_STATE_VERBOSE (1u << 0)\n /*\n  * Remove information about the merge state on the current\n  * branch. (E.g., MERGE_HEAD)\n@@ -137,7 +138,7 @@ void remove_merge_branch_state(struct repository *r);\n  * Remove information about the state of working on the current\n  * branch. (E.g., MERGE_HEAD)\n  */\n-void remove_branch_state(struct repository *r, int verbose);\n+void remove_branch_state(struct repository *r, unsigned flags);\n \n /*\n  * Configure local branch \"local\" as downstream to branch \"remote\"\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex c0f0d2c700..bdd2d816b6 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -950,6 +950,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *old_desc, *reflog_msg;\n+\tunsigned flags = 0;\n+\n \tif (opts->new_branch) {\n \t\tif (opts->new_orphan_branch) {\n \t\t\tenum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;\n@@ -1044,7 +1046,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\t\t\t\t   old_branch_info->path);\n \t\t}\n \t}\n-\tremove_branch_state(the_repository, !opts->quiet);\n+\tif (!opts->quiet)\n+\t\tflags |= REMOVE_BRANCH_STATE_VERBOSE;\n+\tremove_branch_state(the_repository, flags);\n \tstrbuf_release(&msg);\n \tif (!opts->quiet &&\n \t    !opts->force_detach &&\n-- \n2.56.0.134.g299a3c16181\n\n"},{"id":"554173","messageId":"cover.1791206658.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":"cover.1790761727.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v2 0/2] checkout -m: recreate conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-05T13:24:47Z","receivedAt":"2026-10-05T13:25:26Z","isPatch":true,"body":"When \"git checkout -m <path>\" recreates a merge conflict, it uses\nthe labels \"base\", \"ours\", \"theirs\", rather than the labels used by\nthe original merge. This short series teaches the ort machinery to\nwrite the labels to \".git/MERGE_LABELS\" when it switches to a merge\nresult containing conflicts, so that \"git checkout -m\" can then read\nthat file and use the same labels.\n\nAs \"git checkout -m\" is recreating the original conflict I wonder\nif we should remember the conflict style as well so that\n\n    git -c merge.conflictStyle=diff3 git merge topic\n    git checkout -m <unmerged-path>\n\nwould recreate diff3 style conflicts, instead of using the default\nconfig. I cannot decide if that would be convenient or confusing and\nam interested to hear what others think.\n\nChanges since V1:\n\n - use strbuf_getline() rather than strbuf_read_file() to read labels\n   so that the newline handling of the reading and writing sides match.\n\nNB \".git/MERGE_LABELS\" is still undocumented - I'm hoping to find time\nto add some documentation for all the MERGE_* files in a future series.\nJohannes suggested using an index extension to store the labels,\nbut as we already have MERGE_MODE, MERGE_RR and MERGE_MSG I think it\nis easier just to add another file.\n\nbase-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7\nPublished-As: https://github.com/phillipwood/git/releases/tag/pw%2Fconflict-labels%2Fv2\nView-Changes-At: https://github.com/phillipwood/git/compare/3bc034112...18bdf7df4\nFetch-It-Via: git fetch https://github.com/phillipwood/git pw/conflict-labels/v2\n\n\nPhillip Wood (2):\n  remove_branch_state: convert boolean argument to flags\n  merge: remember conflict labels\n\n branch.c           | 17 ++++++++----\n branch.h           |  4 ++-\n builtin/checkout.c | 30 ++++++++++++++++++----\n builtin/commit.c   |  1 +\n merge-ort.c        | 19 ++++++++++++++\n merge.c            | 64 ++++++++++++++++++++++++++++++++++++++++++++++\n merge.h            |  4 +++\n path.c             |  1 +\n path.h             |  1 +\n repository.c       |  1 +\n repository.h       |  1 +\n sequencer.c        |  1 +\n t/t7201-co.sh      | 21 +++++++++++++++\n 13 files changed, 154 insertions(+), 11 deletions(-)\n\nRange-diff against v1:\n1:  86ef0f848a = 1:  86ef0f848a remove_branch_state: convert boolean argument to flags\n2:  fdaf3da993 ! 2:  18bdf7df49 merge: remember conflict labels\n    @@ merge.c: int checkout_fast_forward(struct repository *r,\n     +\treturn 0;\n     +}\n     +\n    -+static int parse_merge_label_line(const char **p, char **line)\n    ++static char *parse_merge_label_line(struct strbuf *buf, FILE *fp)\n     +{\n    -+\tconst char *eol = strchr(*p, '\\n');\n    -+\n    -+\tif (!eol)\n    -+\t\treturn -1;\n    -+\n    -+\t*line = xmemdupz(*p, eol - *p);\n    -+\t*p = eol + 1;\n    -+\n    -+\treturn 0;\n    ++\tif (strbuf_getline(buf, fp) == EOF)\n    ++\t\treturn NULL;\n    ++\n    ++\treturn xmemdupz(buf->buf, buf->len);\n     +}\n     +\n     +int read_merge_labels(struct repository *r,\n     +\t\t      char **pbase, char** pours, char** ptheirs)\n     +{\n     +\tstruct strbuf buf = STRBUF_INIT;\n    -+\tconst char *p;\n     +\tchar *base = NULL, *ours = NULL, *theirs = NULL;\n     +\tint ret = -1;\n    ++\tFILE *fp = fopen(git_path_merge_labels(r), \"r\");\n     +\n    -+\tif (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0)\n    ++\tif (!fp)\n     +\t\treturn -1;\n     +\n    -+\tp = buf.buf;\n    -+\tif (parse_merge_label_line(&p, &base))\n    -+\t\tgoto out;\n    -+\tif (parse_merge_label_line(&p, &ours))\n    -+\t\tgoto out;\n    -+\tif (parse_merge_label_line(&p, &theirs))\n    -+\t\tgoto out;\n    ++\tbase = parse_merge_label_line(&buf, fp);\n    ++\tif (!base)\n    ++\t\tgoto out;\n    ++\n    ++\tours = parse_merge_label_line(&buf, fp);\n    ++\tif (!ours)\n    ++\t\tgoto out;\n    ++\n    ++\ttheirs = parse_merge_label_line(&buf, fp);\n    ++\tif (!theirs)\n    ++\t\tgoto out;\n    ++\n     +\tret = 0;\n     +\t*pbase = base;\n     +\t*pours = ours;\n    @@ merge.c: int checkout_fast_forward(struct repository *r,\n     +\t\tfree(ours);\n     +\t\tfree(theirs);\n     +\t}\n    ++\tfclose(fp);\n     +\tstrbuf_release(&buf);\n     +\n     +\treturn ret;\n-- \n2.56.0.134.g299a3c16181\n\n"},{"id":"554174","messageId":"18bdf7df49dde2c8e7f73f3b46c656abb6b26293.1791206658.git.phillip.wood@dunelm.org.uk","threadId":"66426","inReplyTo":"cover.1791206658.git.phillip.wood@dunelm.org.uk","subject":"[PATCH v2 2/2] merge: remember conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-05T13:24:49Z","receivedAt":"2026-10-05T13:25:26Z","isPatch":true,"body":"From: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nWhen recreating merge conflicts with \"git checkout -m <path>\" the\noriginal conflict labels are lost. For commands like \"git merge\" and\n\"git cherry-pick\" we could use the presence of the related root\nref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the\nlabels. However, if the conflicts are from \"git stash pop\" or \"git\ncheckout -m <branch>\", then there is no ref to deduce the labels from. To\nensure the labels are always available, the merge machinery is updated to\nwrite \".git/MERGE_LABELS\" when it updates the worktree and\nthere are conflicts. The labels are then read from that file by \"git\ncheckout -m <path>\" when recreating the conflicts.\n\nAs \"git checkout -m <branch>\" calls remove_branch_state() which\nordinarily removes the labels file, we need to pass a flag down\nto optionally prevent that so that the labels are available for any\nsubsequent \"git checkout -m <path>\". Note that merge_switch_to_result()\nwe assign \"result->priv\" to \"opt->priv\" and later clear \"opt->priv\" in\norder to get a pointer to the private struct as result->priv is void*.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n branch.c           | 11 ++++++--\n branch.h           |  1 +\n builtin/checkout.c | 24 ++++++++++++++---\n builtin/commit.c   |  1 +\n merge-ort.c        | 19 ++++++++++++++\n merge.c            | 64 ++++++++++++++++++++++++++++++++++++++++++++++\n merge.h            |  4 +++\n path.c             |  1 +\n path.h             |  1 +\n repository.c       |  1 +\n repository.h       |  1 +\n sequencer.c        |  1 +\n t/t7201-co.sh      | 21 +++++++++++++++\n 13 files changed, 144 insertions(+), 6 deletions(-)\n\ndiff --git a/branch.c b/branch.c\nindex 8bc7a395a7..5bb1c28915 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -860,9 +860,11 @@ void create_branches_recursively(struct repository *r, const char *name,\n \tfree(branch_point);\n }\n \n-void remove_merge_branch_state(struct repository *r)\n+static void do_remove_merge_branch_state(struct repository *r, unsigned flags)\n {\n \tunlink(git_path_merge_head(r));\n+\tif (!(flags & REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS))\n+\t\tunlink(git_path_merge_labels(r));\n \tunlink(git_path_merge_rr(r));\n \tunlink(git_path_merge_msg(r));\n \tunlink(git_path_merge_mode(r));\n@@ -871,11 +873,16 @@ void remove_merge_branch_state(struct repository *r)\n \tsave_autostash_ref(r, \"MERGE_AUTOSTASH\");\n }\n \n+void remove_merge_branch_state(struct repository *r)\n+{\n+\tdo_remove_merge_branch_state(r, 0);\n+}\n+\n void remove_branch_state(struct repository *r, unsigned flags)\n {\n \tsequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE);\n \tunlink(git_path_squash_msg(r));\n-\tremove_merge_branch_state(r);\n+\tdo_remove_merge_branch_state(r, flags);\n }\n \n void die_if_checked_out(const char *branch, int ignore_current_worktree)\ndiff --git a/branch.h b/branch.h\nindex 42d1b12918..95b2431f24 100644\n--- a/branch.h\n+++ b/branch.h\n@@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref);\n int validate_new_branchname(const char *name, struct strbuf *ref, int force);\n \n #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0)\n+#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1)\n /*\n  * Remove information about the merge state on the current\n  * branch. (E.g., MERGE_HEAD)\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex bdd2d816b6..295fe0e9fa 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -15,6 +15,7 @@\n #include \"hex.h\"\n #include \"hook.h\"\n #include \"merge-ll.h\"\n+#include \"merge.h\"\n #include \"lockfile.h\"\n #include \"mem-pool.h\"\n #include \"object-file.h\"\n@@ -317,6 +318,7 @@ static int checkout_merged(int pos, const struct checkout *state,\n \tstruct cache_entry *ce = the_repository->index->cache[pos];\n \tconst char *path = ce->name;\n \tmmfile_t ancestor, ours, theirs;\n+\tchar *base_label, *ours_label, *theirs_label;\n \tenum ll_merge_result merge_status;\n \tint status;\n \tstruct object_id oid;\n@@ -347,10 +349,19 @@ static int checkout_merged(int pos, const struct checkout *state,\n \n \trepo_config_get_bool(the_repository, \"merge.renormalize\", &renormalize);\n \tll_opts.renormalize = renormalize;\n+\tif (read_merge_labels(the_repository, &base_label, &ours_label,\n+\t\t\t      &theirs_label)) {\n+\t\tbase_label = xstrdup(\"base\");\n+\t\tours_label = xstrdup(\"ours\");\n+\t\ttheirs_label = xstrdup(\"theirs\");\n+\t}\n \tll_opts.conflict_style = conflict_style;\n-\tmerge_status = ll_merge(&result_buf, path, &ancestor, \"base\",\n-\t\t\t\t&ours, \"ours\", &theirs, \"theirs\",\n+\tmerge_status = ll_merge(&result_buf, path, &ancestor, base_label,\n+\t\t\t\t&ours, ours_label, &theirs, theirs_label,\n \t\t\t\tstate->istate, &ll_opts);\n+\tfree(base_label);\n+\tfree(ours_label);\n+\tfree(theirs_label);\n \tfree(ancestor.ptr);\n \tfree(ours.ptr);\n \tfree(theirs.ptr);\n@@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info)\n \n static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\t\t   struct branch_info *old_branch_info,\n-\t\t\t\t   struct branch_info *new_branch_info)\n+\t\t\t\t   struct branch_info *new_branch_info,\n+\t\t\t\t   bool merge_conflicts)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *old_desc, *reflog_msg;\n@@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t}\n \tif (!opts->quiet)\n \t\tflags |= REMOVE_BRANCH_STATE_VERBOSE;\n+\tif (merge_conflicts)\n+\t\tflags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS;\n \tremove_branch_state(the_repository, flags);\n \tstrbuf_release(&msg);\n \tif (!opts->quiet &&\n@@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts,\n \n \tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n \t\tfputc('\\n', stderr);\n-\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n+\n+\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info,\n+\t\t\t       autostash_res == STASH_APPLY_CONFLICT);\n \n \tif (created_autostash) {\n \t\tdiscard_index(the_repository->index);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 205fbd57e3..c374d5e0d5 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1977,6 +1977,7 @@ int cmd_commit(int argc,\n \n \tsequencer_post_commit_cleanup(the_repository, 0);\n \tunlink(git_path_merge_head(the_repository));\n+\tunlink(git_path_merge_labels(the_repository));\n \tunlink(git_path_merge_msg(the_repository));\n \tunlink(git_path_merge_mode(the_repository));\n \tunlink(git_path_squash_msg(the_repository));\ndiff --git a/merge-ort.c b/merge-ort.c\nindex c410a5d353..783e74c6e3 100644\n--- a/merge-ort.c\n+++ b/merge-ort.c\n@@ -35,6 +35,7 @@\n #include \"hex.h\"\n #include \"entry.h\"\n #include \"merge-ll.h\"\n+#include \"merge.h\"\n #include \"match-trees.h\"\n #include \"mem-pool.h\"\n #include \"object-file.h\"\n@@ -418,6 +419,9 @@ struct merge_options_internal {\n \n \t/* field that holds submodule conflict information */\n \tstruct string_list conflicted_submodules;\n+\n+\t/* Copies of the labels used for conflict markers */\n+\tchar *labels[3];\n };\n \n struct conflicted_submodule_item {\n@@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt,\n \t\t\treturn;\n \t\t}\n \t\ttrace2_region_leave(\"merge\", \"write_auto_merge\", opt->repo);\n+\n+\t\ttrace2_region_enter(\"merge\", \"write_merge_labels\", opt->repo);\n+\t\topt->priv = result->priv;\n+\t\twrite_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1],\n+\t\t\t\t   opt->priv->labels[2]);\n+\t\topt->priv = NULL;\n+\t\ttrace2_region_leave(\"merge\", \"write_merge_labels\", opt->repo);\n \t}\n \tif (display_update_msgs)\n \t\tmerge_display_update_messages(opt, /* detailed */ 0, result);\n@@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt,\n \t * to move it.\n \t */\n \tassert(opt->priv && !result->priv);\n+\tif (!result->clean) {\n+\t\topt->priv->labels[0] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->ancestor);\n+\t\topt->priv->labels[1] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch1);\n+\t\topt->priv->labels[2] =\n+\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch2);\n+\t}\n \tresult->priv = opt->priv;\n \tresult->_properly_initialized = RESULT_INITIALIZED;\n \topt->priv = NULL;\ndiff --git a/merge.c b/merge.c\nindex 0f5e823e63..892a78e0c9 100644\n--- a/merge.c\n+++ b/merge.c\n@@ -8,6 +8,7 @@\n #include \"merge.h\"\n #include \"commit.h\"\n #include \"repository.h\"\n+#include \"path.h\"\n #include \"run-command.h\"\n #include \"resolve-undo.h\"\n #include \"tree.h\"\n@@ -111,3 +112,66 @@ int checkout_fast_forward(struct repository *r,\n \t\treturn error(_(\"unable to write new index file\"));\n \treturn 0;\n }\n+\n+int write_merge_labels(struct repository *r, const char *base,\n+\t\t\t  const char *ours, const char *theirs)\n+{\n+\tFILE *f = fopen_or_warn(git_path_merge_labels(r), \"w\");\n+\n+\tif (!f)\n+\t\treturn -1;\n+\n+\tfprintf(f, \"%s\\n%s\\n%s\\n\", base, ours, theirs);\n+\tif (fclose(f))\n+\t\treturn error_errno(\"could not write '%s'\",\n+\t\t\t\t   git_path_merge_labels(r));\n+\n+\treturn 0;\n+}\n+\n+static char *parse_merge_label_line(struct strbuf *buf, FILE *fp)\n+{\n+\tif (strbuf_getline(buf, fp) == EOF)\n+\t\treturn NULL;\n+\n+\treturn xmemdupz(buf->buf, buf->len);\n+}\n+\n+int read_merge_labels(struct repository *r,\n+\t\t      char **pbase, char** pours, char** ptheirs)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar *base = NULL, *ours = NULL, *theirs = NULL;\n+\tint ret = -1;\n+\tFILE *fp = fopen(git_path_merge_labels(r), \"r\");\n+\n+\tif (!fp)\n+\t\treturn -1;\n+\n+\tbase = parse_merge_label_line(&buf, fp);\n+\tif (!base)\n+\t\tgoto out;\n+\n+\tours = parse_merge_label_line(&buf, fp);\n+\tif (!ours)\n+\t\tgoto out;\n+\n+\ttheirs = parse_merge_label_line(&buf, fp);\n+\tif (!theirs)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\t*pbase = base;\n+\t*pours = ours;\n+\t*ptheirs = theirs;\n+out:\n+\tif (ret) {\n+\t\tfree(base);\n+\t\tfree(ours);\n+\t\tfree(theirs);\n+\t}\n+\tfclose(fp);\n+\tstrbuf_release(&buf);\n+\n+\treturn ret;\n+}\ndiff --git a/merge.h b/merge.h\nindex 21ac7ef2f1..0772737a87 100644\n--- a/merge.h\n+++ b/merge.h\n@@ -13,5 +13,9 @@ int checkout_fast_forward(struct repository *r,\n \t\t\t  const struct object_id *from,\n \t\t\t  const struct object_id *to,\n \t\t\t  int overwrite_ignore);\n+int write_merge_labels(struct repository *r,\n+\t\t       const char *base, const char *ours, const char *theirs);\n+int read_merge_labels(struct repository *r,\n+\t\t      char **base, char **ours, char **theirs);\n \n #endif /* MERGE_H */\ndiff --git a/path.c b/path.c\nindex c3a709a928..7965762602 100644\n--- a/path.c\n+++ b/path.c\n@@ -1655,3 +1655,4 @@ REPO_GIT_PATH_FUNC(merge_mode, \"MERGE_MODE\")\n REPO_GIT_PATH_FUNC(merge_head, \"MERGE_HEAD\")\n REPO_GIT_PATH_FUNC(fetch_head, \"FETCH_HEAD\")\n REPO_GIT_PATH_FUNC(shallow, \"shallow\")\n+REPO_GIT_PATH_FUNC(merge_labels, \"MERGE_LABELS\")\ndiff --git a/path.h b/path.h\nindex 7e7408dd05..8cd12ccfde 100644\n--- a/path.h\n+++ b/path.h\n@@ -142,6 +142,7 @@ const char *git_path_merge_mode(struct repository *r);\n const char *git_path_merge_head(struct repository *r);\n const char *git_path_fetch_head(struct repository *r);\n const char *git_path_shallow(struct repository *r);\n+const char *git_path_merge_labels(struct repository *r);\n \n int ends_with_path_components(const char *path, const char *components);\n \ndiff --git a/repository.c b/repository.c\nindex b857e1c580..210fb819b0 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -367,6 +367,7 @@ static void repo_clear_path_cache(struct repo_path_cache *cache)\n \tFREE_AND_NULL(cache->merge_head);\n \tFREE_AND_NULL(cache->fetch_head);\n \tFREE_AND_NULL(cache->shallow);\n+\tFREE_AND_NULL(cache->merge_labels);\n }\n \n void repo_clear(struct repository *repo)\ndiff --git a/repository.h b/repository.h\nindex 11f5c2ed10..91b1f57db7 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -36,6 +36,7 @@ struct repo_path_cache {\n \tchar *merge_head;\n \tchar *fetch_head;\n \tchar *shallow;\n+\tchar *merge_labels;\n };\n \n struct repository {\ndiff --git a/sequencer.c b/sequencer.c\nindex e25ef5eb61..0710aa6400 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -5145,6 +5145,7 @@ static int pick_commits(struct repository *r,\n \tunlink(rebase_path_stopped_sha());\n \tunlink(rebase_path_amend());\n \tunlink(rebase_path_patch());\n+\tunlink(git_path_merge_labels(r));\n \n \twhile (todo_list->current < todo_list->nr) {\n \t\tstruct todo_item *item = todo_list->items + todo_list->current;\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 9ea9462914..7d0dcf8c8b 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -183,6 +183,27 @@ test_expect_success 'format of merge conflict from checkout -m' '\n \td\n \t>>>>>>> local\n \tEOF\n+\ttest_cmp expect two &&\n+\n+\ttest_path_is_file .git/MERGE_LABELS &&\n+\n+\tgit checkout --conflict=diff3 two &&\n+\tcat >expect <<-\\EOF &&\n+\t<<<<<<< simple\n+\ta\n+\tc\n+\te\n+\t||||||| main\n+\ta\n+\tb\n+\tc\n+\td\n+\te\n+\t=======\n+\tb\n+\td\n+\t>>>>>>> local\n+\tEOF\n \ttest_cmp expect two\n '\n \n-- \n2.56.0.134.g299a3c16181\n\n"},{"id":"554182","messageId":"b2f86941-6ae2-4105-9284-e5e4d530b965@kdbg.org","threadId":"66426","inReplyTo":"cover.1791206658.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2 0/2] checkout -m: recreate conflict labels","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-10-05T14:48:43Z","receivedAt":"2026-10-05T14:48:43Z","isPatch":true,"body":"Am 05.10.26 um 15:24 schrieb Phillip Wood:\n> As \"git checkout -m\" is recreating the original conflict I wonder\n> if we should remember the conflict style as well so that\n> \n>     git -c merge.conflictStyle=diff3 git merge topic\n>     git checkout -m <unmerged-path>\n> \n> would recreate diff3 style conflicts, instead of using the default\n> config. I cannot decide if that would be convenient or confusing and\n> am interested to hear what others think.\n\nI think it hurts more than it helps. For example, I usually get away\nwith the regular conflict markers, but at times I might decide to see\nthe diff3 version. Then I could change the conflict marker style with\n\n   git checkout --conflict=diff3 -m <unmerged-path>\n\nIt would be disappointing if this were not possible.\n\n-- Hannes\n\n\n"},{"id":"554183","messageId":"1221dced-b3d1-4e44-af60-eac342b891b6@gmail.com","threadId":"66426","inReplyTo":"b2f86941-6ae2-4105-9284-e5e4d530b965@kdbg.org","subject":"Re: [PATCH v2 0/2] checkout -m: recreate conflict labels","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-05T15:09:16Z","receivedAt":"2026-10-05T15:09:16Z","isPatch":true,"body":"\n\nOn 05/10/2026 15:48, Johannes Sixt wrote:\n> Am 05.10.26 um 15:24 schrieb Phillip Wood:\n>> As \"git checkout -m\" is recreating the original conflict I wonder\n>> if we should remember the conflict style as well so that\n>>\n>>      git -c merge.conflictStyle=diff3 git merge topic\n>>      git checkout -m <unmerged-path>\n>>\n>> would recreate diff3 style conflicts, instead of using the default\n>> config. I cannot decide if that would be convenient or confusing and\n>> am interested to hear what others think.\n> > I think it hurts more than it helps. For example, I usually get away\n> with the regular conflict markers, but at times I might decide to see\n> the diff3 version. Then I could change the conflict marker style with\n> >     git checkout --conflict=diff3 -m <unmerged-path>\n> > It would be disappointing if this were not possible.\n\nI do the same thing. What I'm talking about above is \"git checkout -m\" using the same conflict style as the command that created the conflicts when the user does not specify a conflict style for the checkout.\n\n    git checkout --conflict=<style> ...\n\nand\n\n    git -c merge.conflictStyle=<style> checkout -m ...\n\nwould continue to work as they do now.\n\nThanks\n\nPhillip\n\n"},{"id":"554190","messageId":"xmqqcxtom9e0.fsf@gitster.g","threadId":"66426","inReplyTo":"cover.1791206658.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2 0/2] checkout -m: recreate conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-05T15:53:27Z","receivedAt":"2026-10-05T15:53:27Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> When \"git checkout -m <path>\" recreates a merge conflict, it uses\n> the labels \"base\", \"ours\", \"theirs\", rather than the labels used by\n> the original merge. This short series teaches the ort machinery to\n> write the labels to \".git/MERGE_LABELS\" when it switches to a merge\n> result containing conflicts, so that \"git checkout -m\" can then read\n> that file and use the same labels.\n>\n> As \"git checkout -m\" is recreating the original conflict I wonder\n> if we should remember the conflict style as well so that\n>\n>     git -c merge.conflictStyle=diff3 git merge topic\n>     git checkout -m <unmerged-path>\n>\n> would recreate diff3 style conflicts, instead of using the default\n> config. I cannot decide if that would be convenient or confusing and\n> am interested to hear what others think.\n\nIt has been quite a while since I invented and last looked at the\ncode paths for \"checkout -m\", but we should use the usual mechanism\nto decide what conflict style to use, so the only scenario that it\nmakes difference between recording and not recording is the case you\nshowed, i.e., the original merge was made with one-shot custom\nconflict style that is different from usual.\n\nAs \"git checkout -m\" can be used twice, after the above sequence,\nyou can\n\n    $ git -c merge.conflictStyle=diff3 checkout -m <path>\n\nto recover without losing any work.  If your regular style is\n\"merge\", then the following sequence might be more commonly useful:\n\n    $ git merge topic\n    $ git diff\n    ... stare at the diff output, feeling lost trying to\n    ... figure out what the correct resolution would be.\n    $ git -c merge.conflictStyle=diff3 checkout -m \\*\n    $ git diff\n    ... now with the common ancestor version, you understand\n    ... what both sides wanted to do better.\n\n"},{"id":"554191","messageId":"xmqqld8cktlc.fsf@gitster.g","threadId":"66426","inReplyTo":"18bdf7df49dde2c8e7f73f3b46c656abb6b26293.1791206658.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2 2/2] merge: remember conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-05T16:19:59Z","receivedAt":"2026-10-05T16:19:59Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref);\n>  int validate_new_branchname(const char *name, struct strbuf *ref, int force);\n>  \n>  #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0)\n> +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1)\n\nNot complaining and I have no improvement suggestions, but this\nphrasing made me imagine that we would be passing this flag bit\nin code paths where we want to write the extra file out.\n\nBut that does not match the reality.  merge_switch_to_result() calls\nwrite_merge_labels() unconditionally.  The bit controls if the file\nwritten survives the clean-up after the operation.\n\n> @@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info)\n>  \n>  static void update_refs_for_switch(const struct checkout_opts *opts,\n>  \t\t\t\t   struct branch_info *old_branch_info,\n> -\t\t\t\t   struct branch_info *new_branch_info)\n> +\t\t\t\t   struct branch_info *new_branch_info,\n> +\t\t\t\t   bool merge_conflicts)\n>  {\n>  \tstruct strbuf msg = STRBUF_INIT;\n>  \tconst char *old_desc, *reflog_msg;\n> @@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n>  \t}\n>  \tif (!opts->quiet)\n>  \t\tflags |= REMOVE_BRANCH_STATE_VERBOSE;\n> +\tif (merge_conflicts)\n> +\t\tflags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS;\n\nOK.\n\n>  \tremove_branch_state(the_repository, flags);\n>  \tstrbuf_release(&msg);\n>  \tif (!opts->quiet &&\n> @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts,\n>  \n>  \tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n>  \t\tfputc('\\n', stderr);\n> -\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n> +\n> +\tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info,\n> +\t\t\t       autostash_res == STASH_APPLY_CONFLICT);\n\nOK, so here we assume STASH_APPLY_CONFLICT result means we called\nwrite_merge_labels() and left the file.  If not, we did not call it\nand the file should not be there.\n\nBut then can't we just unconditionally leave the file, instead of\nnot removing what we wouldn't have created?\n\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 205fbd57e3..c374d5e0d5 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1977,6 +1977,7 @@ int cmd_commit(int argc,\n>  \n>  \tsequencer_post_commit_cleanup(the_repository, 0);\n>  \tunlink(git_path_merge_head(the_repository));\n> +\tunlink(git_path_merge_labels(the_repository));\n>  \tunlink(git_path_merge_msg(the_repository));\n>  \tunlink(git_path_merge_mode(the_repository));\n>  \tunlink(git_path_squash_msg(the_repository));\n\nHere we clean it up unconditionally after we are about to\nsuccessfully finish \"git commit\".\n\n> @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt,\n>  \t\t\treturn;\n>  \t\t}\n>  \t\ttrace2_region_leave(\"merge\", \"write_auto_merge\", opt->repo);\n> +\n> +\t\ttrace2_region_enter(\"merge\", \"write_merge_labels\", opt->repo);\n> +\t\topt->priv = result->priv;\n> +\t\twrite_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1],\n> +\t\t\t\t   opt->priv->labels[2]);\n> +\t\topt->priv = NULL;\n> +\t\ttrace2_region_leave(\"merge\", \"write_merge_labels\", opt->repo);\n>  \t}\n>  \tif (display_update_msgs)\n>  \t\tmerge_display_update_messages(opt, /* detailed */ 0, result);\n\n\n> @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt,\n>  \t * to move it.\n>  \t */\n>  \tassert(opt->priv && !result->priv);\n> +\tif (!result->clean) {\n> +\t\topt->priv->labels[0] =\n> +\t\t\tmem_pool_strdup(&opt->priv->pool, opt->ancestor);\n> +\t\topt->priv->labels[1] =\n> +\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch1);\n> +\t\topt->priv->labels[2] =\n> +\t\t\tmem_pool_strdup(&opt->priv->pool, opt->branch2);\n> +\t}\n\nOK, merge_switch_to_result() is the only thing that consumes these,\nand it will never happen after we call merge_finalize() where we\ndestroy the mempool, so this allocation should be safe.\n\n> +static char *parse_merge_label_line(struct strbuf *buf, FILE *fp)\n> +{\n> +\tif (strbuf_getline(buf, fp) == EOF)\n> +\t\treturn NULL;\n> +\n> +\treturn xmemdupz(buf->buf, buf->len);\n> +}\n\nWouldn't strbuf_detach() be more intuitive?\n\n> +int read_merge_labels(struct repository *r,\n> +\t\t      char **pbase, char** pours, char** ptheirs)\n\nBe consistent.  Asterisk sticks to variables, not types.\n\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tchar *base = NULL, *ours = NULL, *theirs = NULL;\n> +\tint ret = -1;\n> +\tFILE *fp = fopen(git_path_merge_labels(r), \"r\");\n> +\n> +\tif (!fp)\n> +\t\treturn -1;\n> +\n> +\tbase = parse_merge_label_line(&buf, fp);\n> +\tif (!base)\n> +\t\tgoto out;\n> +\n> +\tours = parse_merge_label_line(&buf, fp);\n> +\tif (!ours)\n> +\t\tgoto out;\n> +\n> +\ttheirs = parse_merge_label_line(&buf, fp);\n> +\tif (!theirs)\n> +\t\tgoto out;\n\nThe repetitions are a bit annoying, but it does not get much better:\n\n\tint i;\n\tchar bot[3] = {0}; /* base, ours, theirs */\n\n\tfor (i = 0; i < ARRAY_SIZE(bot); i++)\n        \tif (!(bot[i] = parse_merge_label_line(&buf, fp)))\n\t\t\tgoto out;\n\nso I am OK with what was posted.\n\nIt may be helpful to future developers to leave a comment that we\ndeliberately ignore cruft after these three lines in the file and\nwhy, instead of diagnosing it as an error.\n\n> +\tret = 0;\n> +\t*pbase = base;\n> +\t*pours = ours;\n> +\t*ptheirs = theirs;\n> +out:\n> +\tif (ret) {\n> +\t\tfree(base);\n> +\t\tfree(ours);\n> +\t\tfree(theirs);\n> +\t}\n> +\tfclose(fp);\n> +\tstrbuf_release(&buf);\n> +\n> +\treturn ret;\n> +}\n\n"},{"id":"554192","messageId":"xmqqbj98kt2d.fsf@gitster.g","threadId":"66426","inReplyTo":"18bdf7df49dde2c8e7f73f3b46c656abb6b26293.1791206658.git.phillip.wood@dunelm.org.uk","subject":"Re: [PATCH v2 2/2] merge: remember conflict labels","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-05T16:31:22Z","receivedAt":"2026-10-05T16:31:22Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Note that merge_switch_to_result()\n> we assign \"result->priv\" to \"opt->priv\" and later clear \"opt->priv\" in\n> order to get a pointer to the private struct as result->priv is void*.\n\nI missed this part.\n\n>  \t\ttrace2_region_leave(\"merge\", \"write_auto_merge\", opt->repo);\n> +\n> +\t\ttrace2_region_enter(\"merge\", \"write_merge_labels\", opt->repo);\n> +\t\topt->priv = result->priv;\n> +\t\twrite_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1],\n> +\t\t\t\t   opt->priv->labels[2]);\n> +\t\topt->priv = NULL;\n> +\t\ttrace2_region_leave(\"merge\", \"write_merge_labels\", opt->repo);\n\nWould it be better to do it this way instead?\n\n\tstruct merge_options_internal *priv = result->priv;\n\twrite_merge_labels(opt->repo,\n\t\t\t   priv->labels[0], priv->labels[1], priv->labels[2]);\n\nAlso, with the way merge labels are prepared and passed around, I\nwonder if we should just tighten its function signature and take\n\n\twrite_merge_labels(struct repository *repo, const char *labels[3])\n\nso that this calling site becomes[*]\n\n\tstruct merge_options_internal *priv = result->priv;\n\twrite_merge_labels(opt->repo, priv->labels);\n\n\n\n[Footnote]\n\n * Here, I deviate from the usual naming convention to call an array\n   of things in singular (so the second label would become\n   label[2]), because from the point of view of the API consumer,\n   \"labels\" as a unit is what they pass around, and call it in\n   plural.\n\n"}]}