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 exactmaint
						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; | ||||||
| 	 * Calculate how many renames are left (but all the source |  | ||||||
| 	 * files still remain as options for rename/copies!) | 	if (want_copies || break_idx) { | ||||||
| 	 */ | 		/* | ||||||
|  | 		 * Cull sources: | ||||||
|  | 		 *   - 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
	
	 Junio C Hamano
						Junio C Hamano