blame: use the fingerprint heuristic to match ignored lines

This commit integrates the fuzzy fingerprint heuristic into guess_line_blames(). We actually make two passes. The first pass uses the fuzzy algorithm to find a match within the current diff chunk. If that fails, the second pass searches the entire parent file for the best match. For an example of scanning the entire parent for a match, consider: commit-a 30) #include <sys/header_a.h> commit-b 31) #include <header_b.h> commit-c 32) #include <header_c.h> Then commit X alphabetizes them: commit-X 30) #include <header_b.h> commit-X 31) #include <header_c.h> commit-X 32) #include <sys/header_a.h> If we just check the parent's chunk (i.e. the first pass), we'd get: commit-b 30) #include <header_b.h> commit-c 31) #include <header_c.h> commit-X 32) #include <sys/header_a.h> That's because commit X actually consists of two chunks: one chunk is removing sys/header_a.h, then some context, and the second chunk is adding sys/header_a.h. If we scan the entire parent file, we get: commit-b 30) #include <header_b.h> commit-c 31) #include <header_c.h> commit-a 32) #include <sys/header_a.h> Signed-off-by: Barret Rhoden <brho@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Barret Rhoden committed Jun 20, 2019 at 12:38 UTC a07a97760cbf2e794600157a5a9f61a888a17941
2 files changed +55 -8
blame.c
+55 -5
@@ -989,12 +989,19 @@ static void fill_origin_fingerprints(struct blame_origin *o, mmfile_t *file)
989 return;
990 o->num_lines = find_line_starts(&line_starts, o->file.ptr,
991 o->file.size);
992 - /* TODO: Will fill in fingerprints in a future commit */
992 + o->fingerprints = xcalloc(sizeof(struct fingerprint), o->num_lines);
993 + get_line_fingerprints(o->fingerprints, o->file.ptr, line_starts,
994 + 0, o->num_lines);
995 free(line_starts);
996 }
997
998 static void drop_origin_fingerprints(struct blame_origin *o)
999 {
1000 + if (o->fingerprints) {
1001 + free_line_fingerprints(o->fingerprints, o->num_lines);
1002 + o->num_lines = 0;
1003 + FREE_AND_NULL(o->fingerprints);
1004 + }
1005 }
1006
1007 /*
@@ -1572,9 +1579,34 @@ static int are_lines_adjacent(struct blame_line_tracker *first,
1579 first->s_lno + 1 == second->s_lno;
1580 }
1581
1582 +static int scan_parent_range(struct fingerprint *p_fps,
1583 + struct fingerprint *t_fps, int t_idx,
1584 + int from, int nr_lines)
1585 +{
1586 + int sim, p_idx;
1587 + #define FINGERPRINT_FILE_THRESHOLD 10
1588 + int best_sim_val = FINGERPRINT_FILE_THRESHOLD;
1589 + int best_sim_idx = -1;
1590 +
1591 + for (p_idx = from; p_idx < from + nr_lines; p_idx++) {
1592 + sim = fingerprint_similarity(&t_fps[t_idx], &p_fps[p_idx]);
1593 + if (sim < best_sim_val)
1594 + continue;
1595 + /* Break ties with the closest-to-target line number */
1596 + if (sim == best_sim_val && best_sim_idx != -1 &&
1597 + abs(best_sim_idx - t_idx) < abs(p_idx - t_idx))
1598 + continue;
1599 + best_sim_val = sim;
1600 + best_sim_idx = p_idx;
1601 + }
1602 + return best_sim_idx;
1603 +}
1604 +
1605 /*
1576 - * This cheap heuristic assigns lines in the chunk to their relative location in
1577 - * the parent's chunk. Any additional lines are left with the target.
1606 + * The first pass checks the blame entry (from the target) against the parent's
1607 + * diff chunk. If that fails for a line, the second pass tries to match that
1608 + * line to any part of parent file. That catches cases where a change was
1609 + * broken into two chunks by 'context.'
1610 */
1611 static void guess_line_blames(struct blame_origin *parent,
1612 struct blame_origin *target,
@@ -1583,11 +1615,22 @@ static void guess_line_blames(struct blame_origin *parent,
1615 {
1616 int i, best_idx, target_idx;
1617 int parent_slno = tlno + offset;
1618 + int *fuzzy_matches;
1619
1620 + fuzzy_matches = fuzzy_find_matching_lines(parent, target,
1621 + tlno, parent_slno, same,
1622 + parent_len);
1623 for (i = 0; i < same - tlno; i++) {
1624 target_idx = tlno + i;
1589 - best_idx = target_idx + offset;
1590 - if (best_idx < parent_slno + parent_len) {
1625 + if (fuzzy_matches && fuzzy_matches[i] >= 0) {
1626 + best_idx = fuzzy_matches[i];
1627 + } else {
1628 + best_idx = scan_parent_range(parent->fingerprints,
1629 + target->fingerprints,
1630 + target_idx, 0,
1631 + parent->num_lines);
1632 + }
1633 + if (best_idx >= 0) {
1634 line_blames[i].is_parent = 1;
1635 line_blames[i].s_lno = best_idx;
1636 } else {
@@ -1595,6 +1638,7 @@ static void guess_line_blames(struct blame_origin *parent,
1638 line_blames[i].s_lno = target_idx;
1639 }
1640 }
1641 + free(fuzzy_matches);
1642 }
1643
1644 /*
@@ -2371,6 +2415,12 @@ static void pass_blame(struct blame_scoreboard *sb, struct blame_origin *origin,
2415 if (!porigin)
2416 continue;
2417 pass_blame_to_parent(sb, origin, porigin, 1);
2418 + /*
2419 + * Preemptively drop porigin so we can refresh the
2420 + * fingerprints if we use the parent again, which can
2421 + * occur if you ignore back-to-back commits.
2422 + */
2423 + drop_origin_blob(porigin);
2424 if (!origin->suspects)
2425 goto finish;
2426 }
t/t8014-blame-ignore-fuzzy.sh
-3
@@ -3,9 +3,6 @@
3 test_description='git blame ignore fuzzy heuristic'
4 . ./test-lib.sh
5
6 -# short circuit until blame has the fuzzy capabilities
7 -test_done
8 -
6 pick_author='s/^[0-9a-f^]* *(\([^ ]*\) .*/\1/'
7
8 # Each test is composed of 4 variables: