merge-recursive.[ch]: thoroughly debug these

As a wise man once told me, "Deleted code is debugged code!"  So, move
the functions that are shared between merge-recursive and merge-ort from
the former to the latter, and then debug the remainder of
merge-recursive.[ch].

Joking aside, merge-ort was always intended to replace merge-recursive.
It has numerous advantages over merge-recursive (operates much faster,
can operate without a worktree or index, and fixes a number of known
bugs and suboptimal merges).  Since we have now replaced all callers of
merge-recursive with equivalent functions from merge-ort, move the
shared functions from the former to the latter, and delete the remainder
of merge-recursive.[ch].

Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
main
Elijah Newren 2025-04-08 15:48:40 +00:00 committed by Junio C Hamano
parent 75cd9ae05f
commit ad45b327c0
9 changed files with 225 additions and 4236 deletions

View File

@ -109,23 +109,11 @@ subtree[=<path>];;
two trees to match.

recursive::
This can only resolve two heads using a 3-way merge
algorithm. When there is more than one common
ancestor that can be used for 3-way merge, it creates a
merged tree of the common ancestors and uses that as
the reference tree for the 3-way merge. This has been
reported to result in fewer merge conflicts without
causing mismerges by tests done on actual merge commits
taken from Linux 2.6 kernel development history.
Additionally this can detect and handle merges involving
renames. It does not make use of detected copies. This was
the default strategy for resolving two heads from Git v0.99.9k
until v2.33.0.
+
For a path that is a submodule, the same caution as 'ort' applies to this
strategy.
+
The 'recursive' strategy takes the same options as 'ort'.
This is now a synonym for `ort`. It was an alternative
implementation until v2.49.0, but was redirected to mean `ort`
in v2.50.0. The previous recursive strategy was the default
strategy for resolving two heads from Git v0.99.9k until
v2.33.0.

resolve::
This can only resolve two heads (i.e. the current branch
@ -146,7 +134,7 @@ ours::
ignoring all changes from all other branches. It is meant to
be used to supersede old development history of side
branches. Note that this is different from the -Xours option to
the 'recursive' merge strategy.
the 'ort' merge strategy.

subtree::
This is a modified `ort` strategy. When merging trees A and

View File

@ -356,8 +356,6 @@ understanding these differences can be beneficial.
The behavior for these commands somewhat depends upon the merge
strategy being used:
* `ort` behaves as described above
* `recursive` tries to not vivify files unnecessarily, but does sometimes
vivify files without conflicts.
* `octopus` and `resolve` will always vivify any file changed in the merge
relative to the first parent, which is rather suboptimal.


View File

@ -1069,7 +1069,6 @@ LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-ll.o
LIB_OBJS += merge-ort.o
LIB_OBJS += merge-ort-wrappers.o
LIB_OBJS += merge-recursive.o
LIB_OBJS += merge.o
LIB_OBJS += midx.o
LIB_OBJS += midx-write.o

View File

@ -1,7 +1,7 @@
#ifndef MERGE_ORT_WRAPPERS_H
#define MERGE_ORT_WRAPPERS_H

#include "merge-recursive.h"
#include "merge-ort.h"

/*
* rename-detecting three-way merge, no recursion.

View File

@ -26,6 +26,7 @@
#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
#include "config.h"
#include "diff.h"
#include "diffcore.h"
#include "dir.h"
@ -5322,3 +5323,161 @@ void merge_incore_recursive(struct merge_options *opt,
merge_ort_internal(opt, merge_bases, side1, side2, result);
trace2_region_leave("merge", "incore_recursive", opt->repo);
}

static void merge_recursive_config(struct merge_options *opt, int ui)
{
char *value = NULL;
int renormalize = 0;
git_config_get_int("merge.verbosity", &opt->verbosity);
git_config_get_int("diff.renamelimit", &opt->rename_limit);
git_config_get_int("merge.renamelimit", &opt->rename_limit);
git_config_get_bool("merge.renormalize", &renormalize);
opt->renormalize = renormalize;
if (!git_config_get_string("diff.renames", &value)) {
opt->detect_renames = git_config_rename("diff.renames", value);
free(value);
}
if (!git_config_get_string("merge.renames", &value)) {
opt->detect_renames = git_config_rename("merge.renames", value);
free(value);
}
if (!git_config_get_string("merge.directoryrenames", &value)) {
int boolval = git_parse_maybe_bool(value);
if (0 <= boolval) {
opt->detect_directory_renames = boolval ?
MERGE_DIRECTORY_RENAMES_TRUE :
MERGE_DIRECTORY_RENAMES_NONE;
} else if (!strcasecmp(value, "conflict")) {
opt->detect_directory_renames =
MERGE_DIRECTORY_RENAMES_CONFLICT;
} /* avoid erroring on values from future versions of git */
free(value);
}
if (ui) {
if (!git_config_get_string("diff.algorithm", &value)) {
long diff_algorithm = parse_algorithm_value(value);
if (diff_algorithm < 0)
die(_("unknown value for config '%s': %s"), "diff.algorithm", value);
opt->xdl_opts = (opt->xdl_opts & ~XDF_DIFF_ALGORITHM_MASK) | diff_algorithm;
free(value);
}
}
git_config(git_xmerge_config, NULL);
}

