Merge branch 'en/merge-ort-prepare-to-remove-recursive'

First step of deprecating and removing merge-recursive.

* en/merge-ort-prepare-to-remove-recursive:
  am: switch from merge_recursive_generic() to merge_ort_generic()
  merge-ort: fix merge.directoryRenames=false
  t3650: document bug when directory renames are turned off
  merge-ort: support having merge verbosity be set to 0
  merge-ort: allow rename detection to be disabled
  merge-ort: add new merge_ort_generic() function
maint
Junio C Hamano 2025-03-29 16:39:07 +09:00
commit eb7923be1f
11 changed files with 172 additions and 20 deletions

View File

@ -82,6 +82,11 @@ find-renames[=<n>];;
rename-threshold=<n>;;
Deprecated synonym for `find-renames=<n>`.

no-renames;;
Turn off rename detection. This overrides the `merge.renames`
configuration variable.
See also linkgit:git-diff[1] `--no-renames`.

subtree[=<path>];;
This option is a more advanced form of 'subtree' strategy, where
the strategy makes a guess on how two trees must be shifted to
@ -107,7 +112,7 @@ 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'. However,
there are three additional options that 'ort' ignores (not documented
there are two additional options that 'ort' ignores (not documented
above) that are potentially useful with the 'recursive' strategy:

patience;;
@ -121,11 +126,6 @@ diff-algorithm=[patience|minimal|histogram|myers];;
specifically uses `diff-algorithm=histogram`, while `recursive`
defaults to the `diff.algorithm` config setting.

no-renames;;
Turn off rename detection. This overrides the `merge.renames`
configuration variable.
See also linkgit:git-diff[1] `--no-renames`.

resolve::
This can only resolve two heads (i.e. the current branch
and another branch you pulled from) using a 3-way merge

View File

@ -31,7 +31,7 @@
#include "preload-index.h"
#include "sequencer.h"
#include "revision.h"
#include "merge-recursive.h"
#include "merge-ort-wrappers.h"
#include "log-tree.h"
#include "notes-utils.h"
#include "rerere.h"
@ -1638,12 +1638,13 @@ static int fall_back_threeway(const struct am_state *state, const char *index_pa
o.branch1 = "HEAD";
their_tree_name = xstrfmt("%.*s", linelen(state->msg), state->msg);
o.branch2 = their_tree_name;
o.ancestor = "constructed fake ancestor";
o.detect_directory_renames = MERGE_DIRECTORY_RENAMES_NONE;

if (state->quiet)
o.verbosity = 0;

if (merge_recursive_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {
if (merge_ort_generic(&o, &our_tree, &their_tree, 1, bases, &result)) {
repo_rerere(the_repository, state->allow_rerere_autoupdate);
free(their_tree_name);
return error(_("Failed to merge in the changes."));

View File

@ -1,9 +1,13 @@
#include "git-compat-util.h"
#include "gettext.h"
#include "hash.h"
#include "hex.h"
#include "lockfile.h"
#include "merge-ort.h"
#include "merge-ort-wrappers.h"
#include "read-cache-ll.h"
#include "repository.h"
#include "tag.h"
#include "tree.h"

#include "commit.h"
@ -29,6 +33,7 @@ int merge_ort_nonrecursive(struct merge_options *opt,
struct tree *merge_base)
{
struct merge_result result;
int show_msgs;

if (unclean(opt, head))
return -1;
@ -38,9 +43,10 @@ int merge_ort_nonrecursive(struct merge_options *opt,
return 1;
}

show_msgs = !!opt->verbosity;
memset(&result, 0, sizeof(result));
merge_incore_nonrecursive(opt, merge_base, head, merge, &result);
merge_switch_to_result(opt, head, &result, 1, 1);
merge_switch_to_result(opt, head, &result, 1, show_msgs);

return result.clean;
}
@ -53,14 +59,76 @@ int merge_ort_recursive(struct merge_options *opt,
{
struct tree *head = repo_get_commit_tree(opt->repo, side1);
struct merge_result tmp;
int show_msgs;

if (unclean(opt, head))
return -1;

show_msgs = !!opt->verbosity;
memset(&tmp, 0, sizeof(tmp));
merge_incore_recursive(opt, merge_bases, side1, side2, &tmp);
merge_switch_to_result(opt, head, &tmp, 1, 1);
merge_switch_to_result(opt, head, &tmp, 1, show_msgs);
*result = NULL;

return tmp.clean;
}

static struct commit *get_ref(struct repository *repo,
const struct object_id *oid,
const char *name)
{
struct object *object;

object = deref_tag(repo, parse_object(repo, oid),
name, strlen(name));
if (!object)
return NULL;
if (object->type == OBJ_TREE)
return make_virtual_commit(repo, (struct tree*)object, name);
if (object->type != OBJ_COMMIT)
return NULL;
if (repo_parse_commit(repo, (struct commit *)object))
return NULL;
return (struct commit *)object;
}

int merge_ort_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)
{
int clean;
struct lock_file lock = LOCK_INIT;
struct commit *head_commit = get_ref(opt->repo, head, opt->branch1);
struct commit *next_commit = get_ref(opt->repo, merge, opt->branch2);
struct commit_list *ca = NULL;

if (merge_bases) {
int i;
for (i = 0; i < num_merge_bases; ++i) {
struct commit *base;
if (!(base = get_ref(opt->repo, &merge_bases[i],
oid_to_hex(&merge_bases[i]))))
return error(_("Could not parse object '%s'"),
oid_to_hex(&merge_bases[i]));
commit_list_insert(base, &ca);
}
}

repo_hold_locked_index(opt->repo, &lock, LOCK_DIE_ON_ERROR);
clean = merge_ort_recursive(opt, head_commit, next_commit, ca,
result);
free_commit_list(ca);
if (clean < 0) {
rollback_lock_file(&lock);
return clean;
}

if (write_locked_index(opt->repo->index, &lock,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
return error(_("Unable to write index."));

return clean ? 0 : 1;
}

View File

@ -22,4 +22,16 @@ int merge_ort_recursive(struct merge_options *opt,
const struct commit_list *ancestors,
struct commit **result);

/*
* rename-detecting three-way merge. num_merge_bases must be at least 1.
* Recursive ancestor consolidation will be performed if num_merge_bases > 1.
* Wrapper mimicking the old merge_recursive_generic() function.
*/
int merge_ort_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

@ -3405,6 +3405,11 @@ static int collect_renames(struct merge_options *opt,
pool_diff_free_filepair(&opt->priv->pool, p);
continue;
}
if (opt->detect_directory_renames == MERGE_DIRECTORY_RENAMES_NONE &&
p->status == 'R' && 1) {
possibly_cache_new_pair(renames, p, side_index, NULL);
goto skip_directory_renames;
}

new_path = check_for_directory_rename(opt, p->two->path,
side_index,
@ -3422,6 +3427,7 @@ static int collect_renames(struct merge_options *opt,
if (new_path)
apply_directory_rename_modifications(opt, p, new_path);

skip_directory_renames:
/*
* p->score comes back from diffcore_rename_extended() with
* the similarity of the renamed file. The similarity is
@ -3449,6 +3455,11 @@ static int detect_and_process_renames(struct merge_options *opt)

if (!possible_renames(renames))
goto cleanup;
if (!opt->detect_renames) {
renames->redo_after_renames = 0;
renames->cached_pairs_valid_side = 0;
goto cleanup;
}

trace2_region_enter("merge", "regular renames", opt->repo);
detection_run |= detect_regular_renames(opt, MERGE_SIDE1);
@ -4879,9 +4890,9 @@ static inline void set_commit_tree(struct commit *c, struct tree *t)
c->maybe_tree = t;
}

static struct commit *make_virtual_commit(struct repository *repo,
struct tree *tree,
const char *comment)
struct commit *make_virtual_commit(struct repository *repo,
struct tree *tree,
const char *comment)
{
struct commit *commit = alloc_commit_node(repo);

@ -5021,7 +5032,8 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
trace2_region_leave("merge", "allocate/init", opt->repo);
}

static void merge_check_renames_reusable(struct merge_result *result,
static void merge_check_renames_reusable(struct merge_options *opt,
struct merge_result *result,
struct tree *merge_base,
struct tree *side1,
struct tree *side2)
@ -5046,6 +5058,26 @@ static void merge_check_renames_reusable(struct merge_result *result,
return;
}

/*
* Avoid using cached renames when directory rename detection is
* turned off. Cached renames are far less important in that case,
* and they lead to testcases with an interesting intersection of
* effects from relevant renames optimization, trivial directory
* resolution optimization, and cached renames all converging when
* the target of a cached rename is in a directory that
* collect_merge_info() does not recurse into. To avoid such
* problems, simply disable cached renames for this case (similar
* to the rename/rename(1to1) case; see the "disabling the
* optimization" comment near that case).
*
* This could be revisited in the future; see the commit message
* where this comment was added for some possible pointers.
*/
if (opt->detect_directory_renames == MERGE_DIRECTORY_RENAMES_NONE) {
renames->cached_pairs_valid_side = 0; /* neither side valid */
return;
}

/*
* Handle other cases; note that merge_trees[0..2] will only
* be NULL if opti is, or if all three were manually set to
@ -5187,6 +5219,8 @@ static void merge_ort_internal(struct merge_options *opt,
ancestor_name = "empty tree";
} else if (merge_bases) {
ancestor_name = "merged common ancestors";
} else if (opt->ancestor) {
ancestor_name = opt->ancestor;
} else {
strbuf_add_unique_abbrev(&merge_base_abbrev,
&merged_merge_bases->object.oid,
@ -5252,7 +5286,7 @@ void merge_incore_nonrecursive(struct merge_options *opt,

trace2_region_enter("merge", "merge_start", opt->repo);
assert(opt->ancestor != NULL);
merge_check_renames_reusable(result, merge_base, side1, side2);
merge_check_renames_reusable(opt, result, merge_base, side1, side2);
merge_start(opt, result);
/*
* Record the trees used in this merge, so if there's a next merge in
@ -5276,8 +5310,13 @@ void merge_incore_recursive(struct merge_options *opt,
{
trace2_region_enter("merge", "incore_recursive", opt->repo);

/* We set the ancestor label based on the merge_bases */
assert(opt->ancestor == NULL);
/*
* We set the ancestor label based on the merge_bases...but we
* allow one exception through so that builtin/am can override
* with its constructed fake ancestor.
*/
assert(opt->ancestor == NULL ||
(merge_bases && !merge_bases->next));

trace2_region_enter("merge", "merge_start", opt->repo);
merge_start(opt, result);

View File

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

/* Mostly internal function also used by merge-ort-wrappers.c */
struct commit *make_virtual_commit(struct repository *repo,
struct tree *tree,
const char *comment);

/*
* rename-detecting three-way merge with recursive ancestor consolidation.
* working tree and index are untouched.

View File

@ -195,4 +195,26 @@ test_expect_success 'using replay on bare repo to rebase multiple divergent bran
done
'

test_expect_success 'merge.directoryRenames=false' '
# create a test case that stress-tests the rename caching
git switch -c rename-onto &&

mkdir -p to-rename &&
test_commit to-rename/move &&

mkdir -p renamed-directory &&
git mv to-rename/move* renamed-directory/ &&
test_tick &&
git commit -m renamed-directory &&

git switch -c rename-from HEAD^ &&
test_commit to-rename/add-a-file &&
echo modified >to-rename/add-a-file.t &&
test_tick &&
git commit -m modified to-rename/add-a-file.t &&

git -c merge.directoryRenames=false replay \
--onto rename-onto rename-onto..rename-from
'

test_done

View File

@ -112,7 +112,7 @@ test_expect_success 'am --abort will keep dirty index intact' '
test_expect_success 'am -3 stops on conflict on unborn branch' '
git checkout -f --orphan orphan &&
git reset &&
rm -f otherfile-4 &&
rm -f file-1 otherfile-4 &&
test_must_fail git am -3 0003-*.patch &&
test 2 -eq $(git ls-files -u | wc -l) &&
test 4 = "$(cat otherfile-4)"

View File

@ -19,7 +19,6 @@ am_3way () {
$2 git am --3way patch
}

KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
test_submodule_switch_func "am_3way"

test_expect_success 'setup diff.submodule' '

View File

@ -73,6 +73,12 @@ test_expect_success 'Clean merge' '
test_cmp expect actual
'

# Repeat the previous test, but turn off rename detection
test_expect_success 'Failed merge without rename detection' '
test_must_fail git -c diff.renames=false merge-tree --write-tree side1 side3 >out &&
grep "CONFLICT (modify/delete): numbers deleted" out
'

test_expect_success 'Content merge and a few conflicts' '
git checkout side1^0 &&
test_must_fail git merge side2 &&

View File

@ -207,7 +207,7 @@ test_expect_success 'rebase --apply describes fake ancestor base' '
cd rebase &&
git rebase --abort &&
test_must_fail git -c merge.conflictstyle=diff3 rebase --apply main &&
grep "||||||| constructed merge base" file
grep "||||||| constructed fake ancestor" file
)
'