Merge branch 'en/diffcore-rename'
Performance optimization work on the rename detection continues. * en/diffcore-rename: merge-ort: call diffcore_rename() directly gitdiffcore doc: mention new preliminary step for rename detection diffcore-rename: guide inexact rename detection based on basenames diffcore-rename: complete find_basename_matches() diffcore-rename: compute basenames of source and dest candidates t4001: add a test comparing basename similarity and content similarity diffcore-rename: filter rename_src list when possible diffcore-rename: no point trying to find a match better than exact
This commit is contained in:
commit
12bd17521c
@ -169,6 +169,26 @@ a similarity score different from the default of 50% by giving a
|
|||||||
number after the "-M" or "-C" option (e.g. "-M8" to tell it to use
|
number after the "-M" or "-C" option (e.g. "-M8" to tell it to use
|
||||||
8/10 = 80%).
|
8/10 = 80%).
|
||||||
|
|
||||||
|
Note that when rename detection is on but both copy and break
|
||||||
|
detection are off, rename detection adds a preliminary step that first
|
||||||
|
checks if files are moved across directories while keeping their
|
||||||
|
filename the same. If there is a file added to a directory whose
|
||||||
|
contents is sufficiently similar to a file with the same name that got
|
||||||
|
deleted from a different directory, it will mark them as renames and
|
||||||
|
exclude them from the later quadratic step (the one that pairwise
|
||||||
|
compares all unmatched files to find the "best" matches, determined by
|
||||||
|
the highest content similarity). So, for example, if a deleted
|
||||||
|
docs/ext.txt and an added docs/config/ext.txt are similar enough, they
|
||||||
|
will be marked as a rename and prevent an added docs/ext.md that may
|
||||||
|
be even more similar to the deleted docs/ext.txt from being considered
|
||||||
|
as the rename destination in the later step. For this reason, the
|
||||||
|
preliminary "match same filename" step uses a bit higher threshold to
|
||||||
|
mark a file pair as a rename and stop considering other candidates for
|
||||||
|
better matches. At most, one comparison is done per file in this
|
||||||
|
preliminary pass; so if there are several remaining ext.txt files
|
||||||
|
throughout the directory hierarchy after exact rename detection, this
|
||||||
|
preliminary step will be skipped for those files.
|
||||||
|
|
||||||
Note. When the "-C" option is used with `--find-copies-harder`
|
Note. When the "-C" option is used with `--find-copies-harder`
|
||||||
option, 'git diff-{asterisk}' commands feed unmodified filepairs to
|
option, 'git diff-{asterisk}' commands feed unmodified filepairs to
|
||||||
diffcore mechanism as well as modified ones. This lets the copy
|
diffcore mechanism as well as modified ones. This lets the copy
|
||||||
|
@ -367,6 +367,144 @@ static int find_exact_renames(struct diff_options *options)
|
|||||||
return renames;
|
return renames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *get_basename(const char *filename)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* gitbasename() has to worry about special drives, multiple
|
||||||
|
* directory separator characters, trailing slashes, NULL or
|
||||||
|
* empty strings, etc. We only work on filenames as stored in
|
||||||
|
* git, and thus get to ignore all those complications.
|
||||||
|
*/
|
||||||
|
const char *base = strrchr(filename, '/');
|
||||||
|
return base ? base + 1 : filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int find_basename_matches(struct diff_options *options,
|
||||||
|
int minimum_score)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* When I checked in early 2020, over 76% of file renames in linux
|
||||||
|
* just moved files to a different directory but kept the same
|
||||||
|
* basename. gcc did that with over 64% of renames, gecko did it
|
||||||
|
* with over 79%, and WebKit did it with over 89%.
|
||||||
|
*
|
||||||
|
* Therefore we can bypass the normal exhaustive NxM matrix
|
||||||
|
* comparison of similarities between all potential rename sources
|
||||||
|
* and destinations by instead using file basename as a hint (i.e.
|
||||||
|
* the portion of the filename after the last '/'), checking for
|
||||||
|
* similarity between files with the same basename, and if we find
|
||||||
|
* a pair that are sufficiently similar, record the rename pair and
|
||||||
|
* exclude those two from the NxM matrix.
|
||||||
|
*
|
||||||
|
* This *might* cause us to find a less than optimal pairing (if
|
||||||
|
* there is another file that we are even more similar to but has a
|
||||||
|
* different basename). Given the huge performance advantage
|
||||||
|
* basename matching provides, and given the frequency with which
|
||||||
|
* people use the same basename in real world projects, that's a
|
||||||
|
* trade-off we are willing to accept when doing just rename
|
||||||
|
* detection.
|
||||||
|
*
|
||||||
|
* If someone wants copy detection that implies they are willing to
|
||||||
|
* spend more cycles to find similarities between files, so it may
|
||||||
|
* be less likely that this heuristic is wanted. If someone is
|
||||||
|
* doing break detection, that means they do not want filename
|
||||||
|
* similarity to imply any form of content similiarity, and thus
|
||||||
|
* this heuristic would definitely be incompatible.
|
||||||
|
*/
|
||||||
|
|
||||||
|
int i, renames = 0;
|
||||||
|
struct strintmap sources;
|
||||||
|
struct strintmap dests;
|
||||||
|
struct hashmap_iter iter;
|
||||||
|
struct strmap_entry *entry;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The prefeteching stuff wants to know if it can skip prefetching
|
||||||
|
* blobs that are unmodified...and will then do a little extra work
|
||||||
|
* to verify that the oids are indeed different before prefetching.
|
||||||
|
* Unmodified blobs are only relevant when doing copy detection;
|
||||||
|
* when limiting to rename detection, diffcore_rename[_extended]()
|
||||||
|
* will never be called with unmodified source paths fed to us, so
|
||||||
|
* the extra work necessary to check if rename_src entries are
|
||||||
|
* unmodified would be a small waste.
|
||||||
|
*/
|
||||||
|
int skip_unmodified = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create maps of basename -> fullname(s) for remaining sources and
|
||||||
|
* dests.
|
||||||
|
*/
|
||||||
|
strintmap_init_with_options(&sources, -1, NULL, 0);
|
||||||
|
strintmap_init_with_options(&dests, -1, NULL, 0);
|
||||||
|
for (i = 0; i < rename_src_nr; ++i) {
|
||||||
|
char *filename = rename_src[i].p->one->path;
|
||||||
|
const char *base;
|
||||||
|
|
||||||
|
/* exact renames removed in remove_unneeded_paths_from_src() */
|
||||||
|
assert(!rename_src[i].p->one->rename_used);
|
||||||
|
|
||||||
|
/* Record index within rename_src (i) if basename is unique */
|
||||||
|
base = get_basename(filename);
|
||||||
|
if (strintmap_contains(&sources, base))
|
||||||
|
strintmap_set(&sources, base, -1);
|
||||||
|
else
|
||||||
|
strintmap_set(&sources, base, i);
|
||||||
|
}
|
||||||
|
for (i = 0; i < rename_dst_nr; ++i) {
|
||||||
|
char *filename = rename_dst[i].p->two->path;
|
||||||
|
const char *base;
|
||||||
|
|
||||||
|
if (rename_dst[i].is_rename)
|
||||||
|
continue; /* involved in exact match already. */
|
||||||
|
|
||||||
|
/* Record index within rename_dst (i) if basename is unique */
|
||||||
|
base = get_basename(filename);
|
||||||
|
if (strintmap_contains(&dests, base))
|
||||||
|
strintmap_set(&dests, base, -1);
|
||||||
|
else
|
||||||
|
strintmap_set(&dests, base, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now look for basename matchups and do similarity estimation */
|
||||||
|
strintmap_for_each_entry(&sources, &iter, entry) {
|
||||||
|
const char *base = entry->key;
|
||||||
|
intptr_t src_index = (intptr_t)entry->value;
|
||||||
|
intptr_t dst_index;
|
||||||
|
if (src_index == -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (0 <= (dst_index = strintmap_get(&dests, base))) {
|
||||||
|
struct diff_filespec *one, *two;
|
||||||
|
int score;
|
||||||
|
|
||||||
|
/* Estimate the similarity */
|
||||||
|
one = rename_src[src_index].p->one;
|
||||||
|
two = rename_dst[dst_index].p->two;
|
||||||
|
score = estimate_similarity(options->repo, one, two,
|
||||||
|
minimum_score, skip_unmodified);
|
||||||
|
|
||||||
|
/* If sufficiently similar, record as rename pair */
|
||||||
|
if (score < minimum_score)
|
||||||
|
continue;
|
||||||
|
record_rename_pair(dst_index, src_index, score);
|
||||||
|
renames++;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Found a rename so don't need text anymore; if we
|
||||||
|
* didn't find a rename, the filespec_blob would get
|
||||||
|
* re-used when doing the matrix of comparisons.
|
||||||
|
*/
|
||||||
|
diff_free_filespec_blob(one);
|
||||||
|
diff_free_filespec_blob(two);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strintmap_clear(&sources);
|
||||||
|
strintmap_clear(&dests);
|
||||||
|
|
||||||
|
return renames;
|
||||||
|
}
|
||||||
|
|
||||||
#define NUM_CANDIDATE_PER_DST 4
|
#define NUM_CANDIDATE_PER_DST 4
|
||||||
static void record_if_better(struct diff_score m[], struct diff_score *o)
|
static void record_if_better(struct diff_score m[], struct diff_score *o)
|
||||||
{
|
{
|
||||||
@ -454,6 +592,54 @@ static int find_renames(struct diff_score *mx, int dst_cnt, int minimum_score, i
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void remove_unneeded_paths_from_src(int detecting_copies)
|
||||||
|
{
|
||||||
|
int i, new_num_src;
|
||||||
|
|
||||||
|
if (detecting_copies)
|
||||||
|
return; /* nothing to remove */
|
||||||
|
if (break_idx)
|
||||||
|
return; /* culling incompatible with break detection */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note on reasons why we cull unneeded sources but not destinations:
|
||||||
|
* 1) Pairings are stored in rename_dst (not rename_src), which we
|
||||||
|
* need to keep around. So, we just can't cull rename_dst even
|
||||||
|
* if we wanted to. But doing so wouldn't help because...
|
||||||
|
*
|
||||||
|
* 2) There is a matrix pairwise comparison that follows the
|
||||||
|
* "Performing inexact rename detection" progress message.
|
||||||
|
* Iterating over the destinations is done in the outer loop,
|
||||||
|
* hence we only iterate over each of those once and we can
|
||||||
|
* easily skip the outer loop early if the destination isn't
|
||||||
|
* relevant. That's only one check per destination path to
|
||||||
|
* skip.
|
||||||
|
*
|
||||||
|
* By contrast, the sources are iterated in the inner loop; if
|
||||||
|
* we check whether a source can be skipped, then we'll be
|
||||||
|
* checking it N separate times, once for each destination.
|
||||||
|
* We don't want to have to iterate over known-not-needed
|
||||||
|
* sources N times each, so avoid that by removing the sources
|
||||||
|
* from rename_src here.
|
||||||
|
*/
|
||||||
|
for (i = 0, new_num_src = 0; i < rename_src_nr; i++) {
|
||||||
|
/*
|
||||||
|
* renames are stored in rename_dst, so if a rename has
|
||||||
|
* already been detected using this source, we can just
|
||||||
|
* remove the source knowing rename_dst has its info.
|
||||||
|
*/
|
||||||
|
if (rename_src[i].p->one->rename_used)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (new_num_src < i)
|
||||||
|
memcpy(&rename_src[new_num_src], &rename_src[i],
|
||||||
|
sizeof(struct diff_rename_src));
|
||||||
|
new_num_src++;
|
||||||
|
}
|
||||||
|
|
||||||
|
rename_src_nr = new_num_src;
|
||||||
|
}
|
||||||
|
|
||||||
void diffcore_rename(struct diff_options *options)
|
void diffcore_rename(struct diff_options *options)
|
||||||
{
|
{
|
||||||
int detect_rename = options->detect_rename;
|
int detect_rename = options->detect_rename;
|
||||||
@ -463,9 +649,11 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
struct diff_score *mx;
|
struct diff_score *mx;
|
||||||
int i, j, rename_count, skip_unmodified = 0;
|
int i, j, rename_count, skip_unmodified = 0;
|
||||||
int num_destinations, dst_cnt;
|
int num_destinations, dst_cnt;
|
||||||
|
int num_sources, want_copies;
|
||||||
struct progress *progress = NULL;
|
struct progress *progress = NULL;
|
||||||
|
|
||||||
trace2_region_enter("diff", "setup", options->repo);
|
trace2_region_enter("diff", "setup", options->repo);
|
||||||
|
want_copies = (detect_rename == DIFF_DETECT_COPY);
|
||||||
if (!minimum_score)
|
if (!minimum_score)
|
||||||
minimum_score = DEFAULT_RENAME_SCORE;
|
minimum_score = DEFAULT_RENAME_SCORE;
|
||||||
|
|
||||||
@ -502,7 +690,7 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
p->one->rename_used++;
|
p->one->rename_used++;
|
||||||
register_rename_src(p);
|
register_rename_src(p);
|
||||||
}
|
}
|
||||||
else if (detect_rename == DIFF_DETECT_COPY) {
|
else if (want_copies) {
|
||||||
/*
|
/*
|
||||||
* Increment the "rename_used" score by
|
* Increment the "rename_used" score by
|
||||||
* one, to indicate ourselves as a user.
|
* one, to indicate ourselves as a user.
|
||||||
@ -527,17 +715,60 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
if (minimum_score == MAX_SCORE)
|
if (minimum_score == MAX_SCORE)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
|
||||||
|
num_sources = rename_src_nr;
|
||||||
|
|
||||||
|
if (want_copies || break_idx) {
|
||||||
/*
|
/*
|
||||||
* Calculate how many renames are left (but all the source
|
* Cull sources:
|
||||||
* files still remain as options for rename/copies!)
|
* - remove ones corresponding to exact renames
|
||||||
*/
|
*/
|
||||||
|
trace2_region_enter("diff", "cull after exact", options->repo);
|
||||||
|
remove_unneeded_paths_from_src(want_copies);
|
||||||
|
trace2_region_leave("diff", "cull after exact", options->repo);
|
||||||
|
} else {
|
||||||
|
/* Determine minimum score to match basenames */
|
||||||
|
double factor = 0.5;
|
||||||
|
char *basename_factor = getenv("GIT_BASENAME_FACTOR");
|
||||||
|
int min_basename_score;
|
||||||
|
|
||||||
|
if (basename_factor)
|
||||||
|
factor = strtol(basename_factor, NULL, 10)/100.0;
|
||||||
|
assert(factor >= 0.0 && factor <= 1.0);
|
||||||
|
min_basename_score = minimum_score +
|
||||||
|
(int)(factor * (MAX_SCORE - minimum_score));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cull sources:
|
||||||
|
* - remove ones involved in renames (found via exact match)
|
||||||
|
*/
|
||||||
|
trace2_region_enter("diff", "cull after exact", options->repo);
|
||||||
|
remove_unneeded_paths_from_src(want_copies);
|
||||||
|
trace2_region_leave("diff", "cull after exact", options->repo);
|
||||||
|
|
||||||
|
/* Utilize file basenames to quickly find renames. */
|
||||||
|
trace2_region_enter("diff", "basename matches", options->repo);
|
||||||
|
rename_count += find_basename_matches(options,
|
||||||
|
min_basename_score);
|
||||||
|
trace2_region_leave("diff", "basename matches", options->repo);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cull sources, again:
|
||||||
|
* - remove ones involved in renames (found via basenames)
|
||||||
|
*/
|
||||||
|
trace2_region_enter("diff", "cull basename", options->repo);
|
||||||
|
remove_unneeded_paths_from_src(want_copies);
|
||||||
|
trace2_region_leave("diff", "cull basename", options->repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calculate how many rename destinations are left */
|
||||||
num_destinations = (rename_dst_nr - rename_count);
|
num_destinations = (rename_dst_nr - rename_count);
|
||||||
|
num_sources = rename_src_nr; /* rename_src_nr reflects lower number */
|
||||||
|
|
||||||
/* All done? */
|
/* All done? */
|
||||||
if (!num_destinations)
|
if (!num_destinations || !num_sources)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
|
||||||
switch (too_many_rename_candidates(num_destinations, rename_src_nr,
|
switch (too_many_rename_candidates(num_destinations, num_sources,
|
||||||
options)) {
|
options)) {
|
||||||
case 1:
|
case 1:
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
@ -553,7 +784,7 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
if (options->show_rename_progress) {
|
if (options->show_rename_progress) {
|
||||||
progress = start_delayed_progress(
|
progress = start_delayed_progress(
|
||||||
_("Performing inexact rename detection"),
|
_("Performing inexact rename detection"),
|
||||||
(uint64_t)num_destinations * (uint64_t)rename_src_nr);
|
(uint64_t)num_destinations * (uint64_t)num_sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
mx = xcalloc(st_mult(NUM_CANDIDATE_PER_DST, num_destinations),
|
mx = xcalloc(st_mult(NUM_CANDIDATE_PER_DST, num_destinations),
|
||||||
@ -563,7 +794,7 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
struct diff_score *m;
|
struct diff_score *m;
|
||||||
|
|
||||||
if (rename_dst[i].is_rename)
|
if (rename_dst[i].is_rename)
|
||||||
continue; /* dealt with exact match already. */
|
continue; /* exact or basename match already handled */
|
||||||
|
|
||||||
m = &mx[dst_cnt * NUM_CANDIDATE_PER_DST];
|
m = &mx[dst_cnt * NUM_CANDIDATE_PER_DST];
|
||||||
for (j = 0; j < NUM_CANDIDATE_PER_DST; j++)
|
for (j = 0; j < NUM_CANDIDATE_PER_DST; j++)
|
||||||
@ -573,6 +804,8 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
struct diff_filespec *one = rename_src[j].p->one;
|
struct diff_filespec *one = rename_src[j].p->one;
|
||||||
struct diff_score this_src;
|
struct diff_score this_src;
|
||||||
|
|
||||||
|
assert(!one->rename_used || want_copies || break_idx);
|
||||||
|
|
||||||
if (skip_unmodified &&
|
if (skip_unmodified &&
|
||||||
diff_unmodified_pair(rename_src[j].p))
|
diff_unmodified_pair(rename_src[j].p))
|
||||||
continue;
|
continue;
|
||||||
@ -594,7 +827,7 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
}
|
}
|
||||||
dst_cnt++;
|
dst_cnt++;
|
||||||
display_progress(progress,
|
display_progress(progress,
|
||||||
(uint64_t)dst_cnt * (uint64_t)rename_src_nr);
|
(uint64_t)dst_cnt * (uint64_t)num_sources);
|
||||||
}
|
}
|
||||||
stop_progress(&progress);
|
stop_progress(&progress);
|
||||||
|
|
||||||
@ -602,7 +835,7 @@ void diffcore_rename(struct diff_options *options)
|
|||||||
STABLE_QSORT(mx, dst_cnt * NUM_CANDIDATE_PER_DST, score_compare);
|
STABLE_QSORT(mx, dst_cnt * NUM_CANDIDATE_PER_DST, score_compare);
|
||||||
|
|
||||||
rename_count += find_renames(mx, dst_cnt, minimum_score, 0);
|
rename_count += find_renames(mx, dst_cnt, minimum_score, 0);
|
||||||
if (detect_rename == DIFF_DETECT_COPY)
|
if (want_copies)
|
||||||
rename_count += find_renames(mx, dst_cnt, minimum_score, 1);
|
rename_count += find_renames(mx, dst_cnt, minimum_score, 1);
|
||||||
free(mx);
|
free(mx);
|
||||||
trace2_region_leave("diff", "inexact renames", options->repo);
|
trace2_region_leave("diff", "inexact renames", options->repo);
|
||||||
|
66
merge-ort.c
66
merge-ort.c
@ -535,6 +535,23 @@ static void setup_path_info(struct merge_options *opt,
|
|||||||
result->util = mi;
|
result->util = mi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void add_pair(struct merge_options *opt,
|
||||||
|
struct name_entry *names,
|
||||||
|
const char *pathname,
|
||||||
|
unsigned side,
|
||||||
|
unsigned is_add /* if false, is_delete */)
|
||||||
|
{
|
||||||
|
struct diff_filespec *one, *two;
|
||||||
|
struct rename_info *renames = &opt->priv->renames;
|
||||||
|
int names_idx = is_add ? side : 0;
|
||||||
|
|
||||||
|
one = alloc_filespec(pathname);
|
||||||
|
two = alloc_filespec(pathname);
|
||||||
|
fill_filespec(is_add ? two : one,
|
||||||
|
&names[names_idx].oid, 1, names[names_idx].mode);
|
||||||
|
diff_queue(&renames->pairs[side], one, two);
|
||||||
|
}
|
||||||
|
|
||||||
static void collect_rename_info(struct merge_options *opt,
|
static void collect_rename_info(struct merge_options *opt,
|
||||||
struct name_entry *names,
|
struct name_entry *names,
|
||||||
const char *dirname,
|
const char *dirname,
|
||||||
@ -544,6 +561,7 @@ static void collect_rename_info(struct merge_options *opt,
|
|||||||
unsigned match_mask)
|
unsigned match_mask)
|
||||||
{
|
{
|
||||||
struct rename_info *renames = &opt->priv->renames;
|
struct rename_info *renames = &opt->priv->renames;
|
||||||
|
unsigned side;
|
||||||
|
|
||||||
/* Update dirs_removed, as needed */
|
/* Update dirs_removed, as needed */
|
||||||
if (dirmask == 1 || dirmask == 3 || dirmask == 5) {
|
if (dirmask == 1 || dirmask == 3 || dirmask == 5) {
|
||||||
@ -554,6 +572,21 @@ static void collect_rename_info(struct merge_options *opt,
|
|||||||
if (sides & 2)
|
if (sides & 2)
|
||||||
strset_add(&renames->dirs_removed[2], fullname);
|
strset_add(&renames->dirs_removed[2], fullname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filemask == 0 || filemask == 7)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (side = MERGE_SIDE1; side <= MERGE_SIDE2; ++side) {
|
||||||
|
unsigned side_mask = (1 << side);
|
||||||
|
|
||||||
|
/* Check for deletion on side */
|
||||||
|
if ((filemask & 1) && !(filemask & side_mask))
|
||||||
|
add_pair(opt, names, fullname, side, 0 /* delete */);
|
||||||
|
|
||||||
|
/* Check for addition on side */
|
||||||
|
if (!(filemask & 1) && (filemask & side_mask))
|
||||||
|
add_pair(opt, names, fullname, side, 1 /* add */);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int collect_merge_info_callback(int n,
|
static int collect_merge_info_callback(int n,
|
||||||
@ -2079,6 +2112,27 @@ static int process_renames(struct merge_options *opt,
|
|||||||
return clean_merge;
|
return clean_merge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void resolve_diffpair_statuses(struct diff_queue_struct *q)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* A simplified version of diff_resolve_rename_copy(); would probably
|
||||||
|
* just use that function but it's static...
|
||||||
|
*/
|
||||||
|
int i;
|
||||||
|
struct diff_filepair *p;
|
||||||
|
|
||||||
|
for (i = 0; i < q->nr; ++i) {
|
||||||
|
p = q->queue[i];
|
||||||
|
p->status = 0; /* undecided */
|
||||||
|
if (!DIFF_FILE_VALID(p->one))
|
||||||
|
p->status = DIFF_STATUS_ADDED;
|
||||||
|
else if (!DIFF_FILE_VALID(p->two))
|
||||||
|
p->status = DIFF_STATUS_DELETED;
|
||||||
|
else if (DIFF_PAIR_RENAME(p))
|
||||||
|
p->status = DIFF_STATUS_RENAMED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int compare_pairs(const void *a_, const void *b_)
|
static int compare_pairs(const void *a_, const void *b_)
|
||||||
{
|
{
|
||||||
const struct diff_filepair *a = *((const struct diff_filepair **)a_);
|
const struct diff_filepair *a = *((const struct diff_filepair **)a_);
|
||||||
@ -2089,8 +2143,6 @@ static int compare_pairs(const void *a_, const void *b_)
|
|||||||
|
|
||||||
/* Call diffcore_rename() to compute which files have changed on given side */
|
/* Call diffcore_rename() to compute which files have changed on given side */
|
||||||
static void detect_regular_renames(struct merge_options *opt,
|
static void detect_regular_renames(struct merge_options *opt,
|
||||||
struct tree *merge_base,
|
|
||||||
struct tree *side,
|
|
||||||
unsigned side_index)
|
unsigned side_index)
|
||||||
{
|
{
|
||||||
struct diff_options diff_opts;
|
struct diff_options diff_opts;
|
||||||
@ -2108,11 +2160,11 @@ static void detect_regular_renames(struct merge_options *opt,
|
|||||||
diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
|
diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
|
||||||
diff_setup_done(&diff_opts);
|
diff_setup_done(&diff_opts);
|
||||||
|
|
||||||
|
diff_queued_diff = renames->pairs[side_index];
|
||||||
trace2_region_enter("diff", "diffcore_rename", opt->repo);
|
trace2_region_enter("diff", "diffcore_rename", opt->repo);
|
||||||
diff_tree_oid(&merge_base->object.oid, &side->object.oid, "",
|
diffcore_rename(&diff_opts);
|
||||||
&diff_opts);
|
|
||||||
diffcore_std(&diff_opts);
|
|
||||||
trace2_region_leave("diff", "diffcore_rename", opt->repo);
|
trace2_region_leave("diff", "diffcore_rename", opt->repo);
|
||||||
|
resolve_diffpair_statuses(&diff_queued_diff);
|
||||||
|
|
||||||
if (diff_opts.needed_rename_limit > renames->needed_limit)
|
if (diff_opts.needed_rename_limit > renames->needed_limit)
|
||||||
renames->needed_limit = diff_opts.needed_rename_limit;
|
renames->needed_limit = diff_opts.needed_rename_limit;
|
||||||
@ -2212,8 +2264,8 @@ static int detect_and_process_renames(struct merge_options *opt,
|
|||||||
memset(&combined, 0, sizeof(combined));
|
memset(&combined, 0, sizeof(combined));
|
||||||
|
|
||||||
trace2_region_enter("merge", "regular renames", opt->repo);
|
trace2_region_enter("merge", "regular renames", opt->repo);
|
||||||
detect_regular_renames(opt, merge_base, side1, MERGE_SIDE1);
|
detect_regular_renames(opt, MERGE_SIDE1);
|
||||||
detect_regular_renames(opt, merge_base, side2, MERGE_SIDE2);
|
detect_regular_renames(opt, MERGE_SIDE2);
|
||||||
trace2_region_leave("merge", "regular renames", opt->repo);
|
trace2_region_leave("merge", "regular renames", opt->repo);
|
||||||
|
|
||||||
trace2_region_enter("merge", "directory renames", opt->repo);
|
trace2_region_enter("merge", "directory renames", opt->repo);
|
||||||
|
@ -262,4 +262,28 @@ test_expect_success 'diff-tree -l0 defaults to a big rename limit, not zero' '
|
|||||||
grep "myotherfile.*myfile" actual
|
grep "myotherfile.*myfile" actual
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'basename similarity vs best similarity' '
|
||||||
|
mkdir subdir &&
|
||||||
|
test_write_lines line1 line2 line3 line4 line5 \
|
||||||
|
line6 line7 line8 line9 line10 >subdir/file.txt &&
|
||||||
|
git add subdir/file.txt &&
|
||||||
|
git commit -m "base txt" &&
|
||||||
|
|
||||||
|
git rm subdir/file.txt &&
|
||||||
|
test_write_lines line1 line2 line3 line4 line5 \
|
||||||
|
line6 line7 line8 >file.txt &&
|
||||||
|
test_write_lines line1 line2 line3 line4 line5 \
|
||||||
|
line6 line7 line8 line9 >file.md &&
|
||||||
|
git add file.txt file.md &&
|
||||||
|
git commit -a -m "rename" &&
|
||||||
|
git diff-tree -r -M --name-status HEAD^ HEAD >actual &&
|
||||||
|
# subdir/file.txt is 88% similar to file.md, 78% similar to file.txt,
|
||||||
|
# but since same basenames are checked first...
|
||||||
|
cat >expected <<-\EOF &&
|
||||||
|
A file.md
|
||||||
|
R078 subdir/file.txt file.txt
|
||||||
|
EOF
|
||||||
|
test_cmp expected actual
|
||||||
|
'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
Loading…
Reference in New Issue
Block a user