From ac6889a969aa928ae3e8a85be5327b8fa42f4f8e Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 13 Oct 2026 16:47:53 +0100 Subject: [PATCH 1/3] remove_branch_state: convert boolean argument to flags Convert the "verbose" boolean argument to a flag so that we can add more flags in a future commit. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- 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 && From 63ea7cd6744b42aa7b62b1694c95a5823de897db Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 13 Oct 2026 16:47:54 +0100 Subject: [PATCH 2/3] checkout -m : cache merge settings Lookup the conflict style and normalization config settings once and cache the results, rather than looking up the same data for every path that is being merged. While looking up the config settings each time is fairly inexpensive, the next commit will start reading the conflict labels to use from a file and we want to avoid doing that more than once. Signed-off-by: Junio C Hamano Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/checkout.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index bdd2d816b6..ac66de23f9 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -310,9 +310,22 @@ static int checkout_stage(int stage, const struct cache_entry *ce, int pos, return error(_("path '%s' does not have their version"), ce->name); } +struct checkout_merged_data { + int conflict_style; + int renormalize; +}; + +static void checkout_merged_prepare(struct checkout_merged_data *data) +{ + int renormalize = 0; + + repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); + data->renormalize = renormalize; +} + static int checkout_merged(int pos, const struct checkout *state, int *nr_checkouts, struct mem_pool *ce_mem_pool, - int conflict_style) + struct checkout_merged_data *data) { struct cache_entry *ce = the_repository->index->cache[pos]; const char *path = ce->name; @@ -324,7 +337,6 @@ static int checkout_merged(int pos, const struct checkout *state, struct object_id threeway[3]; unsigned mode = 0; struct ll_merge_options ll_opts = LL_MERGE_OPTIONS_INIT; - int renormalize = 0; memset(threeway, 0, sizeof(threeway)); while (pos < the_repository->index->cache_nr) { @@ -345,9 +357,8 @@ static int checkout_merged(int pos, const struct checkout *state, read_mmblob(&ours, the_repository->objects, &threeway[1]); read_mmblob(&theirs, the_repository->objects, &threeway[2]); - repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); - ll_opts.renormalize = renormalize; - ll_opts.conflict_style = conflict_style; + ll_opts.renormalize = data->renormalize; + ll_opts.conflict_style = data->conflict_style; merge_status = ll_merge(&result_buf, path, &ancestor, "base", &ours, "ours", &theirs, "theirs", state->istate, &ll_opts); @@ -446,6 +457,7 @@ static int checkout_worktree(const struct checkout_opts *opts, int pos; int pc_workers, pc_threshold; struct mem_pool ce_mem_pool; + struct checkout_merged_data checkout_merged_data = {0}; state.force = 1; state.refresh_cache = 1; @@ -462,6 +474,10 @@ static int checkout_worktree(const struct checkout_opts *opts, if (pc_workers > 1) init_parallel_checkout(); + if (opts->merge) { + checkout_merged_prepare(&checkout_merged_data); + checkout_merged_data.conflict_style = opts->conflict_style; + } for (pos = 0; pos < the_repository->index->cache_nr; pos++) { struct cache_entry *ce = the_repository->index->cache[pos]; if (ce->ce_flags & CE_MATCHED) { @@ -479,7 +495,7 @@ static int checkout_worktree(const struct checkout_opts *opts, errs |= checkout_merged(pos, &state, &nr_unmerged, &ce_mem_pool, - opts->conflict_style); + &checkout_merged_data); pos = skip_same_name(ce, pos) - 1; } } From d5f4e42f02214b5d5af23578a4ad8de66427840d Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 13 Oct 2026 16:47:55 +0100 Subject: [PATCH 3/3] merge: remember conflict labels When recreating merge conflicts with "git checkout -m " 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 ", 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 " when recreating the conflicts. As "git checkout -m " 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 ". 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 Signed-off-by: Junio C Hamano --- branch.c | 11 ++++++-- branch.h | 1 + builtin/checkout.c | 32 +++++++++++++++++++--- 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, 155 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 @@ out: 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 ac66de23f9..53a7290fb3 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" @@ -313,14 +314,28 @@ static int checkout_stage(int stage, const struct cache_entry *ce, int pos, struct checkout_merged_data { int conflict_style; int renormalize; + char *base_label, *ours_label, *theirs_label; }; +static void checkout_merged_release(struct checkout_merged_data *data) +{ + free(data->base_label); + free(data->ours_label); + free(data->theirs_label); +} + static void checkout_merged_prepare(struct checkout_merged_data *data) { int renormalize = 0; repo_config_get_bool(the_repository, "merge.renormalize", &renormalize); data->renormalize = renormalize; + if (read_merge_labels(the_repository, &data->base_label, &data->ours_label, + &data->theirs_label)) { + data->base_label = xstrdup("base"); + data->ours_label = xstrdup("ours"); + data->theirs_label = xstrdup("theirs"); + } } static int checkout_merged(int pos, const struct checkout *state, @@ -359,8 +374,8 @@ static int checkout_merged(int pos, const struct checkout *state, ll_opts.renormalize = data->renormalize; ll_opts.conflict_style = data->conflict_style; - merge_status = ll_merge(&result_buf, path, &ancestor, "base", - &ours, "ours", &theirs, "theirs", + merge_status = ll_merge(&result_buf, path, &ancestor, data->base_label, + &ours, data->ours_label, &theirs, data->theirs_label, state->istate, &ll_opts); free(ancestor.ptr); free(ours.ptr); @@ -499,6 +514,10 @@ static int checkout_worktree(const struct checkout_opts *opts, pos = skip_same_name(ce, pos) - 1; } } + + if (opts->merge) + checkout_merged_release(&checkout_merged_data); + if (pc_workers > 1) errs |= run_parallel_checkout(&state, pc_workers, pc_threshold, NULL, NULL); @@ -962,7 +981,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; @@ -1064,6 +1084,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 && @@ -1278,7 +1300,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 840b6b4083..884c0915f4 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1950,6 +1950,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 6dae43e4db..7f76a14e49 100644 --- a/sequencer.c +++ b/sequencer.c @@ -5138,6 +5138,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 '