static void init_merge_options(struct merge_options *opt,
struct repository *repo, int ui)
{
const char *merge_verbosity;
memset(opt, 0, sizeof(struct merge_options));

opt->repo = repo;

opt->detect_renames = -1;
opt->detect_directory_renames = MERGE_DIRECTORY_RENAMES_CONFLICT;
opt->rename_limit = -1;

opt->verbosity = 2;
opt->buffer_output = 1;
strbuf_init(&opt->obuf, 0);

opt->renormalize = 0;

opt->conflict_style = -1;
opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF);

merge_recursive_config(opt, ui);
merge_verbosity = getenv("GIT_MERGE_VERBOSITY");
if (merge_verbosity)
opt->verbosity = strtol(merge_verbosity, NULL, 10);
if (opt->verbosity >= 5)
opt->buffer_output = 0;
}

void init_ui_merge_options(struct merge_options *opt,
struct repository *repo)
{
init_merge_options(opt, repo, 1);
}

void init_basic_merge_options(struct merge_options *opt,
struct repository *repo)
{
init_merge_options(opt, repo, 0);
}

/*
* For now, members of merge_options do not need deep copying, but
* it may change in the future, in which case we would need to update
* this, and also make a matching change to clear_merge_options() to
* release the resources held by a copied instance.
*/
void copy_merge_options(struct merge_options *dst, struct merge_options *src)
{
*dst = *src;
}

void clear_merge_options(struct merge_options *opt UNUSED)
{
; /* no-op as our copy is shallow right now */
}

