merge-recursive: add get_directory_renames()

This populates a set of directory renames for us. The set of directory renames is not yet used, but will be in subsequent commits. Note that the use of a string_list for possible_new_dirs in the new dir_rename_entry struct implies an O(n^2) algorithm; however, in practice I expect the number of distinct directories that files were renamed into from a single original directory to be O(1). My guess is that n has a mode of 1 and a mean of less than 2, so, for now, string_list seems good enough for possible_new_dirs. Reviewed-by: Stefan Beller <sbeller@google.com> Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Feb 14, 2018 at 10:51 UTC 8383408dc79300211778b615b353b3463ccc369a
2 files changed +239 -3
merge-recursive.c
+221 -3
@@ -49,6 +49,44 @@ static unsigned int path_hash(const char *path)
49 return ignore_case ? strihash(path) : strhash(path);
50 }
51
52 +static struct dir_rename_entry *dir_rename_find_entry(struct hashmap *hashmap,
53 + char *dir)
54 +{
55 + struct dir_rename_entry key;
56 +
57 + if (dir == NULL)
58 + return NULL;
59 + hashmap_entry_init(&key, strhash(dir));
60 + key.dir = dir;
61 + return hashmap_get(hashmap, &key, NULL);
62 +}
63 +
64 +static int dir_rename_cmp(const void *unused_cmp_data,
65 + const void *entry,
66 + const void *entry_or_key,
67 + const void *unused_keydata)
68 +{
69 + const struct dir_rename_entry *e1 = entry;
70 + const struct dir_rename_entry *e2 = entry_or_key;
71 +
72 + return strcmp(e1->dir, e2->dir);
73 +}
74 +
75 +static void dir_rename_init(struct hashmap *map)
76 +{
77 + hashmap_init(map, dir_rename_cmp, NULL, 0);
78 +}
79 +
80 +static void dir_rename_entry_init(struct dir_rename_entry *entry,
81 + char *directory)
82 +{
83 + hashmap_entry_init(entry, strhash(directory));
84 + entry->dir = directory;
85 + entry->non_unique_new_dir = 0;
86 + strbuf_init(&entry->new_dir, 0);
87 + string_list_init(&entry->possible_new_dirs, 0);
88 +}
89 +
90 static void flush_output(struct merge_options *o)
91 {
92 if (o->buffer_output < 2 && o->obuf.len) {
@@ -1347,6 +1385,169 @@ static struct diff_queue_struct *get_diffpairs(struct merge_options *o,
1385 return ret;
1386 }
1387
1388 +static void get_renamed_dir_portion(const char *old_path, const char *new_path,
1389 + char **old_dir, char **new_dir)
1390 +{
1391 + char *end_of_old, *end_of_new;
1392 + int old_len, new_len;
1393 +
1394 + *old_dir = NULL;
1395 + *new_dir = NULL;
1396 +
1397 + /*
1398 + * For
1399 + * "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c"
1400 + * the "e/foo.c" part is the same, we just want to know that
1401 + * "a/b/c/d" was renamed to "a/b/some/thing/else"
1402 + * so, for this example, this function returns "a/b/c/d" in
1403 + * *old_dir and "a/b/some/thing/else" in *new_dir.
1404 + *
1405 + * Also, if the basename of the file changed, we don't care. We
1406 + * want to know which portion of the directory, if any, changed.
1407 + */
1408 + end_of_old = strrchr(old_path, '/');
1409 + end_of_new = strrchr(new_path, '/');
1410 +
1411 + if (end_of_old == NULL || end_of_new == NULL)
1412 + return;
1413 + while (*--end_of_new == *--end_of_old &&
1414 + end_of_old != old_path &&
1415 + end_of_new != new_path)
1416 + ; /* Do nothing; all in the while loop */
1417 + /*
1418 + * We've found the first non-matching character in the directory
1419 + * paths. That means the current directory we were comparing
1420 + * represents the rename. Move end_of_old and end_of_new back
1421 + * to the full directory name.
1422 + */
1423 + if (*end_of_old == '/')
1424 + end_of_old++;
1425 + if (*end_of_old != '/')
1426 + end_of_new++;
1427 + end_of_old = strchr(end_of_old, '/');
1428 + end_of_new = strchr(end_of_new, '/');
1429 +
1430 + /*
1431 + * It may have been the case that old_path and new_path were the same
1432 + * directory all along. Don't claim a rename if they're the same.
1433 + */
1434 + old_len = end_of_old - old_path;
1435 + new_len = end_of_new - new_path;
1436 +
1437 + if (old_len != new_len || strncmp(old_path, new_path, old_len)) {
1438 + *old_dir = xstrndup(old_path, old_len);
1439 + *new_dir = xstrndup(new_path, new_len);
1440 + }
1441 +}
1442 +
1443 +static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,
1444 + struct tree *tree)
1445 +{
1446 + struct hashmap *dir_renames;
1447 + struct hashmap_iter iter;
1448 + struct dir_rename_entry *entry;
1449 + int i;
1450 +
1451 + /*
1452 + * Typically, we think of a directory rename as all files from a
1453 + * certain directory being moved to a target directory. However,
1454 + * what if someone first moved two files from the original
1455 + * directory in one commit, and then renamed the directory
1456 + * somewhere else in a later commit? At merge time, we just know
1457 + * that files from the original directory went to two different
1458 + * places, and that the bulk of them ended up in the same place.
1459 + * We want each directory rename to represent where the bulk of the
1460 + * files from that directory end up; this function exists to find
1461 + * where the bulk of the files went.
1462 + *
1463 + * The first loop below simply iterates through the list of file
1464 + * renames, finding out how often each directory rename pair
1465 + * possibility occurs.
1466 + */
1467 + dir_renames = xmalloc(sizeof(struct hashmap));
1468 + dir_rename_init(dir_renames);
1469 + for (i = 0; i < pairs->nr; ++i) {
1470 + struct string_list_item *item;
1471 + int *count;
1472 + struct diff_filepair *pair = pairs->queue[i];
1473 + char *old_dir, *new_dir;
1474 +
1475 + /* File not part of directory rename if it wasn't renamed */
1476 + if (pair->status != 'R')
1477 + continue;
1478 +
1479 + get_renamed_dir_portion(pair->one->path, pair->two->path,
1480 + &old_dir, &new_dir);
1481 + if (!old_dir)
1482 + /* Directory didn't change at all; ignore this one. */
1483 + continue;
1484 +
1485 + entry = dir_rename_find_entry(dir_renames, old_dir);
1486 + if (!entry) {
1487 + entry = xmalloc(sizeof(struct dir_rename_entry));
1488 + dir_rename_entry_init(entry, old_dir);
1489 + hashmap_put(dir_renames, entry);
1490 + } else {
1491 + free(old_dir);
1492 + }
1493 + item = string_list_lookup(&entry->possible_new_dirs, new_dir);
1494 + if (!item) {
1495 + item = string_list_insert(&entry->possible_new_dirs,
1496 + new_dir);
1497 + item->util = xcalloc(1, sizeof(int));
1498 + } else {
1499 + free(new_dir);
1500 + }
1501 + count = item->util;
1502 + *count += 1;
1503 + }
1504 +
1505 + /*
1506 + * For each directory with files moved out of it, we find out which
1507 + * target directory received the most files so we can declare it to
1508 + * be the "winning" target location for the directory rename. This
1509 + * winner gets recorded in new_dir. If there is no winner
1510 + * (multiple target directories received the same number of files),
1511 + * we set non_unique_new_dir. Once we've determined the winner (or
1512 + * that there is no winner), we no longer need possible_new_dirs.
1513 + */
1514 + hashmap_iter_init(dir_renames, &iter);
1515 + while ((entry = hashmap_iter_next(&iter))) {
1516 + int max = 0;
1517 + int bad_max = 0;
1518 + char *best = NULL;
1519 +
1520 + for (i = 0; i < entry->possible_new_dirs.nr; i++) {
1521 + int *count = entry->possible_new_dirs.items[i].util;
1522 +
1523 + if (*count == max)
1524 + bad_max = max;
1525 + else if (*count > max) {
1526 + max = *count;
1527 + best = entry->possible_new_dirs.items[i].string;
1528 + }
1529 + }
1530 + if (bad_max == max)
1531 + entry->non_unique_new_dir = 1;
1532 + else {
1533 + assert(entry->new_dir.len == 0);
1534 + strbuf_addstr(&entry->new_dir, best);
1535 + }
1536 + /*
1537 + * The relevant directory sub-portion of the original full
1538 + * filepaths were xstrndup'ed before inserting into
1539 + * possible_new_dirs, and instead of manually iterating the
1540 + * list and free'ing each, just lie and tell
1541 + * possible_new_dirs that it did the strdup'ing so that it
1542 + * will free them for us.
1543 + */
1544 + entry->possible_new_dirs.strdup_strings = 1;
1545 + string_list_clear(&entry->possible_new_dirs, 1);
1546 + }
1547 +
1548 + return dir_renames;
1549 +}
1550 +
1551 /*
1552 * Get information of all renames which occurred in 'pairs', making use of
1553 * any implicit directory renames inferred from the other side of history.
@@ -1658,8 +1859,21 @@ struct rename_info {
1859 struct string_list *merge_renames;
1860 };
1861
1661 -static void initial_cleanup_rename(struct diff_queue_struct *pairs)
1862 +static void initial_cleanup_rename(struct diff_queue_struct *pairs,
1863 + struct hashmap *dir_renames)
1864 {
1865 + struct hashmap_iter iter;
1866 + struct dir_rename_entry *e;
1867 +
1868 + hashmap_iter_init(dir_renames, &iter);
1869 + while ((e = hashmap_iter_next(&iter))) {
1870 + free(e->dir);
1871 + strbuf_release(&e->new_dir);
1872 + /* possible_new_dirs already cleared in get_directory_renames */
1873 + }
1874 + hashmap_free(dir_renames, 1);
1875 + free(dir_renames);
1876 +
1877 free(pairs->queue);
1878 free(pairs);
1879 }
@@ -1672,6 +1886,7 @@ static int handle_renames(struct merge_options *o,
1886 struct rename_info *ri)
1887 {
1888 struct diff_queue_struct *head_pairs, *merge_pairs;
1889 + struct hashmap *dir_re_head, *dir_re_merge;
1890 int clean;
1891
1892 ri->head_renames = NULL;
@@ -1683,6 +1898,9 @@ static int handle_renames(struct merge_options *o,
1898 head_pairs = get_diffpairs(o, common, head);
1899 merge_pairs = get_diffpairs(o, common, merge);
1900
1901 + dir_re_head = get_directory_renames(head_pairs, head);
1902 + dir_re_merge = get_directory_renames(merge_pairs, merge);
1903 +
1904 ri->head_renames = get_renames(o, head_pairs, head,
1905 common, head, merge, entries);
1906 ri->merge_renames = get_renames(o, merge_pairs, merge,
@@ -1694,8 +1912,8 @@ static int handle_renames(struct merge_options *o,
1912 * data structures are still needed and referenced in
1913 * process_entry(). But there are a few things we can free now.
1914 */
1697 - initial_cleanup_rename(head_pairs);
1698 - initial_cleanup_rename(merge_pairs);
1915 + initial_cleanup_rename(head_pairs, dir_re_head);
1916 + initial_cleanup_rename(merge_pairs, dir_re_merge);
1917
1918 return clean;
1919 }
merge-recursive.h
+18
@@ -29,6 +29,24 @@ struct merge_options {
29 struct string_list df_conflict_file_set;
30 };
31
32 +/*
33 + * For dir_rename_entry, directory names are stored as a full path from the
34 + * toplevel of the repository and do not include a trailing '/'. Also:
35 + *
36 + * dir: original name of directory being renamed
37 + * non_unique_new_dir: if true, could not determine new_dir
38 + * new_dir: final name of directory being renamed
39 + * possible_new_dirs: temporary used to help determine new_dir; see comments
40 + * in get_directory_renames() for details
41 + */
42 +struct dir_rename_entry {
43 + struct hashmap_entry ent; /* must be the first member! */
44 + char *dir;
45 + unsigned non_unique_new_dir:1;
46 + struct strbuf new_dir;
47 + struct string_list possible_new_dirs;
48 +};
49 +
50 /* merge_trees() but with recursive ancestor consolidation */
51 int merge_recursive(struct merge_options *o,
52 struct commit *h1,