]> git.ipfire.org Git - thirdparty/git.git/blobdiff - diffcore-rename.c
Merge branch 'cm/rebase-i-updates'
[thirdparty/git.git] / diffcore-rename.c
index 8fe6c9384bcb5b63458e832f15bb7025a43647b7..e2ed64817654ec04b2e2504010209fbb53a577b7 100644 (file)
@@ -367,6 +367,535 @@ static int find_exact_renames(struct diff_options *options)
        return renames;
 }
 
+struct dir_rename_info {
+       struct strintmap idx_map;
+       struct strmap dir_rename_guess;
+       struct strmap *dir_rename_count;
+       struct strset *relevant_source_dirs;
+       unsigned setup;
+};
+
+static char *get_dirname(const char *filename)
+{
+       char *slash = strrchr(filename, '/');
+       return slash ? xstrndup(filename, slash - filename) : xstrdup("");
+}
+
+static void dirname_munge(char *filename)
+{
+       char *slash = strrchr(filename, '/');
+       if (!slash)
+               slash = filename;
+       *slash = '\0';
+}
+
+static const char *get_highest_rename_path(struct strintmap *counts)
+{
+       int highest_count = 0;
+       const char *highest_destination_dir = NULL;
+       struct hashmap_iter iter;
+       struct strmap_entry *entry;
+
+       strintmap_for_each_entry(counts, &iter, entry) {
+               const char *destination_dir = entry->key;
+               intptr_t count = (intptr_t)entry->value;
+               if (count > highest_count) {
+                       highest_count = count;
+                       highest_destination_dir = destination_dir;
+               }
+       }
+       return highest_destination_dir;
+}
+
+static void increment_count(struct dir_rename_info *info,
+                           char *old_dir,
+                           char *new_dir)
+{
+       struct strintmap *counts;
+       struct strmap_entry *e;
+
+       /* Get the {new_dirs -> counts} mapping using old_dir */
+       e = strmap_get_entry(info->dir_rename_count, old_dir);
+       if (e) {
+               counts = e->value;
+       } else {
+               counts = xmalloc(sizeof(*counts));
+               strintmap_init_with_options(counts, 0, NULL, 1);
+               strmap_put(info->dir_rename_count, old_dir, counts);
+       }
+
+       /* Increment the count for new_dir */
+       strintmap_incr(counts, new_dir, 1);
+}
+
+static void update_dir_rename_counts(struct dir_rename_info *info,
+                                    struct strset *dirs_removed,
+                                    const char *oldname,
+                                    const char *newname)
+{
+       char *old_dir = xstrdup(oldname);
+       char *new_dir = xstrdup(newname);
+       char new_dir_first_char = new_dir[0];
+       int first_time_in_loop = 1;
+
+       if (!info->setup)
+               /*
+                * info->setup is 0 here in two cases: (1) all auxiliary
+                * vars (like dirs_removed) were NULL so
+                * initialize_dir_rename_info() returned early, or (2)
+                * either break detection or copy detection are active so
+                * that we never called initialize_dir_rename_info().  In
+                * the former case, we don't have enough info to know if
+                * directories were renamed (because dirs_removed lets us
+                * know about a necessary prerequisite, namely if they were
+                * removed), and in the latter, we don't care about
+                * directory renames or find_basename_matches.
+                *
+                * This matters because both basename and inexact matching
+                * will also call update_dir_rename_counts().  In either of
+                * the above two cases info->dir_rename_counts will not
+                * have been properly initialized which prevents us from
+                * updating it, but in these two cases we don't care about
+                * dir_rename_counts anyway, so we can just exit early.
+                */
+               return;
+
+       while (1) {
+               /* Get old_dir, skip if its directory isn't relevant. */
+               dirname_munge(old_dir);
+               if (info->relevant_source_dirs &&
+                   !strset_contains(info->relevant_source_dirs, old_dir))
+                       break;
+
+               /* Get new_dir */
+               dirname_munge(new_dir);
+
+               /*
+                * When renaming
+                *   "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c"
+                * then this suggests that both
+                *   a/b/c/d/e/ => a/b/some/thing/else/e/
+                *   a/b/c/d/   => a/b/some/thing/else/
+                * so we want to increment counters for both.  We do NOT,
+                * however, also want to suggest that there was the following
+                * rename:
+                *   a/b/c/ => a/b/some/thing/
+                * so we need to quit at that point.
+                *
+                * Note the when first_time_in_loop, we only strip off the
+                * basename, and we don't care if that's different.
+                */
+               if (!first_time_in_loop) {
+                       char *old_sub_dir = strchr(old_dir, '\0')+1;
+                       char *new_sub_dir = strchr(new_dir, '\0')+1;
+                       if (!*new_dir) {
+                               /*
+                                * Special case when renaming to root directory,
+                                * i.e. when new_dir == "".  In this case, we had
+                                * something like
+                                *    a/b/subdir => subdir
+                                * and so dirname_munge() sets things up so that
+                                *    old_dir = "a/b\0subdir\0"
+                                *    new_dir = "\0ubdir\0"
+                                * We didn't have a '/' to overwrite a '\0' onto
+                                * in new_dir, so we have to compare differently.
+                                */
+                               if (new_dir_first_char != old_sub_dir[0] ||
+                                   strcmp(old_sub_dir+1, new_sub_dir))
+                                       break;
+                       } else {
+                               if (strcmp(old_sub_dir, new_sub_dir))
+                                       break;
+                       }
+               }
+
+               if (strset_contains(dirs_removed, old_dir))
+                       increment_count(info, old_dir, new_dir);
+               else
+                       break;
+
+               /* If we hit toplevel directory ("") for old or new dir, quit */
+               if (!*old_dir || !*new_dir)
+                       break;
+
+               first_time_in_loop = 0;
+       }
+
+       /* Free resources we don't need anymore */
+       free(old_dir);
+       free(new_dir);
+}
+
+static void initialize_dir_rename_info(struct dir_rename_info *info,
+                                      struct strset *dirs_removed,
+                                      struct strmap *dir_rename_count)
+{
+       struct hashmap_iter iter;
+       struct strmap_entry *entry;
+       int i;
+
+       if (!dirs_removed) {
+               info->setup = 0;
+               return;
+       }
+       info->setup = 1;
+
+       info->dir_rename_count = dir_rename_count;
+       if (!info->dir_rename_count) {
+               info->dir_rename_count = xmalloc(sizeof(*dir_rename_count));
+               strmap_init(info->dir_rename_count);
+       }
+       strintmap_init_with_options(&info->idx_map, -1, NULL, 0);
+       strmap_init_with_options(&info->dir_rename_guess, NULL, 0);
+
+       /* Setup info->relevant_source_dirs */
+       info->relevant_source_dirs = dirs_removed;
+
+       /*
+        * Loop setting up both info->idx_map, and doing setup of
+        * info->dir_rename_count.
+        */
+       for (i = 0; i < rename_dst_nr; ++i) {
+               /*
+                * For non-renamed files, make idx_map contain mapping of
+                *   filename -> index (index within rename_dst, that is)
+                */
+               if (!rename_dst[i].is_rename) {
+                       char *filename = rename_dst[i].p->two->path;
+                       strintmap_set(&info->idx_map, filename, i);
+                       continue;
+               }
+
+               /*
+                * For everything else (i.e. renamed files), make
+                * dir_rename_count contain a map of a map:
+                *   old_directory -> {new_directory -> count}
+                * In other words, for every pair look at the directories for
+                * the old filename and the new filename and count how many
+                * times that pairing occurs.
+                */
+               update_dir_rename_counts(info, dirs_removed,
+                                        rename_dst[i].p->one->path,
+                                        rename_dst[i].p->two->path);
+       }
+
+       /*
+        * Now we collapse
+        *    dir_rename_count: old_directory -> {new_directory -> count}
+        * down to
+        *    dir_rename_guess: old_directory -> best_new_directory
+        * where best_new_directory is the one with the highest count.
+        */
+       strmap_for_each_entry(info->dir_rename_count, &iter, entry) {
+               /* entry->key is source_dir */
+               struct strintmap *counts = entry->value;
+               char *best_newdir;
+
+               best_newdir = xstrdup(get_highest_rename_path(counts));
+               strmap_put(&info->dir_rename_guess, entry->key,
+                          best_newdir);
+       }
+}
+
+void partial_clear_dir_rename_count(struct strmap *dir_rename_count)
+{
+       struct hashmap_iter iter;
+       struct strmap_entry *entry;
+
+       strmap_for_each_entry(dir_rename_count, &iter, entry) {
+               struct strintmap *counts = entry->value;
+               strintmap_clear(counts);
+       }
+       strmap_partial_clear(dir_rename_count, 1);
+}
+
+static void cleanup_dir_rename_info(struct dir_rename_info *info,
+                                   struct strset *dirs_removed,
+                                   int keep_dir_rename_count)
+{
+       struct hashmap_iter iter;
+       struct strmap_entry *entry;
+       struct string_list to_remove = STRING_LIST_INIT_NODUP;
+       int i;
+
+       if (!info->setup)
+               return;
+
+       /* idx_map */
+       strintmap_clear(&info->idx_map);
+
+       /* dir_rename_guess */
+       strmap_clear(&info->dir_rename_guess, 1);
+
+       /* dir_rename_count */
+       if (!keep_dir_rename_count) {
+               partial_clear_dir_rename_count(info->dir_rename_count);
+               strmap_clear(info->dir_rename_count, 1);
+               FREE_AND_NULL(info->dir_rename_count);
+               return;
+       }
+
+       /*
+        * Although dir_rename_count was passed in
+        * diffcore_rename_extended() and we want to keep it around and
+        * return it to that caller, we first want to remove any data
+        * associated with directories that weren't renamed.
+        */
+       strmap_for_each_entry(info->dir_rename_count, &iter, entry) {
+               const char *source_dir = entry->key;
+               struct strintmap *counts = entry->value;
+
+               if (!strset_contains(dirs_removed, source_dir)) {
+                       string_list_append(&to_remove, source_dir);
+                       strintmap_clear(counts);
+                       continue;
+               }
+       }
+       for (i = 0; i < to_remove.nr; ++i)
+               strmap_remove(info->dir_rename_count,
+                             to_remove.items[i].string, 1);
+       string_list_clear(&to_remove, 0);
+}
+
+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 idx_possible_rename(char *filename, struct dir_rename_info *info)
+{
+       /*
+        * Our comparison of files with the same basename (see
+        * find_basename_matches() below), is only helpful when after exact
+        * rename detection we have exactly one file with a given basename
+        * among the rename sources and also only exactly one file with
+        * that basename among the rename destinations.  When we have
+        * multiple files with the same basename in either set, we do not
+        * know which to compare against.  However, there are some
+        * filenames that occur in large numbers (particularly
+        * build-related filenames such as 'Makefile', '.gitignore', or
+        * 'build.gradle' that potentially exist within every single
+        * subdirectory), and for performance we want to be able to quickly
+        * find renames for these files too.
+        *
+        * The reason basename comparisons are a useful heuristic was that it
+        * is common for people to move files across directories while keeping
+        * their filename the same.  If we had a way of determining or even
+        * making a good educated guess about which directory these non-unique
+        * basename files had moved the file to, we could check it.
+        * Luckily...
+        *
+        * When an entire directory is in fact renamed, we have two factors
+        * helping us out:
+        *   (a) the original directory disappeared giving us a hint
+        *       about when we can apply an extra heuristic.
+        *   (a) we often have several files within that directory and
+        *       subdirectories that are renamed without changes
+        * So, rules for a heuristic:
+        *   (0) If there basename matches are non-unique (the condition under
+        *       which this function is called) AND
+        *   (1) the directory in which the file was found has disappeared
+        *       (i.e. dirs_removed is non-NULL and has a relevant entry) THEN
+        *   (2) use exact renames of files within the directory to determine
+        *       where the directory is likely to have been renamed to.  IF
+        *       there is at least one exact rename from within that
+        *       directory, we can proceed.
+        *   (3) If there are multiple places the directory could have been
+        *       renamed to based on exact renames, ignore all but one of them.
+        *       Just use the destination with the most renames going to it.
+        *   (4) Check if applying that directory rename to the original file
+        *       would result in a destination filename that is in the
+        *       potential rename set.  If so, return the index of the
+        *       destination file (the index within rename_dst).
+        *   (5) Compare the original file and returned destination for
+        *       similarity, and if they are sufficiently similar, record the
+        *       rename.
+        *
+        * This function, idx_possible_rename(), is only responsible for (4).
+        * The conditions/steps in (1)-(3) are handled via setting up
+        * dir_rename_count and dir_rename_guess in
+        * initialize_dir_rename_info().  Steps (0) and (5) are handled by
+        * the caller of this function.
+        */
+       char *old_dir, *new_dir;
+       struct strbuf new_path = STRBUF_INIT;
+       int idx;
+
+       if (!info->setup)
+               return -1;
+
+       old_dir = get_dirname(filename);
+       new_dir = strmap_get(&info->dir_rename_guess, old_dir);
+       free(old_dir);
+       if (!new_dir)
+               return -1;
+
+       strbuf_addstr(&new_path, new_dir);
+       strbuf_addch(&new_path, '/');
+       strbuf_addstr(&new_path, get_basename(filename));
+
+       idx = strintmap_get(&info->idx_map, new_path.buf);
+       strbuf_release(&new_path);
+       return idx;
+}
+
+static int find_basename_matches(struct diff_options *options,
+                                int minimum_score,
+                                struct dir_rename_info *info,
+                                struct strset *dirs_removed)
+{
+       /*
+        * 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;
+
+       /*
+        * 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 */
+       for (i = 0; i < rename_src_nr; ++i) {
+               char *filename = rename_src[i].p->one->path;
+               const char *base = NULL;
+               intptr_t src_index;
+               intptr_t dst_index;
+
+               /*
+                * If the basename is unique among remaining sources, then
+                * src_index will equal 'i' and we can attempt to match it
+                * to a unique basename in the destinations.  Otherwise,
+                * use directory rename heuristics, if possible.
+                */
+               base = get_basename(filename);
+               src_index = strintmap_get(&sources, base);
+               assert(src_index == -1 || src_index == i);
+
+               if (strintmap_contains(&dests, base)) {
+                       struct diff_filespec *one, *two;
+                       int score;
+
+                       /* Find a matching destination, if possible */
+                       dst_index = strintmap_get(&dests, base);
+                       if (src_index == -1 || dst_index == -1) {
+                               src_index = i;
+                               dst_index = idx_possible_rename(filename, info);
+                       }
+                       if (dst_index == -1)
+                               continue;
+
+                       /* Ignore this dest if already used in a rename */
+                       if (rename_dst[dst_index].is_rename)
+                               continue; /* already used previously */
+
+                       /* 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++;
+                       update_dir_rename_counts(info, dirs_removed,
+                                                one->path, two->path);
+
+                       /*
+                        * 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
 static void record_if_better(struct diff_score m[], struct diff_score *o)
 {
@@ -433,7 +962,12 @@ static int too_many_rename_candidates(int num_destinations, int num_sources,
        return 1;
 }
 
-static int find_renames(struct diff_score *mx, int dst_cnt, int minimum_score, int copies)
+static int find_renames(struct diff_score *mx,
+                       int dst_cnt,
+                       int minimum_score,
+                       int copies,
+                       struct dir_rename_info *info,
+                       struct strset *dirs_removed)
 {
        int count = 0, i;
 
@@ -450,11 +984,64 @@ static int find_renames(struct diff_score *mx, int dst_cnt, int minimum_score, i
                        continue;
                record_rename_pair(mx[i].dst, mx[i].src, mx[i].score);
                count++;
+               update_dir_rename_counts(info, dirs_removed,
+                                        rename_src[mx[i].src].p->one->path,
+                                        rename_dst[mx[i].dst].p->two->path);
        }
        return count;
 }
 
-void diffcore_rename(struct diff_options *options)
+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_extended(struct diff_options *options,
+                             struct strset *dirs_removed,
+                             struct strmap *dir_rename_count)
 {
        int detect_rename = options->detect_rename;
        int minimum_score = options->rename_score;
@@ -463,9 +1050,16 @@ void diffcore_rename(struct diff_options *options)
        struct diff_score *mx;
        int i, j, rename_count, skip_unmodified = 0;
        int num_destinations, dst_cnt;
+       int num_sources, want_copies;
        struct progress *progress = NULL;
+       struct dir_rename_info info;
 
        trace2_region_enter("diff", "setup", options->repo);
+       info.setup = 0;
+       assert(!dir_rename_count || strmap_empty(dir_rename_count));
+       want_copies = (detect_rename == DIFF_DETECT_COPY);
+       if (dirs_removed && (break_idx || want_copies))
+               BUG("dirs_removed incompatible with break/copy detection");
        if (!minimum_score)
                minimum_score = DEFAULT_RENAME_SCORE;
 
@@ -502,7 +1096,7 @@ void diffcore_rename(struct diff_options *options)
                                p->one->rename_used++;
                        register_rename_src(p);
                }
-               else if (detect_rename == DIFF_DETECT_COPY) {
+               else if (want_copies) {
                        /*
                         * Increment the "rename_used" score by
                         * one, to indicate ourselves as a user.
@@ -527,17 +1121,67 @@ void diffcore_rename(struct diff_options *options)
        if (minimum_score == MAX_SCORE)
                goto cleanup;
 
-       /*
-        * Calculate how many renames are left (but all the source
-        * files still remain as options for rename/copies!)
-        */
+       num_sources = rename_src_nr;
+
+       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);
+
+               /* Preparation for basename-driven matching. */
+               trace2_region_enter("diff", "dir rename setup", options->repo);
+               initialize_dir_rename_info(&info,
+                                          dirs_removed, dir_rename_count);
+               trace2_region_leave("diff", "dir rename setup", 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,
+                                                     &info, dirs_removed);
+               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_sources = rename_src_nr; /* rename_src_nr reflects lower number */
 
        /* All done? */
-       if (!num_destinations)
+       if (!num_destinations || !num_sources)
                goto cleanup;
 
-       switch (too_many_rename_candidates(num_destinations, rename_src_nr,
+       switch (too_many_rename_candidates(num_destinations, num_sources,
                                           options)) {
        case 1:
                goto cleanup;
@@ -553,17 +1197,16 @@ void diffcore_rename(struct diff_options *options)
        if (options->show_rename_progress) {
                progress = start_delayed_progress(
                                _("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),
-                    sizeof(*mx));
+       CALLOC_ARRAY(mx, st_mult(NUM_CANDIDATE_PER_DST, num_destinations));
        for (dst_cnt = i = 0; i < rename_dst_nr; i++) {
                struct diff_filespec *two = rename_dst[i].p->two;
                struct diff_score *m;
 
                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];
                for (j = 0; j < NUM_CANDIDATE_PER_DST; j++)
@@ -573,6 +1216,8 @@ void diffcore_rename(struct diff_options *options)
                        struct diff_filespec *one = rename_src[j].p->one;
                        struct diff_score this_src;
 
+                       assert(!one->rename_used || want_copies || break_idx);
+
                        if (skip_unmodified &&
                            diff_unmodified_pair(rename_src[j].p))
                                continue;
@@ -594,16 +1239,18 @@ void diffcore_rename(struct diff_options *options)
                }
                dst_cnt++;
                display_progress(progress,
-                                (uint64_t)dst_cnt * (uint64_t)rename_src_nr);
+                                (uint64_t)dst_cnt * (uint64_t)num_sources);
        }
        stop_progress(&progress);
 
        /* cost matrix sorted by most to least similar pair */
        STABLE_QSORT(mx, dst_cnt * NUM_CANDIDATE_PER_DST, score_compare);
 
-       rename_count += find_renames(mx, dst_cnt, minimum_score, 0);
-       if (detect_rename == DIFF_DETECT_COPY)
-               rename_count += find_renames(mx, dst_cnt, minimum_score, 1);
+       rename_count += find_renames(mx, dst_cnt, minimum_score, 0,
+                                    &info, dirs_removed);
+       if (want_copies)
+               rename_count += find_renames(mx, dst_cnt, minimum_score, 1,
+                                            &info, dirs_removed);
        free(mx);
        trace2_region_leave("diff", "inexact renames", options->repo);
 
@@ -679,6 +1326,7 @@ void diffcore_rename(struct diff_options *options)
                if (rename_dst[i].filespec_to_free)
                        free_filespec(rename_dst[i].filespec_to_free);
 
+       cleanup_dir_rename_info(&info, dirs_removed, dir_rename_count != NULL);
        FREE_AND_NULL(rename_dst);
        rename_dst_nr = rename_dst_alloc = 0;
        FREE_AND_NULL(rename_src);
@@ -690,3 +1338,8 @@ void diffcore_rename(struct diff_options *options)
        trace2_region_leave("diff", "write back to queue", options->repo);
        return;
 }
+
+void diffcore_rename(struct diff_options *options)
+{
+       diffcore_rename_extended(options, NULL, NULL);
+}