int parse_merge_opt(struct merge_options *opt, const char *s)
{
const char *arg;

if (!s || !*s)
return -1;
if (!strcmp(s, "ours"))
opt->recursive_variant = MERGE_VARIANT_OURS;
else if (!strcmp(s, "theirs"))
opt->recursive_variant = MERGE_VARIANT_THEIRS;
else if (!strcmp(s, "subtree"))
opt->subtree_shift = "";
else if (skip_prefix(s, "subtree=", &arg))
opt->subtree_shift = arg;
else if (!strcmp(s, "patience"))
opt->xdl_opts = DIFF_WITH_ALG(opt, PATIENCE_DIFF);
else if (!strcmp(s, "histogram"))
opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF);
else if (skip_prefix(s, "diff-algorithm=", &arg)) {
long value = parse_algorithm_value(arg);
if (value < 0)
return -1;
/* clear out previous settings */
DIFF_XDL_CLR(opt, NEED_MINIMAL);
opt->xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK;
opt->xdl_opts |= value;
}
else if (!strcmp(s, "ignore-space-change"))
DIFF_XDL_SET(opt, IGNORE_WHITESPACE_CHANGE);
else if (!strcmp(s, "ignore-all-space"))
DIFF_XDL_SET(opt, IGNORE_WHITESPACE);
else if (!strcmp(s, "ignore-space-at-eol"))
DIFF_XDL_SET(opt, IGNORE_WHITESPACE_AT_EOL);
else if (!strcmp(s, "ignore-cr-at-eol"))
DIFF_XDL_SET(opt, IGNORE_CR_AT_EOL);
else if (!strcmp(s, "renormalize"))
opt->renormalize = 1;
else if (!strcmp(s, "no-renormalize"))
opt->renormalize = 0;
else if (!strcmp(s, "no-renames"))
opt->detect_renames = 0;
else if (!strcmp(s, "find-renames")) {
opt->detect_renames = 1;
opt->rename_score = 0;
}
else if (skip_prefix(s, "find-renames=", &arg) ||
skip_prefix(s, "rename-threshold=", &arg)) {
if ((opt->rename_score = parse_rename_score(&arg)) == -1 || *arg != 0)
return -1;
opt->detect_renames = 1;
}
/*
* Please update $__git_merge_strategy_options in
* git-completion.bash when you add new options
*/
else
return -1;
return 0;
}

View File

@ -1,10 +1,11 @@
#ifndef MERGE_ORT_H
#define MERGE_ORT_H

#include "merge-recursive.h"
#include "hash.h"
#include "strbuf.h"

struct commit;
struct commit_list;
struct tree;
struct strmap;

@ -44,6 +45,51 @@ struct merge_result {
unsigned _properly_initialized;
};

struct merge_options_internal;
struct merge_options {
struct repository *repo;

/* ref names used in console messages and conflict markers */
const char *ancestor;
const char *branch1;
const char *branch2;

/* rename related options */
int detect_renames;
enum {
MERGE_DIRECTORY_RENAMES_NONE = 0,
MERGE_DIRECTORY_RENAMES_CONFLICT = 1,
MERGE_DIRECTORY_RENAMES_TRUE = 2
} detect_directory_renames;
int rename_limit;
int rename_score;
int show_rename_progress;

/* xdiff-related options (patience, ignore whitespace, ours/theirs) */
long xdl_opts;
int conflict_style;
enum {
MERGE_VARIANT_NORMAL = 0,
MERGE_VARIANT_OURS,
MERGE_VARIANT_THEIRS
} recursive_variant;

/* console output related options */
int verbosity;
unsigned buffer_output; /* 1: output at end, 2: keep buffered */
struct strbuf obuf; /* output buffer; if buffer_output == 2, caller
* must handle and call strbuf_release */

/* miscellaneous control options */
const char *subtree_shift;
unsigned renormalize : 1;
unsigned record_conflict_msgs_as_headers : 1;
const char *msg_header_prefix;

/* internal fields used by the implementation */
struct merge_options_internal *priv;
};

/* Mostly internal function also used by merge-ort-wrappers.c */
struct commit *make_virtual_commit(struct repository *repo,
struct tree *tree,
@ -119,4 +165,16 @@ void merge_get_conflicted_files(struct merge_result *result,
void merge_finalize(struct merge_options *opt,
struct merge_result *result);


/* for use by porcelain commands */
void init_ui_merge_options(struct merge_options *opt, struct repository *repo);
/* for use by plumbing commands */
void init_basic_merge_options(struct merge_options *opt, struct repository *repo);

void copy_merge_options(struct merge_options *dst, struct merge_options *src);
void clear_merge_options(struct merge_options *opt);

/* parse the option in s and update the relevant field of opt */
int parse_merge_opt(struct merge_options *opt, const char *s);

#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,132 +0,0 @@
#ifndef MERGE_RECURSIVE_H
#define MERGE_RECURSIVE_H

#include "strbuf.h"

struct commit;
struct commit_list;
struct object_id;
struct repository;
struct tree;

struct merge_options_internal;
struct merge_options {
struct repository *repo;

/* ref names used in console messages and conflict markers */
const char *ancestor;
const char *branch1;
const char *branch2;

/* rename related options */
int detect_renames;
enum {
MERGE_DIRECTORY_RENAMES_NONE = 0,
MERGE_DIRECTORY_RENAMES_CONFLICT = 1,
MERGE_DIRECTORY_RENAMES_TRUE = 2
} detect_directory_renames;
int rename_limit;
int rename_score;
int show_rename_progress;

/* xdiff-related options (patience, ignore whitespace, ours/theirs) */
long xdl_opts;
int conflict_style;
enum {
MERGE_VARIANT_NORMAL = 0,
MERGE_VARIANT_OURS,
MERGE_VARIANT_THEIRS
} recursive_variant;

/* console output related options */
int verbosity;
unsigned buffer_output; /* 1: output at end, 2: keep buffered */
struct strbuf obuf; /* output buffer; if buffer_output == 2, caller
* must handle and call strbuf_release */

/* miscellaneous control options */
const char *subtree_shift;
unsigned renormalize : 1;
unsigned record_conflict_msgs_as_headers : 1;
const char *msg_header_prefix;

/* internal fields used by the implementation */
struct merge_options_internal *priv;
};

/* for use by porcelain commands */
void init_ui_merge_options(struct merge_options *opt, struct repository *repo);
/* for use by plumbing commands */
void init_basic_merge_options(struct merge_options *opt, struct repository *repo);

void copy_merge_options(struct merge_options *dst, struct merge_options *src);
void clear_merge_options(struct merge_options *opt);

/* parse the option in s and update the relevant field of opt */
int parse_merge_opt(struct merge_options *opt, const char *s);

/*
* RETURN VALUES: All the merge_* functions below return a value as follows:
* > 0 Merge was clean
* = 0 Merge had conflicts
* < 0 Merge hit an unexpected and unrecoverable problem (e.g. disk
* full) and aborted merge part-way through.
*/

/*
* rename-detecting three-way merge, no recursion.
*
* Outputs:
* - See RETURN VALUES above
* - opt->repo->index has the new index
* - new index NOT written to disk
* - The working tree is updated with results of the merge
*/
int merge_trees(struct merge_options *opt,
struct tree *head,
struct tree *merge,
struct tree *merge_base);

/*
* merge_recursive is like merge_trees() but with recursive ancestor
* consolidation.
*
* NOTE: empirically, about a decade ago it was determined that with more
* than two merge bases, optimal behavior was found when the
* merge_bases were passed in the order of oldest commit to newest
* commit. Also, merge_bases will be consumed (emptied) so make a
* copy if you need it.
*
* Outputs:
* - See RETURN VALUES above
* - *result is treated as scratch space for temporary recursive merges
* - opt->repo->index has the new index
* - new index NOT written to disk
* - The working tree is updated with results of the merge
*/
int merge_recursive(struct merge_options *opt,
struct commit *h1,
struct commit *h2,
const struct commit_list *merge_bases,
struct commit **result);

/*
* merge_recursive_generic can operate on trees instead of commits, by
* wrapping the trees into virtual commits, and calling merge_recursive().
* It also writes out the in-memory index to disk if the merge is successful.
*
* Outputs:
* - See RETURN VALUES above
* - *result is treated as scratch space for temporary recursive merges
* - opt->repo->index has the new index
* - new index also written to $GIT_INDEX_FILE on disk
* - The working tree is updated with results of the merge
*/
int merge_recursive_generic(struct merge_options *opt,
const struct object_id *head,
const struct object_id *merge,
int num_merge_bases,
const struct object_id *merge_bases,
struct commit **result);

#endif

View File

@ -338,7 +338,6 @@ libgit_sources = [
'merge-ll.c',
'merge-ort.c',
'merge-ort-wrappers.c',
'merge-recursive.c',
'merge.c',
'midx.c',
'midx-write.c',