merge: remember conflict labels
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> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
1 parent
63ea7cd674
commit
d5f4e42f02
13 files changed
+155
-6
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+28
-4
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
+20
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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")
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,6 +36,7 @@ struct repo_path_cache {
|
||||
char *merge_head;
|
||||
char *fetch_head;
|
||||
char *shallow;
|
||||
char *merge_labels;
|
||||
};
|
||||
|
||||
struct repository {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
'
|
||||
|
||||
|
||||
Reference in new issue
Block a user