* [PATCH 0/2] checkout -m: recreate conflict labels
@ 2026-09-30 9:48 Phillip Wood
2026-09-30 9:48 ` [PATCH 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood
` (4 more replies)
0 siblings, 5 replies; 26+ messages in thread
From: Phillip Wood @ 2026-09-30 9:48 UTC (permalink / raw)
To: git; +Cc: Elijah Newren, Phillip Wood
When "git checkout -m <path>" recreates a merge conflict, it uses
the labels "base", "ours", "theirs", rather than the labels used by
the original merge. This short series teaches the ort machinery to
write the labels to ".git/MERGE_LABELS" when it switches to a merge
result containing conflicts, so that "git checkout -m" can then read
that file and use the same labels.
As "git checkout -m" is recreating the original conflict I wonder
if we should remember the conflict style as well so that
git -c merge.conflictStyle=diff3 git merge topic
git checkout -m <unmerged-path>
would recreate diff3 style conflicts, instead of using the default
config. I cannot decide if that would be convenient or confusing and
am interested to hear what others think.
base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7
Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Fconflict-labels%2Fv1
View-Changes-At: https://github.com/phillipwood/git/compare/3bc034112...fdaf3da99
Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/conflict-labels/v1
Phillip Wood (2):
remove_branch_state: convert boolean argument to flags
merge: remember conflict labels
branch.c | 17 +++++++++----
branch.h | 4 ++-
builtin/checkout.c | 30 ++++++++++++++++++----
builtin/commit.c | 1 +
merge-ort.c | 19 ++++++++++++++
merge.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++
merge.h | 4 +++
path.c | 1 +
path.h | 1 +
repository.c | 1 +
repository.h | 1 +
sequencer.c | 1 +
t/t7201-co.sh | 21 ++++++++++++++++
13 files changed, 153 insertions(+), 11 deletions(-)
--
2.56.0.rc2.84.gaf8b4f0d381
^ permalink raw reply [flat|nested] 26+ messages in thread* [PATCH 1/2] remove_branch_state: convert boolean argument to flags 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood @ 2026-09-30 9:48 ` Phillip Wood 2026-09-30 9:48 ` [PATCH 2/2] merge: remember conflict labels Phillip Wood ` (3 subsequent siblings) 4 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-09-30 9:48 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> Convert the "verbose" boolean argument to a flag so that we can add more flags in a future commit. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 4 ++-- branch.h | 3 ++- builtin/checkout.c | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/branch.c b/branch.c index 22f4f46b96..8bc7a395a7 100644 --- a/branch.c +++ b/branch.c @@ -871,9 +871,9 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } -void remove_branch_state(struct repository *r, int verbose) +void remove_branch_state(struct repository *r, unsigned flags) { - sequencer_post_commit_cleanup(r, verbose); + sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); remove_merge_branch_state(r); } diff --git a/branch.h b/branch.h index e9b1f7b37d..42d1b12918 100644 --- a/branch.h +++ b/branch.h @@ -127,6 +127,7 @@ int validate_branchname(const char *name, struct strbuf *ref); */ int validate_new_branchname(const char *name, struct strbuf *ref, int force); +#define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) @@ -137,7 +138,7 @@ void remove_merge_branch_state(struct repository *r); * Remove information about the state of working on the current * branch. (E.g., MERGE_HEAD) */ -void remove_branch_state(struct repository *r, int verbose); +void remove_branch_state(struct repository *r, unsigned flags); /* * Configure local branch "local" as downstream to branch "remote" diff --git a/builtin/checkout.c b/builtin/checkout.c index c0f0d2c700..bdd2d816b6 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -950,6 +950,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; + unsigned flags = 0; + if (opts->new_branch) { if (opts->new_orphan_branch) { enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET; @@ -1044,7 +1046,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts, old_branch_info->path); } } - remove_branch_state(the_repository, !opts->quiet); + if (!opts->quiet) + flags |= REMOVE_BRANCH_STATE_VERBOSE; + remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && !opts->force_detach && -- 2.56.0.rc2.84.gaf8b4f0d381 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH 2/2] merge: remember conflict labels 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood 2026-09-30 9:48 ` [PATCH 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood @ 2026-09-30 9:48 ` Phillip Wood 2026-09-30 16:42 ` Junio C Hamano 2026-09-30 20:24 ` [PATCH 0/2] checkout -m: recreate " Johannes Sixt ` (2 subsequent siblings) 4 siblings, 1 reply; 26+ messages in thread From: Phillip Wood @ 2026-09-30 9:48 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> When recreating merge conflicts with "git checkout -m <path>" the original conflict labels are lost. For commands like "git merge" and "git cherry-pick" we could use the presence of the related root ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the labels. However, if the conflicts are from "git stash pop" or "git checkout -m <branch>", then there is no ref to deduce the labels from. To ensure the labels are always available, the merge machinery is updated to write ".git/MERGE_LABELS" when it updates the worktree and there are conflicts. The labels are then read from that file by "git checkout -m <path>" when recreating the conflicts. As "git checkout -m <branch>" calls remove_branch_state() which ordinarily removes the labels file, we need to pass a flag down to optionally prevent that so that the labels are available for any subsequent "git checkout -m <path>". Note that merge_switch_to_result() we assign "result->priv" to "opt->priv" and later clear "opt->priv" in order to get a pointer to the private struct as result->priv is void*. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 11 ++++++-- branch.h | 1 + builtin/checkout.c | 24 +++++++++++++++--- builtin/commit.c | 1 + merge-ort.c | 19 ++++++++++++++ merge.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++ merge.h | 4 +++ path.c | 1 + path.h | 1 + repository.c | 1 + repository.h | 1 + sequencer.c | 1 + t/t7201-co.sh | 21 ++++++++++++++++ 13 files changed, 143 insertions(+), 6 deletions(-) diff --git a/branch.c b/branch.c index 8bc7a395a7..5bb1c28915 100644 --- a/branch.c +++ b/branch.c @@ -860,9 +860,11 @@ void create_branches_recursively(struct repository *r, const char *name, free(branch_point); } -void remove_merge_branch_state(struct repository *r) +static void do_remove_merge_branch_state(struct repository *r, unsigned flags) { unlink(git_path_merge_head(r)); + if (!(flags & REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS)) + unlink(git_path_merge_labels(r)); unlink(git_path_merge_rr(r)); unlink(git_path_merge_msg(r)); unlink(git_path_merge_mode(r)); @@ -871,11 +873,16 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } +void remove_merge_branch_state(struct repository *r) +{ + do_remove_merge_branch_state(r, 0); +} + void remove_branch_state(struct repository *r, unsigned flags) { sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); - remove_merge_branch_state(r); + do_remove_merge_branch_state(r, flags); } void die_if_checked_out(const char *branch, int ignore_current_worktree) diff --git a/branch.h b/branch.h index 42d1b12918..95b2431f24 100644 --- a/branch.h +++ b/branch.h @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref); int validate_new_branchname(const char *name, struct strbuf *ref, int force); #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) diff --git a/builtin/checkout.c b/builtin/checkout.c index bdd2d816b6..295fe0e9fa 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -15,6 +15,7 @@ #include "hex.h" #include "hook.h" #include "merge-ll.h" +#include "merge.h" #include "lockfile.h" #include "mem-pool.h" #include "object-file.h" @@ -317,6 +318,7 @@ static int checkout_merged(int pos, const struct checkout *state, struct cache_entry *ce = the_repository->index->cache[pos]; const char *path = ce->name; mmfile_t ancestor, ours, theirs; + char *base_label, *ours_label, *theirs_label; enum ll_merge_result merge_status; int status; struct object_id oid; @@ -347,10 +349,19 @@ static int checkout_merged(int pos, const struct checkout *state, repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); ll_opts.renormalize = renormalize; + if (read_merge_labels(the_repository, &base_label, &ours_label, + &theirs_label)) { + base_label = xstrdup("base"); + ours_label = xstrdup("ours"); + theirs_label = xstrdup("theirs"); + } ll_opts.conflict_style = conflict_style; - merge_status = ll_merge(&result_buf, path, &ancestor, "base", - &ours, "ours", &theirs, "theirs", + merge_status = ll_merge(&result_buf, path, &ancestor, base_label, + &ours, ours_label, &theirs, theirs_label, state->istate, &ll_opts); + free(base_label); + free(ours_label); + free(theirs_label); free(ancestor.ptr); free(ours.ptr); free(theirs.ptr); @@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info) static void update_refs_for_switch(const struct checkout_opts *opts, struct branch_info *old_branch_info, - struct branch_info *new_branch_info) + struct branch_info *new_branch_info, + bool merge_conflicts) { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; @@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, } if (!opts->quiet) flags |= REMOVE_BRANCH_STATE_VERBOSE; + if (merge_conflicts) + flags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS; remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts, if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) fputc('\n', stderr); - update_refs_for_switch(opts, &old_branch_info, new_branch_info); + + update_refs_for_switch(opts, &old_branch_info, new_branch_info, + autostash_res == STASH_APPLY_CONFLICT); if (created_autostash) { discard_index(the_repository->index); diff --git a/builtin/commit.c b/builtin/commit.c index 205fbd57e3..c374d5e0d5 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1977,6 +1977,7 @@ int cmd_commit(int argc, sequencer_post_commit_cleanup(the_repository, 0); unlink(git_path_merge_head(the_repository)); + unlink(git_path_merge_labels(the_repository)); unlink(git_path_merge_msg(the_repository)); unlink(git_path_merge_mode(the_repository)); unlink(git_path_squash_msg(the_repository)); diff --git a/merge-ort.c b/merge-ort.c index c410a5d353..783e74c6e3 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -35,6 +35,7 @@ #include "hex.h" #include "entry.h" #include "merge-ll.h" +#include "merge.h" #include "match-trees.h" #include "mem-pool.h" #include "object-file.h" @@ -418,6 +419,9 @@ struct merge_options_internal { /* field that holds submodule conflict information */ struct string_list conflicted_submodules; + + /* Copies of the labels used for conflict markers */ + char *labels[3]; }; struct conflicted_submodule_item { @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt, return; } trace2_region_leave("merge", "write_auto_merge", opt->repo); + + trace2_region_enter("merge", "write_merge_labels", opt->repo); + opt->priv = result->priv; + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], + opt->priv->labels[2]); + opt->priv = NULL; + trace2_region_leave("merge", "write_merge_labels", opt->repo); } if (display_update_msgs) merge_display_update_messages(opt, /* detailed */ 0, result); @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, * to move it. */ assert(opt->priv && !result->priv); + if (!result->clean) { + opt->priv->labels[0] = + mem_pool_strdup(&opt->priv->pool, opt->ancestor); + opt->priv->labels[1] = + mem_pool_strdup(&opt->priv->pool, opt->branch1); + opt->priv->labels[2] = + mem_pool_strdup(&opt->priv->pool, opt->branch2); + } result->priv = opt->priv; result->_properly_initialized = RESULT_INITIALIZED; opt->priv = NULL; diff --git a/merge.c b/merge.c index 0f5e823e63..95495ae1ba 100644 --- a/merge.c +++ b/merge.c @@ -8,6 +8,7 @@ #include "merge.h" #include "commit.h" #include "repository.h" +#include "path.h" #include "run-command.h" #include "resolve-undo.h" #include "tree.h" @@ -111,3 +112,65 @@ int checkout_fast_forward(struct repository *r, return error(_("unable to write new index file")); return 0; } + +int write_merge_labels(struct repository *r, const char *base, + const char *ours, const char *theirs) +{ + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); + + if (!f) + return -1; + + fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); + if (fclose(f)) + return error_errno("could not write '%s'", + git_path_merge_labels(r)); + + return 0; +} + +static int parse_merge_label_line(const char **p, char **line) +{ + const char *eol = strchr(*p, '\n'); + + if (!eol) + return -1; + + *line = xmemdupz(*p, eol - *p); + *p = eol + 1; + + return 0; +} + +int read_merge_labels(struct repository *r, + char **pbase, char** pours, char** ptheirs) +{ + struct strbuf buf = STRBUF_INIT; + const char *p; + char *base = NULL, *ours = NULL, *theirs = NULL; + int ret = -1; + + if (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0) + return -1; + + p = buf.buf; + if (parse_merge_label_line(&p, &base)) + goto out; + if (parse_merge_label_line(&p, &ours)) + goto out; + if (parse_merge_label_line(&p, &theirs)) + goto out; + ret = 0; + *pbase = base; + *pours = ours; + *ptheirs = theirs; +out: + if (ret) { + free(base); + free(ours); + free(theirs); + } + strbuf_release(&buf); + + return ret; +} diff --git a/merge.h b/merge.h index 21ac7ef2f1..0772737a87 100644 --- a/merge.h +++ b/merge.h @@ -13,5 +13,9 @@ int checkout_fast_forward(struct repository *r, const struct object_id *from, const struct object_id *to, int overwrite_ignore); +int write_merge_labels(struct repository *r, + const char *base, const char *ours, const char *theirs); +int read_merge_labels(struct repository *r, + char **base, char **ours, char **theirs); #endif /* MERGE_H */ diff --git a/path.c b/path.c index c3a709a928..7965762602 100644 --- a/path.c +++ b/path.c @@ -1655,3 +1655,4 @@ REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE") REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD") REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD") REPO_GIT_PATH_FUNC(shallow, "shallow") +REPO_GIT_PATH_FUNC(merge_labels, "MERGE_LABELS") diff --git a/path.h b/path.h index 7e7408dd05..8cd12ccfde 100644 --- a/path.h +++ b/path.h @@ -142,6 +142,7 @@ const char *git_path_merge_mode(struct repository *r); const char *git_path_merge_head(struct repository *r); const char *git_path_fetch_head(struct repository *r); const char *git_path_shallow(struct repository *r); +const char *git_path_merge_labels(struct repository *r); int ends_with_path_components(const char *path, const char *components); diff --git a/repository.c b/repository.c index b857e1c580..210fb819b0 100644 --- a/repository.c +++ b/repository.c @@ -367,6 +367,7 @@ static void repo_clear_path_cache(struct repo_path_cache *cache) FREE_AND_NULL(cache->merge_head); FREE_AND_NULL(cache->fetch_head); FREE_AND_NULL(cache->shallow); + FREE_AND_NULL(cache->merge_labels); } void repo_clear(struct repository *repo) diff --git a/repository.h b/repository.h index 11f5c2ed10..91b1f57db7 100644 --- a/repository.h +++ b/repository.h @@ -36,6 +36,7 @@ struct repo_path_cache { char *merge_head; char *fetch_head; char *shallow; + char *merge_labels; }; struct repository { diff --git a/sequencer.c b/sequencer.c index e25ef5eb61..0710aa6400 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5145,6 +5145,7 @@ static int pick_commits(struct repository *r, unlink(rebase_path_stopped_sha()); unlink(rebase_path_amend()); unlink(rebase_path_patch()); + unlink(git_path_merge_labels(r)); while (todo_list->current < todo_list->nr) { struct todo_item *item = todo_list->items + todo_list->current; diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 9ea9462914..7d0dcf8c8b 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -183,6 +183,27 @@ test_expect_success 'format of merge conflict from checkout -m' ' d >>>>>>> local EOF + test_cmp expect two && + + test_path_is_file .git/MERGE_LABELS && + + git checkout --conflict=diff3 two && + cat >expect <<-\EOF && + <<<<<<< simple + a + c + e + ||||||| main + a + b + c + d + e + ======= + b + d + >>>>>>> local + EOF test_cmp expect two ' -- 2.56.0.rc2.84.gaf8b4f0d381 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [PATCH 2/2] merge: remember conflict labels 2026-09-30 9:48 ` [PATCH 2/2] merge: remember conflict labels Phillip Wood @ 2026-09-30 16:42 ` Junio C Hamano 2026-10-01 8:54 ` Phillip Wood 0 siblings, 1 reply; 26+ messages in thread From: Junio C Hamano @ 2026-09-30 16:42 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren Phillip Wood <phillip.wood123@gmail.com> writes: > From: Phillip Wood <phillip.wood@dunelm.org.uk> > > When recreating merge conflicts with "git checkout -m <path>" the > original conflict labels are lost. For commands like "git merge" and > "git cherry-pick" we could use the presence of the related root > ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the > labels. However, if the conflicts are from "git stash pop" or "git > checkout -m <branch>", then there is no ref to deduce the labels from. To > ensure the labels are always available, the merge machinery is updated to > write ".git/MERGE_LABELS" when it updates the worktree and > there are conflicts. The labels are then read from that file by "git > checkout -m <path>" when recreating the conflicts. > > As "git checkout -m <branch>" calls remove_branch_state() which > ordinarily removes the labels file, we need to pass a flag down > to optionally prevent that so that the labels are available for any > subsequent "git checkout -m <path>". Note that merge_switch_to_result() > we assign "result->priv" to "opt->priv" and later clear "opt->priv" in > order to get a pointer to the private struct as result->priv is void*. > > Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> > --- > branch.c | 11 ++++++-- > branch.h | 1 + > builtin/checkout.c | 24 +++++++++++++++--- > builtin/commit.c | 1 + > merge-ort.c | 19 ++++++++++++++ > merge.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++ > merge.h | 4 +++ > path.c | 1 + > path.h | 1 + > repository.c | 1 + > repository.h | 1 + > sequencer.c | 1 + > t/t7201-co.sh | 21 ++++++++++++++++ > 13 files changed, 143 insertions(+), 6 deletions(-) Where do we talk about MERGE_HEAD and CHERRY_PICK_HEAD in the current documentation set? Do we want to mention MERGE_LABELS alongside them? > + > +int write_merge_labels(struct repository *r, const char *base, > + const char *ours, const char *theirs) > +{ > + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); > + > + if (!f) > + return -1; > + > + fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); > + if (fclose(f)) > + return error_errno("could not write '%s'", > + git_path_merge_labels(r)); > + > + return 0; > +} > + We write three items, one per line, delimited by LF. As this goes through stdio, wouldn't Windows write CRLF-delimited lines? I guess if we read this back through stdio, that will cancel out and we get the LF-delimited lines back? Wait. Do we want to read this file via stdio, one line at a time, using three calls to fgets()? No, we do not give a strict upper limit to the length of these labels. So if we read with strbuf_read_line() or something, we would be safe, I guess, but alas there is no such helper function X-<. > +static int parse_merge_label_line(const char **p, char **line) > +{ > + const char *eol = strchr(*p, '\n'); > + > + if (!eol) > + return -1; > + > + *line = xmemdupz(*p, eol - *p); > + *p = eol + 1; > + > + return 0; > +} OK, this reads one line at a time from the file contents already fully read by strbuf_read_file(), as seen below. Which means that the CRLF fprintf() may have written in write_merge_labels() will come back to this function, and our 'ours' may become 'ours\015' after stripping only the LF at the end? > +int read_merge_labels(struct repository *r, > + char **pbase, char** pours, char** ptheirs) > +{ > + struct strbuf buf = STRBUF_INIT; > + const char *p; > + char *base = NULL, *ours = NULL, *theirs = NULL; > + int ret = -1; > + > + if (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0) > + return -1; Can strbuf_read_file() fill '.buf' halfway and return a failure, or does it ensure that it frees '.buf' before returning failure? Just double-checking. ... goes and checks ... strbuf_read_file() calls strbuf_read(), which calls read_in_full() to fill a sufficiently large buffer, and a failure from there results in strbuf_release() or strbuf_setlen() resetting back to the '.len' before strbuf_read() was called (i.e., 0 in this case), so we do not leak anything on the error path and this code is safe, I think. > + > + p = buf.buf; > + if (parse_merge_label_line(&p, &base)) > + goto out; > + if (parse_merge_label_line(&p, &ours)) > + goto out; > + if (parse_merge_label_line(&p, &theirs)) > + goto out; OK, we read three things. > + ret = 0; > + *pbase = base; > + *pours = ours; > + *ptheirs = theirs; > +out: > + if (ret) { > + free(base); > + free(ours); > + free(theirs); > + } > + strbuf_release(&buf); OK, so the contract is that we will not touch p{base,ours,theirs} if we return failure, and we will not leak anything when doing so. Which is very sensible. > + return ret; > +} Looking good so far, modulo a small worry about writing via stdio and reading back while bypassing stdio. But perhaps CRLF is so annoying that the compat/mingw layer takes care of all of the above worries by passing the 'binary' bit down to the msvcrt/ucrt layer, in which case we should not have to worry about it. I dunno. Thanks for working on these patches. ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 2/2] merge: remember conflict labels 2026-09-30 16:42 ` Junio C Hamano @ 2026-10-01 8:54 ` Phillip Wood 2026-10-01 17:14 ` Junio C Hamano 0 siblings, 1 reply; 26+ messages in thread From: Phillip Wood @ 2026-10-01 8:54 UTC (permalink / raw) To: Junio C Hamano; +Cc: git, Elijah Newren Hi Junio On 30/09/2026 17:42, Junio C Hamano wrote: > Phillip Wood <phillip.wood123@gmail.com> writes: > >> From: Phillip Wood <phillip.wood@dunelm.org.uk> >> >> When recreating merge conflicts with "git checkout -m <path>" the >> original conflict labels are lost. For commands like "git merge" and >> "git cherry-pick" we could use the presence of the related root >> ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the >> labels. However, if the conflicts are from "git stash pop" or "git >> checkout -m <branch>", then there is no ref to deduce the labels from. To >> ensure the labels are always available, the merge machinery is updated to >> write ".git/MERGE_LABELS" when it updates the worktree and >> there are conflicts. The labels are then read from that file by "git >> checkout -m <path>" when recreating the conflicts. >> >> As "git checkout -m <branch>" calls remove_branch_state() which >> ordinarily removes the labels file, we need to pass a flag down >> to optionally prevent that so that the labels are available for any >> subsequent "git checkout -m <path>". Note that merge_switch_to_result() >> we assign "result->priv" to "opt->priv" and later clear "opt->priv" in >> order to get a pointer to the private struct as result->priv is void*. >> >> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> >> --- >> branch.c | 11 ++++++-- >> branch.h | 1 + >> builtin/checkout.c | 24 +++++++++++++++--- >> builtin/commit.c | 1 + >> merge-ort.c | 19 ++++++++++++++ >> merge.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++ >> merge.h | 4 +++ >> path.c | 1 + >> path.h | 1 + >> repository.c | 1 + >> repository.h | 1 + >> sequencer.c | 1 + >> t/t7201-co.sh | 21 ++++++++++++++++ >> 13 files changed, 143 insertions(+), 6 deletions(-) > > Where do we talk about MERGE_HEAD and CHERRY_PICK_HEAD in the > current documentation set? Do we want to mention MERGE_LABELS > alongside them? We talk about those in gitrevisions, the "refs" section of gitglossary and in the merge documentation. As this is not a ref I don't think it fits with MERGE_HEAD, it is more like MERGE_MSG, or MERGE_MODE. The merge man page mentions MERGE_MSG in passing but never explicitly says what it contains and MERGE_MODE is undocumented as far as I can see. We would perhaps benefit from documenting the common files like COMMIT_EDITMSG, MERGE_MSG, SQUASH_MSG, MERGE_HEAD, FETCH_HEAD and MERGE_LABELS somewhere in gitrepository briefly explaining what they contain and how they are used as a separate series. >> +static int parse_merge_label_line(const char **p, char **line) >> +{ >> + const char *eol = strchr(*p, '\n'); >> + >> + if (!eol) >> + return -1; >> + >> + *line = xmemdupz(*p, eol - *p); >> + *p = eol + 1; >> + >> + return 0; >> +} > > > OK, this reads one line at a time from the file contents already > fully read by strbuf_read_file(), as seen below. > > Which means that the CRLF fprintf() may have written in > write_merge_labels() will come back to this function, and our 'ours' > may become 'ours\015' after stripping only the LF at the end? That's a good point, I've changed it to use strbuf_getline() instead. > Thanks for working on these patches. Thanks for reviewing them, I'll send a re-roll in a couple of days Phillip ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 2/2] merge: remember conflict labels 2026-10-01 8:54 ` Phillip Wood @ 2026-10-01 17:14 ` Junio C Hamano 0 siblings, 0 replies; 26+ messages in thread From: Junio C Hamano @ 2026-10-01 17:14 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren Phillip Wood <phillip.wood123@gmail.com> writes: > We > would perhaps benefit from documenting the common files like > COMMIT_EDITMSG, MERGE_MSG, SQUASH_MSG, MERGE_HEAD, FETCH_HEAD and > MERGE_LABELS somewhere in gitrepository briefly explaining what they > contain and how they are used as a separate series. Sounds good. Thanks. ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 0/2] checkout -m: recreate conflict labels 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood 2026-09-30 9:48 ` [PATCH 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-09-30 9:48 ` [PATCH 2/2] merge: remember conflict labels Phillip Wood @ 2026-09-30 20:24 ` Johannes Sixt 2026-09-30 20:40 ` Junio C Hamano 2026-10-01 8:45 ` Phillip Wood 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood 4 siblings, 2 replies; 26+ messages in thread From: Johannes Sixt @ 2026-09-30 20:24 UTC (permalink / raw) To: Phillip Wood; +Cc: Elijah Newren, Phillip Wood, git Am 30.09.26 um 11:48 schrieb Phillip Wood: > When "git checkout -m <path>" recreates a merge conflict, it uses > the labels "base", "ours", "theirs", rather than the labels used by > the original merge. This short series teaches the ort machinery to > write the labels to ".git/MERGE_LABELS" when it switches to a merge > result containing conflicts, so that "git checkout -m" can then read > that file and use the same labels. Would an index extension not be a better place to store auxiliary information about merges? -- Hannes ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 0/2] checkout -m: recreate conflict labels 2026-09-30 20:24 ` [PATCH 0/2] checkout -m: recreate " Johannes Sixt @ 2026-09-30 20:40 ` Junio C Hamano 2026-09-30 21:20 ` Johannes Sixt 2026-10-01 8:45 ` Phillip Wood 1 sibling, 1 reply; 26+ messages in thread From: Junio C Hamano @ 2026-09-30 20:40 UTC (permalink / raw) To: Johannes Sixt; +Cc: Phillip Wood, Elijah Newren, Phillip Wood, git Johannes Sixt <j6t@kdbg.org> writes: > Am 30.09.26 um 11:48 schrieb Phillip Wood: >> When "git checkout -m <path>" recreates a merge conflict, it uses >> the labels "base", "ours", "theirs", rather than the labels used by >> the original merge. This short series teaches the ort machinery to >> write the labels to ".git/MERGE_LABELS" when it switches to a merge >> result containing conflicts, so that "git checkout -m" can then read >> that file and use the same labels. > > Would an index extension not be a better place to store auxiliary > information about merges? Wow. MERGE_HEAD, CHERRY_PICK_HEAD, and all others replaced with index extensions? That would unclutter $GIT_DIR/ quite a lot (for some reason, I find ORIG_HEAD is a bit of eyesore). It makes the information less accessible, so I am not sure how I feel about the proposal, but it is an interesting thought. Thanks. ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 0/2] checkout -m: recreate conflict labels 2026-09-30 20:40 ` Junio C Hamano @ 2026-09-30 21:20 ` Johannes Sixt 0 siblings, 0 replies; 26+ messages in thread From: Johannes Sixt @ 2026-09-30 21:20 UTC (permalink / raw) To: Junio C Hamano; +Cc: Phillip Wood, Elijah Newren, Phillip Wood, git Am 30.09.26 um 22:40 schrieb Junio C Hamano: > Johannes Sixt <j6t@kdbg.org> writes: > >> Am 30.09.26 um 11:48 schrieb Phillip Wood: >>> When "git checkout -m <path>" recreates a merge conflict, it uses >>> the labels "base", "ours", "theirs", rather than the labels used by >>> the original merge. This short series teaches the ort machinery to >>> write the labels to ".git/MERGE_LABELS" when it switches to a merge >>> result containing conflicts, so that "git checkout -m" can then read >>> that file and use the same labels. >> >> Would an index extension not be a better place to store auxiliary >> information about merges? > > Wow. MERGE_HEAD, CHERRY_PICK_HEAD, and all others replaced with > index extensions? Absolutely not. IIUC, MERGE_LABELS should not be a pseudo ref, but a file carrying auxiliary information. > That would unclutter $GIT_DIR/ quite a lot (for > some reason, I find ORIG_HEAD is a bit of eyesore). It makes the > information less accessible, so I am not sure how I feel about the > proposal, but it is an interesting thought. I don't know how accessible data in an index extension is. But if it's prohibitively difficult to access, then the idea is dead on arrival. -- Hannes ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH 0/2] checkout -m: recreate conflict labels 2026-09-30 20:24 ` [PATCH 0/2] checkout -m: recreate " Johannes Sixt 2026-09-30 20:40 ` Junio C Hamano @ 2026-10-01 8:45 ` Phillip Wood 1 sibling, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-01 8:45 UTC (permalink / raw) To: Johannes Sixt, Phillip Wood; +Cc: Elijah Newren, git On 30/09/2026 21:24, Johannes Sixt wrote: > Am 30.09.26 um 11:48 schrieb Phillip Wood: >> When "git checkout -m <path>" recreates a merge conflict, it uses >> the labels "base", "ours", "theirs", rather than the labels used by >> the original merge. This short series teaches the ort machinery to >> write the labels to ".git/MERGE_LABELS" when it switches to a merge >> result containing conflicts, so that "git checkout -m" can then read >> that file and use the same labels. > > Would an index extension not be a better place to store auxiliary > information about merges? I did briefly consider that, but it makes it much harder for other merge strategies such as git-merge-octopus (which I should probably update to write MERGE_LABELS) to store the labels. We already have MERGE_MODE, MERGE_RR and MERGE_MSG storing various bits of merge-related information so this series just follows existing practice. Thanks Phillip ^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH v2 0/2] checkout -m: recreate conflict labels 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood ` (2 preceding siblings ...) 2026-09-30 20:24 ` [PATCH 0/2] checkout -m: recreate " Johannes Sixt @ 2026-10-05 13:24 ` Phillip Wood 2026-10-05 13:24 ` [PATCH v2 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood ` (3 more replies) 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood 4 siblings, 4 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-05 13:24 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood When "git checkout -m <path>" recreates a merge conflict, it uses the labels "base", "ours", "theirs", rather than the labels used by the original merge. This short series teaches the ort machinery to write the labels to ".git/MERGE_LABELS" when it switches to a merge result containing conflicts, so that "git checkout -m" can then read that file and use the same labels. As "git checkout -m" is recreating the original conflict I wonder if we should remember the conflict style as well so that git -c merge.conflictStyle=diff3 git merge topic git checkout -m <unmerged-path> would recreate diff3 style conflicts, instead of using the default config. I cannot decide if that would be convenient or confusing and am interested to hear what others think. Changes since V1: - use strbuf_getline() rather than strbuf_read_file() to read labels so that the newline handling of the reading and writing sides match. NB ".git/MERGE_LABELS" is still undocumented - I'm hoping to find time to add some documentation for all the MERGE_* files in a future series. Johannes suggested using an index extension to store the labels, but as we already have MERGE_MODE, MERGE_RR and MERGE_MSG I think it is easier just to add another file. base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7 Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Fconflict-labels%2Fv2 View-Changes-At: https://github.com/phillipwood/git/compare/3bc034112...18bdf7df4 Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/conflict-labels/v2 Phillip Wood (2): remove_branch_state: convert boolean argument to flags merge: remember conflict labels branch.c | 17 ++++++++---- branch.h | 4 ++- builtin/checkout.c | 30 ++++++++++++++++++---- builtin/commit.c | 1 + merge-ort.c | 19 ++++++++++++++ merge.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++ merge.h | 4 +++ path.c | 1 + path.h | 1 + repository.c | 1 + repository.h | 1 + sequencer.c | 1 + t/t7201-co.sh | 21 +++++++++++++++ 13 files changed, 154 insertions(+), 11 deletions(-) Range-diff against v1: 1: 86ef0f848a = 1: 86ef0f848a remove_branch_state: convert boolean argument to flags 2: fdaf3da993 ! 2: 18bdf7df49 merge: remember conflict labels @@ merge.c: int checkout_fast_forward(struct repository *r, + return 0; +} + -+static int parse_merge_label_line(const char **p, char **line) ++static char *parse_merge_label_line(struct strbuf *buf, FILE *fp) +{ -+ const char *eol = strchr(*p, '\n'); -+ -+ if (!eol) -+ return -1; -+ -+ *line = xmemdupz(*p, eol - *p); -+ *p = eol + 1; -+ -+ return 0; ++ if (strbuf_getline(buf, fp) == EOF) ++ return NULL; ++ ++ return xmemdupz(buf->buf, buf->len); +} + +int read_merge_labels(struct repository *r, + char **pbase, char** pours, char** ptheirs) +{ + struct strbuf buf = STRBUF_INIT; -+ const char *p; + char *base = NULL, *ours = NULL, *theirs = NULL; + int ret = -1; ++ FILE *fp = fopen(git_path_merge_labels(r), "r"); + -+ if (strbuf_read_file(&buf, git_path_merge_labels(r), 0) < 0) ++ if (!fp) + return -1; + -+ p = buf.buf; -+ if (parse_merge_label_line(&p, &base)) -+ goto out; -+ if (parse_merge_label_line(&p, &ours)) -+ goto out; -+ if (parse_merge_label_line(&p, &theirs)) -+ goto out; ++ base = parse_merge_label_line(&buf, fp); ++ if (!base) ++ goto out; ++ ++ ours = parse_merge_label_line(&buf, fp); ++ if (!ours) ++ goto out; ++ ++ theirs = parse_merge_label_line(&buf, fp); ++ if (!theirs) ++ goto out; ++ + ret = 0; + *pbase = base; + *pours = ours; @@ merge.c: int checkout_fast_forward(struct repository *r, + free(ours); + free(theirs); + } ++ fclose(fp); + strbuf_release(&buf); + + return ret; -- 2.56.0.134.g299a3c16181 ^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH v2 1/2] remove_branch_state: convert boolean argument to flags 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood @ 2026-10-05 13:24 ` Phillip Wood 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood ` (2 subsequent siblings) 3 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-05 13:24 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> Convert the "verbose" boolean argument to a flag so that we can add more flags in a future commit. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 4 ++-- branch.h | 3 ++- builtin/checkout.c | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/branch.c b/branch.c index 22f4f46b96..8bc7a395a7 100644 --- a/branch.c +++ b/branch.c @@ -871,9 +871,9 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } -void remove_branch_state(struct repository *r, int verbose) +void remove_branch_state(struct repository *r, unsigned flags) { - sequencer_post_commit_cleanup(r, verbose); + sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); remove_merge_branch_state(r); } diff --git a/branch.h b/branch.h index e9b1f7b37d..42d1b12918 100644 --- a/branch.h +++ b/branch.h @@ -127,6 +127,7 @@ int validate_branchname(const char *name, struct strbuf *ref); */ int validate_new_branchname(const char *name, struct strbuf *ref, int force); +#define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) @@ -137,7 +138,7 @@ void remove_merge_branch_state(struct repository *r); * Remove information about the state of working on the current * branch. (E.g., MERGE_HEAD) */ -void remove_branch_state(struct repository *r, int verbose); +void remove_branch_state(struct repository *r, unsigned flags); /* * Configure local branch "local" as downstream to branch "remote" diff --git a/builtin/checkout.c b/builtin/checkout.c index c0f0d2c700..bdd2d816b6 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -950,6 +950,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; + unsigned flags = 0; + if (opts->new_branch) { if (opts->new_orphan_branch) { enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET; @@ -1044,7 +1046,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts, old_branch_info->path); } } - remove_branch_state(the_repository, !opts->quiet); + if (!opts->quiet) + flags |= REMOVE_BRANCH_STATE_VERBOSE; + remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && !opts->force_detach && -- 2.56.0.134.g299a3c16181 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood 2026-10-05 13:24 ` [PATCH v2 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood @ 2026-10-05 13:24 ` Phillip Wood 2026-10-05 16:19 ` Junio C Hamano ` (2 more replies) 2026-10-05 14:48 ` [PATCH v2 0/2] checkout -m: recreate " Johannes Sixt 2026-10-05 15:53 ` Junio C Hamano 3 siblings, 3 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-05 13:24 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> When recreating merge conflicts with "git checkout -m <path>" the original conflict labels are lost. For commands like "git merge" and "git cherry-pick" we could use the presence of the related root ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the labels. However, if the conflicts are from "git stash pop" or "git checkout -m <branch>", then there is no ref to deduce the labels from. To ensure the labels are always available, the merge machinery is updated to write ".git/MERGE_LABELS" when it updates the worktree and there are conflicts. The labels are then read from that file by "git checkout -m <path>" when recreating the conflicts. As "git checkout -m <branch>" calls remove_branch_state() which ordinarily removes the labels file, we need to pass a flag down to optionally prevent that so that the labels are available for any subsequent "git checkout -m <path>". Note that merge_switch_to_result() we assign "result->priv" to "opt->priv" and later clear "opt->priv" in order to get a pointer to the private struct as result->priv is void*. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 11 ++++++-- branch.h | 1 + builtin/checkout.c | 24 ++++++++++++++--- builtin/commit.c | 1 + merge-ort.c | 19 ++++++++++++++ merge.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++ merge.h | 4 +++ path.c | 1 + path.h | 1 + repository.c | 1 + repository.h | 1 + sequencer.c | 1 + t/t7201-co.sh | 21 +++++++++++++++ 13 files changed, 144 insertions(+), 6 deletions(-) diff --git a/branch.c b/branch.c index 8bc7a395a7..5bb1c28915 100644 --- a/branch.c +++ b/branch.c @@ -860,9 +860,11 @@ void create_branches_recursively(struct repository *r, const char *name, free(branch_point); } -void remove_merge_branch_state(struct repository *r) +static void do_remove_merge_branch_state(struct repository *r, unsigned flags) { unlink(git_path_merge_head(r)); + if (!(flags & REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS)) + unlink(git_path_merge_labels(r)); unlink(git_path_merge_rr(r)); unlink(git_path_merge_msg(r)); unlink(git_path_merge_mode(r)); @@ -871,11 +873,16 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } +void remove_merge_branch_state(struct repository *r) +{ + do_remove_merge_branch_state(r, 0); +} + void remove_branch_state(struct repository *r, unsigned flags) { sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); - remove_merge_branch_state(r); + do_remove_merge_branch_state(r, flags); } void die_if_checked_out(const char *branch, int ignore_current_worktree) diff --git a/branch.h b/branch.h index 42d1b12918..95b2431f24 100644 --- a/branch.h +++ b/branch.h @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref); int validate_new_branchname(const char *name, struct strbuf *ref, int force); #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) diff --git a/builtin/checkout.c b/builtin/checkout.c index bdd2d816b6..295fe0e9fa 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -15,6 +15,7 @@ #include "hex.h" #include "hook.h" #include "merge-ll.h" +#include "merge.h" #include "lockfile.h" #include "mem-pool.h" #include "object-file.h" @@ -317,6 +318,7 @@ static int checkout_merged(int pos, const struct checkout *state, struct cache_entry *ce = the_repository->index->cache[pos]; const char *path = ce->name; mmfile_t ancestor, ours, theirs; + char *base_label, *ours_label, *theirs_label; enum ll_merge_result merge_status; int status; struct object_id oid; @@ -347,10 +349,19 @@ static int checkout_merged(int pos, const struct checkout *state, repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); ll_opts.renormalize = renormalize; + if (read_merge_labels(the_repository, &base_label, &ours_label, + &theirs_label)) { + base_label = xstrdup("base"); + ours_label = xstrdup("ours"); + theirs_label = xstrdup("theirs"); + } ll_opts.conflict_style = conflict_style; - merge_status = ll_merge(&result_buf, path, &ancestor, "base", - &ours, "ours", &theirs, "theirs", + merge_status = ll_merge(&result_buf, path, &ancestor, base_label, + &ours, ours_label, &theirs, theirs_label, state->istate, &ll_opts); + free(base_label); + free(ours_label); + free(theirs_label); free(ancestor.ptr); free(ours.ptr); free(theirs.ptr); @@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info) static void update_refs_for_switch(const struct checkout_opts *opts, struct branch_info *old_branch_info, - struct branch_info *new_branch_info) + struct branch_info *new_branch_info, + bool merge_conflicts) { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; @@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, } if (!opts->quiet) flags |= REMOVE_BRANCH_STATE_VERBOSE; + if (merge_conflicts) + flags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS; remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts, if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) fputc('\n', stderr); - update_refs_for_switch(opts, &old_branch_info, new_branch_info); + + update_refs_for_switch(opts, &old_branch_info, new_branch_info, + autostash_res == STASH_APPLY_CONFLICT); if (created_autostash) { discard_index(the_repository->index); diff --git a/builtin/commit.c b/builtin/commit.c index 205fbd57e3..c374d5e0d5 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1977,6 +1977,7 @@ int cmd_commit(int argc, sequencer_post_commit_cleanup(the_repository, 0); unlink(git_path_merge_head(the_repository)); + unlink(git_path_merge_labels(the_repository)); unlink(git_path_merge_msg(the_repository)); unlink(git_path_merge_mode(the_repository)); unlink(git_path_squash_msg(the_repository)); diff --git a/merge-ort.c b/merge-ort.c index c410a5d353..783e74c6e3 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -35,6 +35,7 @@ #include "hex.h" #include "entry.h" #include "merge-ll.h" +#include "merge.h" #include "match-trees.h" #include "mem-pool.h" #include "object-file.h" @@ -418,6 +419,9 @@ struct merge_options_internal { /* field that holds submodule conflict information */ struct string_list conflicted_submodules; + + /* Copies of the labels used for conflict markers */ + char *labels[3]; }; struct conflicted_submodule_item { @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt, return; } trace2_region_leave("merge", "write_auto_merge", opt->repo); + + trace2_region_enter("merge", "write_merge_labels", opt->repo); + opt->priv = result->priv; + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], + opt->priv->labels[2]); + opt->priv = NULL; + trace2_region_leave("merge", "write_merge_labels", opt->repo); } if (display_update_msgs) merge_display_update_messages(opt, /* detailed */ 0, result); @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, * to move it. */ assert(opt->priv && !result->priv); + if (!result->clean) { + opt->priv->labels[0] = + mem_pool_strdup(&opt->priv->pool, opt->ancestor); + opt->priv->labels[1] = + mem_pool_strdup(&opt->priv->pool, opt->branch1); + opt->priv->labels[2] = + mem_pool_strdup(&opt->priv->pool, opt->branch2); + } result->priv = opt->priv; result->_properly_initialized = RESULT_INITIALIZED; opt->priv = NULL; diff --git a/merge.c b/merge.c index 0f5e823e63..892a78e0c9 100644 --- a/merge.c +++ b/merge.c @@ -8,6 +8,7 @@ #include "merge.h" #include "commit.h" #include "repository.h" +#include "path.h" #include "run-command.h" #include "resolve-undo.h" #include "tree.h" @@ -111,3 +112,66 @@ int checkout_fast_forward(struct repository *r, return error(_("unable to write new index file")); return 0; } + +int write_merge_labels(struct repository *r, const char *base, + const char *ours, const char *theirs) +{ + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); + + if (!f) + return -1; + + fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); + if (fclose(f)) + return error_errno("could not write '%s'", + git_path_merge_labels(r)); + + return 0; +} + +static char *parse_merge_label_line(struct strbuf *buf, FILE *fp) +{ + if (strbuf_getline(buf, fp) == EOF) + return NULL; + + return xmemdupz(buf->buf, buf->len); +} + +int read_merge_labels(struct repository *r, + char **pbase, char** pours, char** ptheirs) +{ + struct strbuf buf = STRBUF_INIT; + char *base = NULL, *ours = NULL, *theirs = NULL; + int ret = -1; + FILE *fp = fopen(git_path_merge_labels(r), "r"); + + if (!fp) + return -1; + + base = parse_merge_label_line(&buf, fp); + if (!base) + goto out; + + ours = parse_merge_label_line(&buf, fp); + if (!ours) + goto out; + + theirs = parse_merge_label_line(&buf, fp); + if (!theirs) + goto out; + + ret = 0; + *pbase = base; + *pours = ours; + *ptheirs = theirs; +out: + if (ret) { + free(base); + free(ours); + free(theirs); + } + fclose(fp); + strbuf_release(&buf); + + return ret; +} diff --git a/merge.h b/merge.h index 21ac7ef2f1..0772737a87 100644 --- a/merge.h +++ b/merge.h @@ -13,5 +13,9 @@ int checkout_fast_forward(struct repository *r, const struct object_id *from, const struct object_id *to, int overwrite_ignore); +int write_merge_labels(struct repository *r, + const char *base, const char *ours, const char *theirs); +int read_merge_labels(struct repository *r, + char **base, char **ours, char **theirs); #endif /* MERGE_H */ diff --git a/path.c b/path.c index c3a709a928..7965762602 100644 --- a/path.c +++ b/path.c @@ -1655,3 +1655,4 @@ REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE") REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD") REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD") REPO_GIT_PATH_FUNC(shallow, "shallow") +REPO_GIT_PATH_FUNC(merge_labels, "MERGE_LABELS") diff --git a/path.h b/path.h index 7e7408dd05..8cd12ccfde 100644 --- a/path.h +++ b/path.h @@ -142,6 +142,7 @@ const char *git_path_merge_mode(struct repository *r); const char *git_path_merge_head(struct repository *r); const char *git_path_fetch_head(struct repository *r); const char *git_path_shallow(struct repository *r); +const char *git_path_merge_labels(struct repository *r); int ends_with_path_components(const char *path, const char *components); diff --git a/repository.c b/repository.c index b857e1c580..210fb819b0 100644 --- a/repository.c +++ b/repository.c @@ -367,6 +367,7 @@ static void repo_clear_path_cache(struct repo_path_cache *cache) FREE_AND_NULL(cache->merge_head); FREE_AND_NULL(cache->fetch_head); FREE_AND_NULL(cache->shallow); + FREE_AND_NULL(cache->merge_labels); } void repo_clear(struct repository *repo) diff --git a/repository.h b/repository.h index 11f5c2ed10..91b1f57db7 100644 --- a/repository.h +++ b/repository.h @@ -36,6 +36,7 @@ struct repo_path_cache { char *merge_head; char *fetch_head; char *shallow; + char *merge_labels; }; struct repository { diff --git a/sequencer.c b/sequencer.c index e25ef5eb61..0710aa6400 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5145,6 +5145,7 @@ static int pick_commits(struct repository *r, unlink(rebase_path_stopped_sha()); unlink(rebase_path_amend()); unlink(rebase_path_patch()); + unlink(git_path_merge_labels(r)); while (todo_list->current < todo_list->nr) { struct todo_item *item = todo_list->items + todo_list->current; diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 9ea9462914..7d0dcf8c8b 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -183,6 +183,27 @@ test_expect_success 'format of merge conflict from checkout -m' ' d >>>>>>> local EOF + test_cmp expect two && + + test_path_is_file .git/MERGE_LABELS && + + git checkout --conflict=diff3 two && + cat >expect <<-\EOF && + <<<<<<< simple + a + c + e + ||||||| main + a + b + c + d + e + ======= + b + d + >>>>>>> local + EOF test_cmp expect two ' -- 2.56.0.134.g299a3c16181 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood @ 2026-10-05 16:19 ` Junio C Hamano 2026-10-06 15:21 ` Phillip Wood 2026-10-05 16:31 ` Junio C Hamano 2026-10-06 15:46 ` Junio C Hamano 2 siblings, 1 reply; 26+ messages in thread From: Junio C Hamano @ 2026-10-05 16:19 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren, Johannes Sixt Phillip Wood <phillip.wood123@gmail.com> writes: > @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref); > int validate_new_branchname(const char *name, struct strbuf *ref, int force); > > #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) > +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1) Not complaining and I have no improvement suggestions, but this phrasing made me imagine that we would be passing this flag bit in code paths where we want to write the extra file out. But that does not match the reality. merge_switch_to_result() calls write_merge_labels() unconditionally. The bit controls if the file written survives the clean-up after the operation. > @@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info) > > static void update_refs_for_switch(const struct checkout_opts *opts, > struct branch_info *old_branch_info, > - struct branch_info *new_branch_info) > + struct branch_info *new_branch_info, > + bool merge_conflicts) > { > struct strbuf msg = STRBUF_INIT; > const char *old_desc, *reflog_msg; > @@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, > } > if (!opts->quiet) > flags |= REMOVE_BRANCH_STATE_VERBOSE; > + if (merge_conflicts) > + flags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS; OK. > remove_branch_state(the_repository, flags); > strbuf_release(&msg); > if (!opts->quiet && > @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts, > > if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) > fputc('\n', stderr); > - update_refs_for_switch(opts, &old_branch_info, new_branch_info); > + > + update_refs_for_switch(opts, &old_branch_info, new_branch_info, > + autostash_res == STASH_APPLY_CONFLICT); OK, so here we assume STASH_APPLY_CONFLICT result means we called write_merge_labels() and left the file. If not, we did not call it and the file should not be there. But then can't we just unconditionally leave the file, instead of not removing what we wouldn't have created? > diff --git a/builtin/commit.c b/builtin/commit.c > index 205fbd57e3..c374d5e0d5 100644 > --- a/builtin/commit.c > +++ b/builtin/commit.c > @@ -1977,6 +1977,7 @@ int cmd_commit(int argc, > > sequencer_post_commit_cleanup(the_repository, 0); > unlink(git_path_merge_head(the_repository)); > + unlink(git_path_merge_labels(the_repository)); > unlink(git_path_merge_msg(the_repository)); > unlink(git_path_merge_mode(the_repository)); > unlink(git_path_squash_msg(the_repository)); Here we clean it up unconditionally after we are about to successfully finish "git commit". > @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt, > return; > } > trace2_region_leave("merge", "write_auto_merge", opt->repo); > + > + trace2_region_enter("merge", "write_merge_labels", opt->repo); > + opt->priv = result->priv; > + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], > + opt->priv->labels[2]); > + opt->priv = NULL; > + trace2_region_leave("merge", "write_merge_labels", opt->repo); > } > if (display_update_msgs) > merge_display_update_messages(opt, /* detailed */ 0, result); > @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, > * to move it. > */ > assert(opt->priv && !result->priv); > + if (!result->clean) { > + opt->priv->labels[0] = > + mem_pool_strdup(&opt->priv->pool, opt->ancestor); > + opt->priv->labels[1] = > + mem_pool_strdup(&opt->priv->pool, opt->branch1); > + opt->priv->labels[2] = > + mem_pool_strdup(&opt->priv->pool, opt->branch2); > + } OK, merge_switch_to_result() is the only thing that consumes these, and it will never happen after we call merge_finalize() where we destroy the mempool, so this allocation should be safe. > +static char *parse_merge_label_line(struct strbuf *buf, FILE *fp) > +{ > + if (strbuf_getline(buf, fp) == EOF) > + return NULL; > + > + return xmemdupz(buf->buf, buf->len); > +} Wouldn't strbuf_detach() be more intuitive? > +int read_merge_labels(struct repository *r, > + char **pbase, char** pours, char** ptheirs) Be consistent. Asterisk sticks to variables, not types. > +{ > + struct strbuf buf = STRBUF_INIT; > + char *base = NULL, *ours = NULL, *theirs = NULL; > + int ret = -1; > + FILE *fp = fopen(git_path_merge_labels(r), "r"); > + > + if (!fp) > + return -1; > + > + base = parse_merge_label_line(&buf, fp); > + if (!base) > + goto out; > + > + ours = parse_merge_label_line(&buf, fp); > + if (!ours) > + goto out; > + > + theirs = parse_merge_label_line(&buf, fp); > + if (!theirs) > + goto out; The repetitions are a bit annoying, but it does not get much better: int i; char bot[3] = {0}; /* base, ours, theirs */ for (i = 0; i < ARRAY_SIZE(bot); i++) if (!(bot[i] = parse_merge_label_line(&buf, fp))) goto out; so I am OK with what was posted. It may be helpful to future developers to leave a comment that we deliberately ignore cruft after these three lines in the file and why, instead of diagnosing it as an error. > + ret = 0; > + *pbase = base; > + *pours = ours; > + *ptheirs = theirs; > +out: > + if (ret) { > + free(base); > + free(ours); > + free(theirs); > + } > + fclose(fp); > + strbuf_release(&buf); > + > + return ret; > +} ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 16:19 ` Junio C Hamano @ 2026-10-06 15:21 ` Phillip Wood 0 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-06 15:21 UTC (permalink / raw) To: Junio C Hamano; +Cc: git, Elijah Newren, Johannes Sixt On 05/10/2026 17:19, Junio C Hamano wrote: > Phillip Wood <phillip.wood123@gmail.com> writes: > >> @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref); >> int validate_new_branchname(const char *name, struct strbuf *ref, int force); >> >> #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) >> +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1) > > Not complaining and I have no improvement suggestions, but this > phrasing made me imagine that we would be passing this flag bit > in code paths where we want to write the extra file out. I can see why you'd think that, would appending "_FILE" make it clearer? (though the name is long enough already) > But that does not match the reality. merge_switch_to_result() calls > write_merge_labels() unconditionally. The bit controls if the file > written survives the clean-up after the operation. >> @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts, >> >> if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) >> fputc('\n', stderr); >> - update_refs_for_switch(opts, &old_branch_info, new_branch_info); >> + >> + update_refs_for_switch(opts, &old_branch_info, new_branch_info, >> + autostash_res == STASH_APPLY_CONFLICT); > > OK, so here we assume STASH_APPLY_CONFLICT result means we called > write_merge_labels() and left the file. If not, we did not call it > and the file should not be there. > > But then can't we just unconditionally leave the file, instead of > not removing what we wouldn't have created? Hmm, If a previous command such as "git stash pop" or "git checkout -m" had conflicts and wrote the file, and then the user resolves the conflicts and runs "git checkout" without "-m" (or with "-m" without creating conflicts) don't we want to remove the file? >> +static char *parse_merge_label_line(struct strbuf *buf, FILE *fp) >> +{ >> + if (strbuf_getline(buf, fp) == EOF) >> + return NULL; >> + >> + return xmemdupz(buf->buf, buf->len); >> +} > > Wouldn't strbuf_detach() be more intuitive? > >> +int read_merge_labels(struct repository *r, >> + char **pbase, char** pours, char** ptheirs) > > Be consistent. Asterisk sticks to variables, not types. Oops, I'll fix those. >> +{ >> + struct strbuf buf = STRBUF_INIT; >> + char *base = NULL, *ours = NULL, *theirs = NULL; >> + int ret = -1; >> + FILE *fp = fopen(git_path_merge_labels(r), "r"); >> + >> + if (!fp) >> + return -1; >> + >> + base = parse_merge_label_line(&buf, fp); >> + if (!base) >> + goto out; >> + >> + ours = parse_merge_label_line(&buf, fp); >> + if (!ours) >> + goto out; >> + >> + theirs = parse_merge_label_line(&buf, fp); >> + if (!theirs) >> + goto out; > > The repetitions are a bit annoying, but it does not get much better: > > int i; > char bot[3] = {0}; /* base, ours, theirs */ > > for (i = 0; i < ARRAY_SIZE(bot); i++) > if (!(bot[i] = parse_merge_label_line(&buf, fp))) > goto out; > > so I am OK with what was posted. Yeah, they are a bit annoying, but as there are only three of them it isn't too bad. > It may be helpful to future developers to leave a comment that we > deliberately ignore cruft after these three lines in the file and > why, instead of diagnosing it as an error. Will do Thanks Phillip >> + ret = 0; >> + *pbase = base; >> + *pours = ours; >> + *ptheirs = theirs; >> +out: >> + if (ret) { >> + free(base); >> + free(ours); >> + free(theirs); >> + } >> + fclose(fp); >> + strbuf_release(&buf); >> + >> + return ret; >> +} ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood 2026-10-05 16:19 ` Junio C Hamano @ 2026-10-05 16:31 ` Junio C Hamano 2026-10-06 15:05 ` Phillip Wood 2026-10-06 15:46 ` Junio C Hamano 2 siblings, 1 reply; 26+ messages in thread From: Junio C Hamano @ 2026-10-05 16:31 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren, Johannes Sixt Phillip Wood <phillip.wood123@gmail.com> writes: > Note that merge_switch_to_result() > we assign "result->priv" to "opt->priv" and later clear "opt->priv" in > order to get a pointer to the private struct as result->priv is void*. I missed this part. > trace2_region_leave("merge", "write_auto_merge", opt->repo); > + > + trace2_region_enter("merge", "write_merge_labels", opt->repo); > + opt->priv = result->priv; > + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], > + opt->priv->labels[2]); > + opt->priv = NULL; > + trace2_region_leave("merge", "write_merge_labels", opt->repo); Would it be better to do it this way instead? struct merge_options_internal *priv = result->priv; write_merge_labels(opt->repo, priv->labels[0], priv->labels[1], priv->labels[2]); Also, with the way merge labels are prepared and passed around, I wonder if we should just tighten its function signature and take write_merge_labels(struct repository *repo, const char *labels[3]) so that this calling site becomes[*] struct merge_options_internal *priv = result->priv; write_merge_labels(opt->repo, priv->labels); [Footnote] * Here, I deviate from the usual naming convention to call an array of things in singular (so the second label would become label[2]), because from the point of view of the API consumer, "labels" as a unit is what they pass around, and call it in plural. ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 16:31 ` Junio C Hamano @ 2026-10-06 15:05 ` Phillip Wood 0 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-06 15:05 UTC (permalink / raw) To: Junio C Hamano; +Cc: git, Elijah Newren, Johannes Sixt On 05/10/2026 17:31, Junio C Hamano wrote: > Phillip Wood <phillip.wood123@gmail.com> writes: > >> Note that merge_switch_to_result() >> we assign "result->priv" to "opt->priv" and later clear "opt->priv" in >> order to get a pointer to the private struct as result->priv is void*. > > I missed this part. > >> trace2_region_leave("merge", "write_auto_merge", opt->repo); >> + >> + trace2_region_enter("merge", "write_merge_labels", opt->repo); >> + opt->priv = result->priv; >> + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], >> + opt->priv->labels[2]); >> + opt->priv = NULL; >> + trace2_region_leave("merge", "write_merge_labels", opt->repo); > > Would it be better to do it this way instead? > > struct merge_options_internal *priv = result->priv; > write_merge_labels(opt->repo, > priv->labels[0], priv->labels[1], priv->labels[2]); Yes, maybe we should have a preparatory commit that adds that "priv" variable and updates the existing code that does the same dance. Another option would be to make result->priv a pointer to an opaque struct like opts->priv - I don't really see any advantage in keeping it as a void*. > Also, with the way merge labels are prepared and passed around, I > wonder if we should just tighten its function signature and take > > write_merge_labels(struct repository *repo, const char *labels[3]) > > so that this calling site becomes[*] > > struct merge_options_internal *priv = result->priv; > write_merge_labels(opt->repo, priv->labels); That's a nice idea Thanks Phillip > > > [Footnote] > > * Here, I deviate from the usual naming convention to call an array > of things in singular (so the second label would become > label[2]), because from the point of view of the API consumer, > "labels" as a unit is what they pass around, and call it in > plural. ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood 2026-10-05 16:19 ` Junio C Hamano 2026-10-05 16:31 ` Junio C Hamano @ 2026-10-06 15:46 ` Junio C Hamano 2026-10-07 13:38 ` Phillip Wood 2 siblings, 1 reply; 26+ messages in thread From: Junio C Hamano @ 2026-10-06 15:46 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren, Johannes Sixt Phillip Wood <phillip.wood123@gmail.com> writes: > @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt, > return; > } > trace2_region_leave("merge", "write_auto_merge", opt->repo); > + > + trace2_region_enter("merge", "write_merge_labels", opt->repo); > + opt->priv = result->priv; > + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], > + opt->priv->labels[2]); > + opt->priv = NULL; > + trace2_region_leave("merge", "write_merge_labels", opt->repo); A "if (result->clean >= 0 && update_worktree_and_index)" condition guards this code path, so we unconditionall call write_merge_labels(), whether the result is clean or with conflict. Am I reading the code correctly? > @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, > * to move it. > */ > assert(opt->priv && !result->priv); > + if (!result->clean) { > + opt->priv->labels[0] = > + mem_pool_strdup(&opt->priv->pool, opt->ancestor); > + opt->priv->labels[1] = > + mem_pool_strdup(&opt->priv->pool, opt->branch1); > + opt->priv->labels[2] = > + mem_pool_strdup(&opt->priv->pool, opt->branch2); > + } But we only populate the labels[] when conflicted. What would we write when we do not have conflicts? > +int write_merge_labels(struct repository *r, const char *base, > + const char *ours, const char *theirs) > +{ > + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); > + > + if (!f) > + return -1; > + > + fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); > + if (fclose(f)) > + return error_errno("could not write '%s'", > + git_path_merge_labels(r)); > + > + return 0; > +} Would the answer be "(null)\n(null)\n(null)\n"? ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 2/2] merge: remember conflict labels 2026-10-06 15:46 ` Junio C Hamano @ 2026-10-07 13:38 ` Phillip Wood 0 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-07 13:38 UTC (permalink / raw) To: Junio C Hamano; +Cc: git, Elijah Newren, Johannes Sixt On 06/10/2026 16:46, Junio C Hamano wrote: > Phillip Wood <phillip.wood123@gmail.com> writes: > >> @@ -4969,6 +4973,13 @@ void merge_switch_to_result(struct merge_options *opt, >> return; >> } >> trace2_region_leave("merge", "write_auto_merge", opt->repo); >> + >> + trace2_region_enter("merge", "write_merge_labels", opt->repo); >> + opt->priv = result->priv; >> + write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], >> + opt->priv->labels[2]); >> + opt->priv = NULL; >> + trace2_region_leave("merge", "write_merge_labels", opt->repo); > > A "if (result->clean >= 0 && update_worktree_and_index)" condition > guards this code path, so we unconditionall call > write_merge_labels(), whether the result is clean or with conflict. > Am I reading the code correctly? Yes, I think so. Ouch! how did I miss that? Will fix, thanks Phillip >> @@ -5234,6 +5245,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, >> * to move it. >> */ >> assert(opt->priv && !result->priv); >> + if (!result->clean) { >> + opt->priv->labels[0] = >> + mem_pool_strdup(&opt->priv->pool, opt->ancestor); >> + opt->priv->labels[1] = >> + mem_pool_strdup(&opt->priv->pool, opt->branch1); >> + opt->priv->labels[2] = >> + mem_pool_strdup(&opt->priv->pool, opt->branch2); >> + } > > But we only populate the labels[] when conflicted. What would we > write when we do not have conflicts? > >> +int write_merge_labels(struct repository *r, const char *base, >> + const char *ours, const char *theirs) >> +{ >> + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); >> + >> + if (!f) >> + return -1; >> + >> + fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); >> + if (fclose(f)) >> + return error_errno("could not write '%s'", >> + git_path_merge_labels(r)); >> + >> + return 0; >> +} > > Would the answer be "(null)\n(null)\n(null)\n"? ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 0/2] checkout -m: recreate conflict labels 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood 2026-10-05 13:24 ` [PATCH v2 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood @ 2026-10-05 14:48 ` Johannes Sixt 2026-10-05 15:09 ` Phillip Wood 2026-10-05 15:53 ` Junio C Hamano 3 siblings, 1 reply; 26+ messages in thread From: Johannes Sixt @ 2026-10-05 14:48 UTC (permalink / raw) To: Phillip Wood; +Cc: Elijah Newren, Phillip Wood, git Am 05.10.26 um 15:24 schrieb Phillip Wood: > As "git checkout -m" is recreating the original conflict I wonder > if we should remember the conflict style as well so that > > git -c merge.conflictStyle=diff3 git merge topic > git checkout -m <unmerged-path> > > would recreate diff3 style conflicts, instead of using the default > config. I cannot decide if that would be convenient or confusing and > am interested to hear what others think. I think it hurts more than it helps. For example, I usually get away with the regular conflict markers, but at times I might decide to see the diff3 version. Then I could change the conflict marker style with git checkout --conflict=diff3 -m <unmerged-path> It would be disappointing if this were not possible. -- Hannes ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 0/2] checkout -m: recreate conflict labels 2026-10-05 14:48 ` [PATCH v2 0/2] checkout -m: recreate " Johannes Sixt @ 2026-10-05 15:09 ` Phillip Wood 0 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-05 15:09 UTC (permalink / raw) To: Johannes Sixt, Phillip Wood; +Cc: Elijah Newren, git On 05/10/2026 15:48, Johannes Sixt wrote: > Am 05.10.26 um 15:24 schrieb Phillip Wood: >> As "git checkout -m" is recreating the original conflict I wonder >> if we should remember the conflict style as well so that >> >> git -c merge.conflictStyle=diff3 git merge topic >> git checkout -m <unmerged-path> >> >> would recreate diff3 style conflicts, instead of using the default >> config. I cannot decide if that would be convenient or confusing and >> am interested to hear what others think. > > I think it hurts more than it helps. For example, I usually get away > with the regular conflict markers, but at times I might decide to see > the diff3 version. Then I could change the conflict marker style with > > git checkout --conflict=diff3 -m <unmerged-path> > > It would be disappointing if this were not possible. I 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. git checkout --conflict=<style> ... and git -c merge.conflictStyle=<style> checkout -m ... would continue to work as they do now. Thanks Phillip ^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [PATCH v2 0/2] checkout -m: recreate conflict labels 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood ` (2 preceding siblings ...) 2026-10-05 14:48 ` [PATCH v2 0/2] checkout -m: recreate " Johannes Sixt @ 2026-10-05 15:53 ` Junio C Hamano 3 siblings, 0 replies; 26+ messages in thread From: Junio C Hamano @ 2026-10-05 15:53 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren, Johannes Sixt Phillip Wood <phillip.wood123@gmail.com> writes: > When "git checkout -m <path>" recreates a merge conflict, it uses > the labels "base", "ours", "theirs", rather than the labels used by > the original merge. This short series teaches the ort machinery to > write the labels to ".git/MERGE_LABELS" when it switches to a merge > result containing conflicts, so that "git checkout -m" can then read > that file and use the same labels. > > As "git checkout -m" is recreating the original conflict I wonder > if we should remember the conflict style as well so that > > git -c merge.conflictStyle=diff3 git merge topic > git checkout -m <unmerged-path> > > would recreate diff3 style conflicts, instead of using the default > config. I cannot decide if that would be convenient or confusing and > am interested to hear what others think. It has been quite a while since I invented and last looked at the code paths for "checkout -m", but we should use the usual mechanism to decide what conflict style to use, so the only scenario that it makes difference between recording and not recording is the case you showed, i.e., the original merge was made with one-shot custom conflict style that is different from usual. As "git checkout -m" can be used twice, after the above sequence, you can $ git -c merge.conflictStyle=diff3 checkout -m <path> to recover without losing any work. If your regular style is "merge", then the following sequence might be more commonly useful: $ git merge topic $ git diff ... stare at the diff output, feeling lost trying to ... figure out what the correct resolution would be. $ git -c merge.conflictStyle=diff3 checkout -m \* $ git diff ... now with the common ancestor version, you understand ... what both sides wanted to do better. ^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH v3 0/2] checkout -m: recreate conflict labels 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood ` (3 preceding siblings ...) 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood @ 2026-10-09 9:13 ` Phillip Wood 2026-10-09 9:13 ` [PATCH v3 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood ` (2 more replies) 4 siblings, 3 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-09 9:13 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood When "git checkout -m <path>" recreates a merge conflict, it uses the labels "base", "ours", "theirs", rather than the labels used by the original merge. This short series teaches the ort machinery to write the labels to ".git/MERGE_LABELS" when it switches to a merge result containing conflicts, so that "git checkout -m" can then read that file and use the same labels. Thanks to Junio and Johannes for their comments. Changes since V2: - only write ".git/MERGE_LABELS" when there are conflicts and added a check to an existing "checkout -m <branch>" test - change write_merge_labels() to take an array of labels - use a local variable to store the internal merge state when writing labels - use strbuf_detach() rather than xmemdupz() when reading labels - add a comment to say we ignore trailing cruft when reading the labels file Changes since V1: - use strbuf_getline() rather than strbuf_read_file() to read labels so that the newline handling of the reading and writing sides match. NB ".git/MERGE_LABELS" is still undocumented - I'm hoping to find time to add some documentation for all the MERGE_* files in a future series. Johannes suggested using an index extension to store the labels, but as we already have MERGE_MODE, MERGE_RR and MERGE_MSG I think it is easier just to add another file. base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7 Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Fconflict-labels%2Fv3 View-Changes-At: https://github.com/phillipwood/git/compare/3bc034112...182edb2e8 Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/conflict-labels/v3 Phillip Wood (2): remove_branch_state: convert boolean argument to flags merge: remember conflict labels branch.c | 17 ++++++++---- branch.h | 4 ++- builtin/checkout.c | 30 +++++++++++++++++---- builtin/commit.c | 1 + merge-ort.c | 20 ++++++++++++++ merge.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++ merge.h | 3 +++ path.c | 1 + path.h | 1 + repository.c | 1 + repository.h | 1 + sequencer.c | 1 + t/t7201-co.sh | 22 ++++++++++++++++ 13 files changed, 157 insertions(+), 11 deletions(-) Range-diff against v2: 1: 86ef0f848a = 1: 86ef0f848a remove_branch_state: convert boolean argument to flags 2: 18bdf7df49 ! 2: 182edb2e88 merge: remember conflict labels @@ merge-ort.c: struct merge_options_internal { struct string_list conflicted_submodules; + + /* Copies of the labels used for conflict markers */ -+ char *labels[3]; ++ const char *labels[3]; }; struct conflicted_submodule_item { @@ merge-ort.c: void merge_switch_to_result(struct merge_options *opt, } trace2_region_leave("merge", "write_auto_merge", opt->repo); + -+ trace2_region_enter("merge", "write_merge_labels", opt->repo); -+ opt->priv = result->priv; -+ write_merge_labels(opt->repo, opt->priv->labels[0], opt->priv->labels[1], -+ opt->priv->labels[2]); -+ opt->priv = NULL; -+ trace2_region_leave("merge", "write_merge_labels", opt->repo); ++ if (!result->clean) { ++ struct merge_options_internal *priv = result->priv; ++ ++ trace2_region_enter("merge", "write_merge_labels", opt->repo); ++ write_merge_labels(opt->repo, priv->labels); ++ trace2_region_leave("merge", "write_merge_labels", opt->repo); ++ } } if (display_update_msgs) merge_display_update_messages(opt, /* detailed */ 0, result); @@ merge.c: int checkout_fast_forward(struct repository *r, return 0; } + -+int write_merge_labels(struct repository *r, const char *base, -+ const char *ours, const char *theirs) ++int write_merge_labels(struct repository *r, const char *labels[3]) +{ + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); + + if (!f) + return -1; + -+ fprintf(f, "%s\n%s\n%s\n", base, ours, theirs); ++ fprintf(f, "%s\n%s\n%s\n", labels[0], labels[1], labels[2]); + if (fclose(f)) + return error_errno("could not write '%s'", + git_path_merge_labels(r)); + + return 0; +} + -+static char *parse_merge_label_line(struct strbuf *buf, FILE *fp) -+{ -+ if (strbuf_getline(buf, fp) == EOF) -+ return NULL; -+ -+ return xmemdupz(buf->buf, buf->len); -+} -+ -+int read_merge_labels(struct repository *r, -+ char **pbase, char** pours, char** ptheirs) ++static char *parse_merge_label_line(FILE *fp) +{ + struct strbuf buf = STRBUF_INIT; ++ ++ if (strbuf_getline(&buf, fp) == EOF) { ++ strbuf_release(&buf); ++ return NULL; ++ } ++ ++ return strbuf_detach(&buf, NULL); ++} ++ ++int read_merge_labels(struct repository *r, ++ char **pbase, char **pours, char **ptheirs) ++{ + char *base = NULL, *ours = NULL, *theirs = NULL; + int ret = -1; + FILE *fp = fopen(git_path_merge_labels(r), "r"); + + if (!fp) + return -1; + -+ base = parse_merge_label_line(&buf, fp); ++ base = parse_merge_label_line(fp); + if (!base) + goto out; + -+ ours = parse_merge_label_line(&buf, fp); ++ ours = parse_merge_label_line(fp); + if (!ours) + goto out; + -+ theirs = parse_merge_label_line(&buf, fp); ++ theirs = parse_merge_label_line(fp); + if (!theirs) + goto out; ++ /* We ignore any trailing lines */ + + ret = 0; + *pbase = base; @@ merge.c: int checkout_fast_forward(struct repository *r, + free(theirs); + } + fclose(fp); -+ strbuf_release(&buf); + + return ret; +} @@ merge.h: int checkout_fast_forward(struct repository *r, const struct object_id *from, const struct object_id *to, int overwrite_ignore); -+int write_merge_labels(struct repository *r, -+ const char *base, const char *ours, const char *theirs); ++int write_merge_labels(struct repository *r, const char *labels[3]); +int read_merge_labels(struct repository *r, + char **base, char **ours, char **theirs); @@ sequencer.c: static int pick_commits(struct repository *r, struct todo_item *item = todo_list->items + todo_list->current; ## t/t7201-co.sh ## +@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' ' + + fill 0 1 2 3 4 5 6 7 8 >one && + git checkout -m side >messages && ++ test_path_is_missing .git/MERGE_LABELS && + + test "$(git symbolic-ref HEAD)" = "refs/heads/side" && + @@ t/t7201-co.sh: test_expect_success 'format of merge conflict from checkout -m' ' d >>>>>>> local -- 2.56.0.134.g299a3c16181 ^ permalink raw reply [flat|nested] 26+ messages in thread
* [PATCH v3 1/2] remove_branch_state: convert boolean argument to flags 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood @ 2026-10-09 9:13 ` Phillip Wood 2026-10-09 9:13 ` [PATCH v3 2/2] merge: remember conflict labels Phillip Wood 2026-10-09 20:31 ` [PATCH v3 0/2] checkout -m: recreate " Junio C Hamano 2 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-09 9:13 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> Convert the "verbose" boolean argument to a flag so that we can add more flags in a future commit. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 4 ++-- branch.h | 3 ++- builtin/checkout.c | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/branch.c b/branch.c index 22f4f46b96..8bc7a395a7 100644 --- a/branch.c +++ b/branch.c @@ -871,9 +871,9 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } -void remove_branch_state(struct repository *r, int verbose) +void remove_branch_state(struct repository *r, unsigned flags) { - sequencer_post_commit_cleanup(r, verbose); + sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); remove_merge_branch_state(r); } diff --git a/branch.h b/branch.h index e9b1f7b37d..42d1b12918 100644 --- a/branch.h +++ b/branch.h @@ -127,6 +127,7 @@ int validate_branchname(const char *name, struct strbuf *ref); */ int validate_new_branchname(const char *name, struct strbuf *ref, int force); +#define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) @@ -137,7 +138,7 @@ void remove_merge_branch_state(struct repository *r); * Remove information about the state of working on the current * branch. (E.g., MERGE_HEAD) */ -void remove_branch_state(struct repository *r, int verbose); +void remove_branch_state(struct repository *r, unsigned flags); /* * Configure local branch "local" as downstream to branch "remote" diff --git a/builtin/checkout.c b/builtin/checkout.c index c0f0d2c700..bdd2d816b6 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -950,6 +950,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; + unsigned flags = 0; + if (opts->new_branch) { if (opts->new_orphan_branch) { enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET; @@ -1044,7 +1046,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts, old_branch_info->path); } } - remove_branch_state(the_repository, !opts->quiet); + if (!opts->quiet) + flags |= REMOVE_BRANCH_STATE_VERBOSE; + remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && !opts->force_detach && -- 2.56.0.134.g299a3c16181 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* [PATCH v3 2/2] merge: remember conflict labels 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood 2026-10-09 9:13 ` [PATCH v3 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood @ 2026-10-09 9:13 ` Phillip Wood 2026-10-09 20:31 ` [PATCH v3 0/2] checkout -m: recreate " Junio C Hamano 2 siblings, 0 replies; 26+ messages in thread From: Phillip Wood @ 2026-10-09 9:13 UTC (permalink / raw) To: git; +Cc: Elijah Newren, Johannes Sixt, Phillip Wood From: Phillip Wood <phillip.wood@dunelm.org.uk> When recreating merge conflicts with "git checkout -m <path>" the original conflict labels are lost. For commands like "git merge" and "git cherry-pick" we could use the presence of the related root ref (MERGE_HEAD and CHERRY_PICK_HEAD respectively) to recreate the labels. However, if the conflicts are from "git stash pop" or "git checkout -m <branch>", then there is no ref to deduce the labels from. To ensure the labels are always available, the merge machinery is updated to write ".git/MERGE_LABELS" when it updates the worktree and there are conflicts. The labels are then read from that file by "git checkout -m <path>" when recreating the conflicts. As "git checkout -m <branch>" calls remove_branch_state() which ordinarily removes the labels file, we need to pass a flag down to optionally prevent that so that the labels are available for any subsequent "git checkout -m <path>". Note that merge_switch_to_result() we assign "result->priv" to "opt->priv" and later clear "opt->priv" in order to get a pointer to the private struct as result->priv is void*. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> --- branch.c | 11 ++++++-- branch.h | 1 + builtin/checkout.c | 24 ++++++++++++++--- builtin/commit.c | 1 + merge-ort.c | 20 ++++++++++++++ merge.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++ merge.h | 3 +++ path.c | 1 + path.h | 1 + repository.c | 1 + repository.h | 1 + sequencer.c | 1 + t/t7201-co.sh | 22 ++++++++++++++++ 13 files changed, 147 insertions(+), 6 deletions(-) diff --git a/branch.c b/branch.c index 8bc7a395a7..5bb1c28915 100644 --- a/branch.c +++ b/branch.c @@ -860,9 +860,11 @@ void create_branches_recursively(struct repository *r, const char *name, free(branch_point); } -void remove_merge_branch_state(struct repository *r) +static void do_remove_merge_branch_state(struct repository *r, unsigned flags) { unlink(git_path_merge_head(r)); + if (!(flags & REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS)) + unlink(git_path_merge_labels(r)); unlink(git_path_merge_rr(r)); unlink(git_path_merge_msg(r)); unlink(git_path_merge_mode(r)); @@ -871,11 +873,16 @@ void remove_merge_branch_state(struct repository *r) save_autostash_ref(r, "MERGE_AUTOSTASH"); } +void remove_merge_branch_state(struct repository *r) +{ + do_remove_merge_branch_state(r, 0); +} + void remove_branch_state(struct repository *r, unsigned flags) { sequencer_post_commit_cleanup(r, flags & REMOVE_BRANCH_STATE_VERBOSE); unlink(git_path_squash_msg(r)); - remove_merge_branch_state(r); + do_remove_merge_branch_state(r, flags); } void die_if_checked_out(const char *branch, int ignore_current_worktree) diff --git a/branch.h b/branch.h index 42d1b12918..95b2431f24 100644 --- a/branch.h +++ b/branch.h @@ -128,6 +128,7 @@ int validate_branchname(const char *name, struct strbuf *ref); int validate_new_branchname(const char *name, struct strbuf *ref, int force); #define REMOVE_BRANCH_STATE_VERBOSE (1u << 0) +#define REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS (1u << 1) /* * Remove information about the merge state on the current * branch. (E.g., MERGE_HEAD) diff --git a/builtin/checkout.c b/builtin/checkout.c index bdd2d816b6..295fe0e9fa 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -15,6 +15,7 @@ #include "hex.h" #include "hook.h" #include "merge-ll.h" +#include "merge.h" #include "lockfile.h" #include "mem-pool.h" #include "object-file.h" @@ -317,6 +318,7 @@ static int checkout_merged(int pos, const struct checkout *state, struct cache_entry *ce = the_repository->index->cache[pos]; const char *path = ce->name; mmfile_t ancestor, ours, theirs; + char *base_label, *ours_label, *theirs_label; enum ll_merge_result merge_status; int status; struct object_id oid; @@ -347,10 +349,19 @@ static int checkout_merged(int pos, const struct checkout *state, repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); ll_opts.renormalize = renormalize; + if (read_merge_labels(the_repository, &base_label, &ours_label, + &theirs_label)) { + base_label = xstrdup("base"); + ours_label = xstrdup("ours"); + theirs_label = xstrdup("theirs"); + } ll_opts.conflict_style = conflict_style; - merge_status = ll_merge(&result_buf, path, &ancestor, "base", - &ours, "ours", &theirs, "theirs", + merge_status = ll_merge(&result_buf, path, &ancestor, base_label, + &ours, ours_label, &theirs, theirs_label, state->istate, &ll_opts); + free(base_label); + free(ours_label); + free(theirs_label); free(ancestor.ptr); free(ours.ptr); free(theirs.ptr); @@ -946,7 +957,8 @@ static void report_tracking(struct branch_info *new_branch_info) static void update_refs_for_switch(const struct checkout_opts *opts, struct branch_info *old_branch_info, - struct branch_info *new_branch_info) + struct branch_info *new_branch_info, + bool merge_conflicts) { struct strbuf msg = STRBUF_INIT; const char *old_desc, *reflog_msg; @@ -1048,6 +1060,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts, } if (!opts->quiet) flags |= REMOVE_BRANCH_STATE_VERBOSE; + if (merge_conflicts) + flags |= REMOVE_BRANCH_STATE_PRESERVE_CONFLICT_LABELS; remove_branch_state(the_repository, flags); strbuf_release(&msg); if (!opts->quiet && @@ -1262,7 +1276,9 @@ static int switch_branches(const struct checkout_opts *opts, if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet) fputc('\n', stderr); - update_refs_for_switch(opts, &old_branch_info, new_branch_info); + + update_refs_for_switch(opts, &old_branch_info, new_branch_info, + autostash_res == STASH_APPLY_CONFLICT); if (created_autostash) { discard_index(the_repository->index); diff --git a/builtin/commit.c b/builtin/commit.c index 205fbd57e3..c374d5e0d5 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1977,6 +1977,7 @@ int cmd_commit(int argc, sequencer_post_commit_cleanup(the_repository, 0); unlink(git_path_merge_head(the_repository)); + unlink(git_path_merge_labels(the_repository)); unlink(git_path_merge_msg(the_repository)); unlink(git_path_merge_mode(the_repository)); unlink(git_path_squash_msg(the_repository)); diff --git a/merge-ort.c b/merge-ort.c index c410a5d353..8f720e0d12 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -35,6 +35,7 @@ #include "hex.h" #include "entry.h" #include "merge-ll.h" +#include "merge.h" #include "match-trees.h" #include "mem-pool.h" #include "object-file.h" @@ -418,6 +419,9 @@ struct merge_options_internal { /* field that holds submodule conflict information */ struct string_list conflicted_submodules; + + /* Copies of the labels used for conflict markers */ + const char *labels[3]; }; struct conflicted_submodule_item { @@ -4969,6 +4973,14 @@ void merge_switch_to_result(struct merge_options *opt, return; } trace2_region_leave("merge", "write_auto_merge", opt->repo); + + if (!result->clean) { + struct merge_options_internal *priv = result->priv; + + trace2_region_enter("merge", "write_merge_labels", opt->repo); + write_merge_labels(opt->repo, priv->labels); + trace2_region_leave("merge", "write_merge_labels", opt->repo); + } } if (display_update_msgs) merge_display_update_messages(opt, /* detailed */ 0, result); @@ -5234,6 +5246,14 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt, * to move it. */ assert(opt->priv && !result->priv); + if (!result->clean) { + opt->priv->labels[0] = + mem_pool_strdup(&opt->priv->pool, opt->ancestor); + opt->priv->labels[1] = + mem_pool_strdup(&opt->priv->pool, opt->branch1); + opt->priv->labels[2] = + mem_pool_strdup(&opt->priv->pool, opt->branch2); + } result->priv = opt->priv; result->_properly_initialized = RESULT_INITIALIZED; opt->priv = NULL; diff --git a/merge.c b/merge.c index 0f5e823e63..6a557d11ac 100644 --- a/merge.c +++ b/merge.c @@ -8,6 +8,7 @@ #include "merge.h" #include "commit.h" #include "repository.h" +#include "path.h" #include "run-command.h" #include "resolve-undo.h" #include "tree.h" @@ -111,3 +112,68 @@ int checkout_fast_forward(struct repository *r, return error(_("unable to write new index file")); return 0; } + +int write_merge_labels(struct repository *r, const char *labels[3]) +{ + FILE *f = fopen_or_warn(git_path_merge_labels(r), "w"); + + if (!f) + return -1; + + fprintf(f, "%s\n%s\n%s\n", labels[0], labels[1], labels[2]); + if (fclose(f)) + return error_errno("could not write '%s'", + git_path_merge_labels(r)); + + return 0; +} + +static char *parse_merge_label_line(FILE *fp) +{ + struct strbuf buf = STRBUF_INIT; + + if (strbuf_getline(&buf, fp) == EOF) { + strbuf_release(&buf); + return NULL; + } + + return strbuf_detach(&buf, NULL); +} + +int read_merge_labels(struct repository *r, + char **pbase, char **pours, char **ptheirs) +{ + char *base = NULL, *ours = NULL, *theirs = NULL; + int ret = -1; + FILE *fp = fopen(git_path_merge_labels(r), "r"); + + if (!fp) + return -1; + + base = parse_merge_label_line(fp); + if (!base) + goto out; + + ours = parse_merge_label_line(fp); + if (!ours) + goto out; + + theirs = parse_merge_label_line(fp); + if (!theirs) + goto out; + /* We ignore any trailing lines */ + + ret = 0; + *pbase = base; + *pours = ours; + *ptheirs = theirs; +out: + if (ret) { + free(base); + free(ours); + free(theirs); + } + fclose(fp); + + return ret; +} diff --git a/merge.h b/merge.h index 21ac7ef2f1..3d936d6988 100644 --- a/merge.h +++ b/merge.h @@ -13,5 +13,8 @@ int checkout_fast_forward(struct repository *r, const struct object_id *from, const struct object_id *to, int overwrite_ignore); +int write_merge_labels(struct repository *r, const char *labels[3]); +int read_merge_labels(struct repository *r, + char **base, char **ours, char **theirs); #endif /* MERGE_H */ diff --git a/path.c b/path.c index c3a709a928..7965762602 100644 --- a/path.c +++ b/path.c @@ -1655,3 +1655,4 @@ REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE") REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD") REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD") REPO_GIT_PATH_FUNC(shallow, "shallow") +REPO_GIT_PATH_FUNC(merge_labels, "MERGE_LABELS") diff --git a/path.h b/path.h index 7e7408dd05..8cd12ccfde 100644 --- a/path.h +++ b/path.h @@ -142,6 +142,7 @@ const char *git_path_merge_mode(struct repository *r); const char *git_path_merge_head(struct repository *r); const char *git_path_fetch_head(struct repository *r); const char *git_path_shallow(struct repository *r); +const char *git_path_merge_labels(struct repository *r); int ends_with_path_components(const char *path, const char *components); diff --git a/repository.c b/repository.c index b857e1c580..210fb819b0 100644 --- a/repository.c +++ b/repository.c @@ -367,6 +367,7 @@ static void repo_clear_path_cache(struct repo_path_cache *cache) FREE_AND_NULL(cache->merge_head); FREE_AND_NULL(cache->fetch_head); FREE_AND_NULL(cache->shallow); + FREE_AND_NULL(cache->merge_labels); } void repo_clear(struct repository *repo) diff --git a/repository.h b/repository.h index 11f5c2ed10..91b1f57db7 100644 --- a/repository.h +++ b/repository.h @@ -36,6 +36,7 @@ struct repo_path_cache { char *merge_head; char *fetch_head; char *shallow; + char *merge_labels; }; struct repository { diff --git a/sequencer.c b/sequencer.c index e25ef5eb61..0710aa6400 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5145,6 +5145,7 @@ static int pick_commits(struct repository *r, unlink(rebase_path_stopped_sha()); unlink(rebase_path_amend()); unlink(rebase_path_patch()); + unlink(git_path_merge_labels(r)); while (todo_list->current < todo_list->nr) { struct todo_item *item = todo_list->items + todo_list->current; diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 9ea9462914..3e9e04738a 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -99,6 +99,7 @@ test_expect_success 'checkout -m with dirty tree' ' fill 0 1 2 3 4 5 6 7 8 >one && git checkout -m side >messages && + test_path_is_missing .git/MERGE_LABELS && test "$(git symbolic-ref HEAD)" = "refs/heads/side" && @@ -183,6 +184,27 @@ test_expect_success 'format of merge conflict from checkout -m' ' d >>>>>>> local EOF + test_cmp expect two && + + test_path_is_file .git/MERGE_LABELS && + + git checkout --conflict=diff3 two && + cat >expect <<-\EOF && + <<<<<<< simple + a + c + e + ||||||| main + a + b + c + d + e + ======= + b + d + >>>>>>> local + EOF test_cmp expect two ' -- 2.56.0.134.g299a3c16181 ^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [PATCH v3 0/2] checkout -m: recreate conflict labels 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood 2026-10-09 9:13 ` [PATCH v3 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-10-09 9:13 ` [PATCH v3 2/2] merge: remember conflict labels Phillip Wood @ 2026-10-09 20:31 ` Junio C Hamano 2 siblings, 0 replies; 26+ messages in thread From: Junio C Hamano @ 2026-10-09 20:31 UTC (permalink / raw) To: Phillip Wood; +Cc: git, Elijah Newren, Johannes Sixt Phillip Wood <phillip.wood123@gmail.com> writes: > Changes since V2: > > - only write ".git/MERGE_LABELS" when there are conflicts and added > a check to an existing "checkout -m <branch>" test > - change write_merge_labels() to take an array of labels > - use a local variable to store the internal merge state when writing > labels > - use strbuf_detach() rather than xmemdupz() when reading labels > - add a comment to say we ignore trailing cruft when reading the > labels file These patches looked nicely done. Will replace. Thanks. ^ permalink raw reply [flat|nested] 26+ messages in thread
end of thread, other threads:[~2026-10-09 20:31 UTC | newest] Thread overview: 26+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-30 9:48 [PATCH 0/2] checkout -m: recreate conflict labels Phillip Wood 2026-09-30 9:48 ` [PATCH 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-09-30 9:48 ` [PATCH 2/2] merge: remember conflict labels Phillip Wood 2026-09-30 16:42 ` Junio C Hamano 2026-10-01 8:54 ` Phillip Wood 2026-10-01 17:14 ` Junio C Hamano 2026-09-30 20:24 ` [PATCH 0/2] checkout -m: recreate " Johannes Sixt 2026-09-30 20:40 ` Junio C Hamano 2026-09-30 21:20 ` Johannes Sixt 2026-10-01 8:45 ` Phillip Wood 2026-10-05 13:24 ` [PATCH v2 " Phillip Wood 2026-10-05 13:24 ` [PATCH v2 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-10-05 13:24 ` [PATCH v2 2/2] merge: remember conflict labels Phillip Wood 2026-10-05 16:19 ` Junio C Hamano 2026-10-06 15:21 ` Phillip Wood 2026-10-05 16:31 ` Junio C Hamano 2026-10-06 15:05 ` Phillip Wood 2026-10-06 15:46 ` Junio C Hamano 2026-10-07 13:38 ` Phillip Wood 2026-10-05 14:48 ` [PATCH v2 0/2] checkout -m: recreate " Johannes Sixt 2026-10-05 15:09 ` Phillip Wood 2026-10-05 15:53 ` Junio C Hamano 2026-10-09 9:13 ` [PATCH v3 " Phillip Wood 2026-10-09 9:13 ` [PATCH v3 1/2] remove_branch_state: convert boolean argument to flags Phillip Wood 2026-10-09 9:13 ` [PATCH v3 2/2] merge: remember conflict labels Phillip Wood 2026-10-09 20:31 ` [PATCH v3 0/2] checkout -m: recreate " Junio C Hamano
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox