blame: add the ability to ignore commits and their changes

Commits that make formatting changes or function renames are often not interesting when blaming a file. A user may deem such a commit as 'not interesting' and want to ignore and its changes it when assigning blame. For example, say a file has the following git history / rev-list: ---O---A---X---B---C---D---Y---E---F Commits X and Y both touch a particular line, and the other commits do not: X: "Take a third parameter" -MyFunc(1, 2); +MyFunc(1, 2, 3); Y: "Remove camelcase" -MyFunc(1, 2, 3); +my_func(1, 2, 3); git-blame will blame Y for the change. I'd like to be able to ignore Y: both the existence of the commit as well as any changes it made. This differs from -S rev-list, which specifies the list of commits to process for the blame. We would still process Y, but just don't let the blame 'stick.' This patch adds the ability for users to ignore a revision with --ignore-rev=rev, which may be repeated. They can specify a set of files of full object names of revs, e.g. SHA-1 hashes, one per line. A single file may be specified with the blame.ignoreRevFile config option or with --ignore-rev-file=file. Both the config option and the command line option may be repeated multiple times. An empty file name "" will clear the list of revs from previously processed files. Config options are processed before command line options. For a typical use case, projects will maintain the file containing revisions for commits that perform mass reformatting, and their users have the option to ignore all of the commits in that file. Additionally, a user can use the --ignore-rev option for one-off investigation. To go back to the example above, X was a substantive change to the function, but not the change the user is interested in. The user inspected X, but wanted to find the previous change to that line - perhaps a commit that introduced that function call. To make this work, we can't simply remove all ignored commits from the rev-list. We need to diff the changes introduced by Y so that we can ignore them. We let the blames get passed to Y, just like when processing normally. When Y is the target, we make sure that Y does not *keep* any blames. Any changes that Y is responsible for get passed to its parent. Note we make one pass through all of the scapegoats (parents) to attempt to pass blame normally; we don't know if we *need* to ignore the commit until we've checked all of the parents. The blame_entry will get passed up the tree until we find a commit that has a diff chunk that affects those lines. One issue is that the ignored commit *did* make some change, and there is no general solution to finding the line in the parent commit that corresponds to a given line in the ignored commit. That makes it hard to attribute a particular line within an ignored commit's diff correctly. For example, the parent of an ignored commit has this, say at line 11: commit-a 11) #include "a.h" commit-b 12) #include "b.h" Commit X, which we will ignore, swaps these lines: commit-X 11) #include "b.h" commit-X 12) #include "a.h" We can pass that blame entry to the parent, but line 11 will be attributed to commit A, even though "include b.h" came from commit B. The blame mechanism will be looking at the parent's view of the file at line number 11. ignore_blame_entry() is set up to allow alternative algorithms for guessing per-line blames. Any line that is not attributed to the parent will continue to be blamed on the ignored commit as if that commit was not ignored. Upcoming patches have the ability to detect these lines and mark them in the blame output. The existing algorithm is simple: blame each line on the corresponding line in the parent's diff chunk. Any lines beyond that stay with the target. For example, the parent of an ignored commit has this, say at line 11: commit-a 11) void new_func_1(void *x, void *y); commit-b 12) void new_func_2(void *x, void *y); commit-c 13) some_line_c commit-d 14) some_line_d After a commit 'X', we have: commit-X 11) void new_func_1(void *x, commit-X 12) void *y); commit-X 13) void new_func_2(void *x, commit-X 14) void *y); commit-c 15) some_line_c commit-d 16) some_line_d Commit X nets two additionally lines: 13 and 14. The current guess_line_blames() algorithm will not attribute these to the parent, whose diff chunk is only two lines - not four. When we ignore with the current algorithm, we get: commit-a 11) void new_func_1(void *x, commit-b 12) void *y); commit-X 13) void new_func_2(void *x, commit-X 14) void *y); commit-c 15) some_line_c commit-d 16) some_line_d Note that line 12 was blamed on B, though B was the commit for new_func_2(), not new_func_1(). Even when guess_line_blames() finds a line in the parent, it may still be incorrect. Signed-off-by: Barret Rhoden <brho@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Barret Rhoden committed May 15, 2019 at 17:44 UTC ae3f36dea16e51041c56ba9ed6b38380c8421816
7 files changed +432 -9
Documentation/blame-options.txt
+14
@@ -110,5 +110,19 @@ commit. And the default value is 40. If there are more than one
110 `-C` options given, the <num> argument of the last `-C` will
111 take effect.
112
113 +--ignore-rev <rev>::
114 + Ignore changes made by the revision when assigning blame, as if the
115 + change never happened. Lines that were changed or added by an ignored
116 + commit will be blamed on the previous commit that changed that line or
117 + nearby lines. This option may be specified multiple times to ignore
118 + more than one revision.
119 +
120 +--ignore-revs-file <file>::
121 + Ignore revisions listed in `file`, which must be in the same format as an
122 + `fsck.skipList`. This option may be repeated, and these files will be
123 + processed after any files specified with the `blame.ignoreRevsFile` config
124 + option. An empty file name, `""`, will clear the list of revs from
125 + previously processed files.
126 +
127 -h::
128 Show help message.
Documentation/config/blame.txt
+7
@@ -19,3 +19,10 @@ blame.showEmail::
19 blame.showRoot::
20 Do not treat root commits as boundaries in linkgit:git-blame[1].
21 This option defaults to false.
22 +
23 +blame.ignoreRevsFile::
24 + Ignore revisions listed in the file, one unabbreviated object name per
25 + line, in linkgit:git-blame[1]. Whitespace and comments beginning with
26 + `#` are ignored. This option may be repeated multiple times. Empty
27 + file names will reset the list of ignored revisions. This option will
28 + be handled before the command line option `--ignore-revs-file`.
Documentation/git-blame.txt
+1
@@ -10,6 +10,7 @@ SYNOPSIS
10 [verse]
11 'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental]
12 [-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
13 + [--ignore-rev <rev>] [--ignore-revs-file <file>]
14 [--progress] [--abbrev=<n>] [<rev> | --contents <file> | --reverse <rev>..<rev>]
15 [--] <file>
16
blame.c
+167 -9
@@ -859,6 +859,103 @@ static struct blame_entry *split_blame_at(struct blame_entry *e, int len,
859 return n;
860 }
861
862 +struct blame_line_tracker {
863 + int is_parent;
864 + int s_lno;
865 +};
866 +
867 +static int are_lines_adjacent(struct blame_line_tracker *first,
868 + struct blame_line_tracker *second)
869 +{
870 + return first->is_parent == second->is_parent &&
871 + first->s_lno + 1 == second->s_lno;
872 +}
873 +
874 +/*
875 + * This cheap heuristic assigns lines in the chunk to their relative location in
876 + * the parent's chunk. Any additional lines are left with the target.
877 + */
878 +static void guess_line_blames(struct blame_origin *parent,
879 + struct blame_origin *target,
880 + int tlno, int offset, int same, int parent_len,
881 + struct blame_line_tracker *line_blames)
882 +{
883 + int i, best_idx, target_idx;
884 + int parent_slno = tlno + offset;
885 +
886 + for (i = 0; i < same - tlno; i++) {
887 + target_idx = tlno + i;
888 + best_idx = target_idx + offset;
889 + if (best_idx < parent_slno + parent_len) {
890 + line_blames[i].is_parent = 1;
891 + line_blames[i].s_lno = best_idx;
892 + } else {
893 + line_blames[i].is_parent = 0;
894 + line_blames[i].s_lno = target_idx;
895 + }
896 + }
897 +}
898 +
899 +/*
900 + * This decides which parts of a blame entry go to the parent (added to the
901 + * ignoredp list) and which stay with the target (added to the diffp list). The
902 + * actual decision was made in a separate heuristic function, and those answers
903 + * for the lines in 'e' are in line_blames. This consumes e, essentially
904 + * putting it on a list.
905 + *
906 + * Note that the blame entries on the ignoredp list are not necessarily sorted
907 + * with respect to the parent's line numbers yet.
908 + */
909 +static void ignore_blame_entry(struct blame_entry *e,
910 + struct blame_origin *parent,
911 + struct blame_origin *target,
912 + struct blame_entry **diffp,
913 + struct blame_entry **ignoredp,
914 + struct blame_line_tracker *line_blames)
915 +{
916 + int entry_len, nr_lines, i;
917 +
918 + /*
919 + * We carve new entries off the front of e. Each entry comes from a
920 + * contiguous chunk of lines: adjacent lines from the same origin
921 + * (either the parent or the target).
922 + */
923 + entry_len = 1;
924 + nr_lines = e->num_lines; /* e changes in the loop */
925 + for (i = 0; i < nr_lines; i++) {
926 + struct blame_entry *next = NULL;
927 +
928 + /*
929 + * We are often adjacent to the next line - only split the blame
930 + * entry when we have to.
931 + */
932 + if (i + 1 < nr_lines) {
933 + if (are_lines_adjacent(&line_blames[i],
934 + &line_blames[i + 1])) {
935 + entry_len++;
936 + continue;
937 + }
938 + next = split_blame_at(e, entry_len,
939 + blame_origin_incref(e->suspect));
940 + }
941 + if (line_blames[i].is_parent) {
942 + blame_origin_decref(e->suspect);
943 + e->suspect = blame_origin_incref(parent);
944 + e->s_lno = line_blames[i - entry_len + 1].s_lno;
945 + e->next = *ignoredp;
946 + *ignoredp = e;
947 + } else {
948 + /* e->s_lno is already in the target's address space. */
949 + e->next = *diffp;
950 + *diffp = e;
951 + }
952 + assert(e->num_lines == entry_len);
953 + e = next;
954 + entry_len = 1;
955 + }
956 + assert(!e);
957 +}
958 +
959 /*
960 * Process one hunk from the patch between the current suspect for
961 * blame_entry e and its parent. This first blames any unfinished
@@ -868,13 +965,20 @@ static struct blame_entry *split_blame_at(struct blame_entry *e, int len,
965 * -C options may lead to overlapping/duplicate source line number
966 * ranges, all we can rely on from sorting/merging is the order of the
967 * first suspect line number.
968 + *
969 + * tlno: line number in the target where this chunk begins
970 + * same: line number in the target where this chunk ends
971 + * offset: add to tlno to get the chunk starting point in the parent
972 + * parent_len: number of lines in the parent chunk
973 */
974 static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
873 - int tlno, int offset, int same,
874 - struct blame_origin *parent)
975 + int tlno, int offset, int same, int parent_len,
976 + struct blame_origin *parent,
977 + struct blame_origin *target, int ignore_diffs)
978 {
979 struct blame_entry *e = **srcq;
877 - struct blame_entry *samep = NULL, *diffp = NULL;
980 + struct blame_entry *samep = NULL, *diffp = NULL, *ignoredp = NULL;
981 + struct blame_line_tracker *line_blames = NULL;
982
983 while (e && e->s_lno < tlno) {
984 struct blame_entry *next = e->next;
@@ -923,6 +1027,14 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
1027 */
1028 samep = NULL;
1029 diffp = NULL;
1030 +
1031 + if (ignore_diffs && same - tlno > 0) {
1032 + line_blames = xcalloc(sizeof(struct blame_line_tracker),
1033 + same - tlno);
1034 + guess_line_blames(parent, target, tlno, offset, same,
1035 + parent_len, line_blames);
1036 + }
1037 +
1038 while (e && e->s_lno < same) {
1039 struct blame_entry *next = e->next;
1040
@@ -942,10 +1054,29 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
1054 n->next = samep;
1055 samep = n;
1056 }
945 - e->next = diffp;
946 - diffp = e;
1057 + if (ignore_diffs) {
1058 + ignore_blame_entry(e, parent, target, &diffp, &ignoredp,
1059 + line_blames + e->s_lno - tlno);
1060 + } else {
1061 + e->next = diffp;
1062 + diffp = e;
1063 + }
1064 e = next;
1065 }
1066 + free(line_blames);
1067 + if (ignoredp) {
1068 + /*
1069 + * Note ignoredp is not sorted yet, and thus neither is dstq.
1070 + * That list must be sorted before we queue_blames(). We defer
1071 + * sorting until after all diff hunks are processed, so that
1072 + * guess_line_blames() can pick *any* line in the parent. The
1073 + * slight drawback is that we end up sorting all blame entries
1074 + * passed to the parent, including those that are unrelated to
1075 + * changes made by the ignored commit.
1076 + */
1077 + **dstq = reverse_blame(ignoredp, **dstq);
1078 + *dstq = &ignoredp->next;
1079 + }
1080 **srcq = reverse_blame(diffp, reverse_blame(samep, e));
1081 /* Move across elements that are in the unblamable portion */
1082 if (diffp)
@@ -954,7 +1085,9 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
1085
1086 struct blame_chunk_cb_data {
1087 struct blame_origin *parent;
1088 + struct blame_origin *target;
1089 long offset;
1090 + int ignore_diffs;
1091 struct blame_entry **dstq;
1092 struct blame_entry **srcq;
1093 };
@@ -967,7 +1100,8 @@ static int blame_chunk_cb(long start_a, long count_a,
1100 if (start_a - start_b != d->offset)
1101 die("internal error in blame::blame_chunk_cb");
1102 blame_chunk(&d->dstq, &d->srcq, start_b, start_a - start_b,
970 - start_b + count_b, d->parent);
1103 + start_b + count_b, count_a, d->parent, d->target,
1104 + d->ignore_diffs);
1105 d->offset = start_a + count_a - (start_b + count_b);
1106 return 0;
1107 }
@@ -979,7 +1113,7 @@ static int blame_chunk_cb(long start_a, long count_a,
1113 */
1114 static void pass_blame_to_parent(struct blame_scoreboard *sb,
1115 struct blame_origin *target,
982 - struct blame_origin *parent)
1116 + struct blame_origin *parent, int ignore_diffs)
1117 {
1118 mmfile_t file_p, file_o;
1119 struct blame_chunk_cb_data d;
@@ -989,7 +1123,9 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
1123 return; /* nothing remains for this target */
1124
1125 d.parent = parent;
1126 + d.target = target;
1127 d.offset = 0;
1128 + d.ignore_diffs = ignore_diffs;
1129 d.dstq = &newdest; d.srcq = &target->suspects;
1130
1131 fill_origin_blob(&sb->revs->diffopt, parent, &file_p, &sb->num_read_blob);
@@ -1001,8 +1137,13 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
1137 oid_to_hex(&parent->commit->object.oid),
1138 oid_to_hex(&target->commit->object.oid));
1139 /* The rest are the same as the parent */
1004 - blame_chunk(&d.dstq, &d.srcq, INT_MAX, d.offset, INT_MAX, parent);
1140 + blame_chunk(&d.dstq, &d.srcq, INT_MAX, d.offset, INT_MAX, 0,
1141 + parent, target, 0);
1142 *d.dstq = NULL;
1143 + if (ignore_diffs)
1144 + newdest = llist_mergesort(newdest, get_next_blame,
1145 + set_next_blame,
1146 + compare_blame_suspect);
1147 queue_blames(sb, parent, newdest);
1148
1149 return;
@@ -1506,11 +1647,28 @@ static void pass_blame(struct blame_scoreboard *sb, struct blame_origin *origin,
1647 blame_origin_incref(porigin);
1648 origin->previous = porigin;
1649 }
1509 - pass_blame_to_parent(sb, origin, porigin);
1650 + pass_blame_to_parent(sb, origin, porigin, 0);
1651 if (!origin->suspects)
1652 goto finish;
1653 }
1654
1655 + /*
1656 + * Pass remaining suspects for ignored commits to their parents.
1657 + */
1658 + if (oidset_contains(&sb->ignore_list, &commit->object.oid)) {
1659 + for (i = 0, sg = first_scapegoat(revs, commit, sb->reverse);
1660 + i < num_sg && sg;
1661 + sg = sg->next, i++) {
1662 + struct blame_origin *porigin = sg_origin[i];
1663 +
1664 + if (!porigin)
1665 + continue;
1666 + pass_blame_to_parent(sb, origin, porigin, 1);
1667 + if (!origin->suspects)
1668 + goto finish;
1669 + }
1670 + }
1671 +
1672 /*
1673 * Optionally find moves in parents' files.
1674 */
blame.h
+2
@@ -117,6 +117,8 @@ struct blame_scoreboard {
117 /* linked list of blames */
118 struct blame_entry *ent;
119
120 + struct oidset ignore_list;
121 +
122 /* look-up a line in the final buffer */
123 int num_lines;
124 int *lineno;
builtin/blame.c
+38
@@ -52,6 +52,7 @@ static int no_whole_file_rename;
52 static int show_progress;
53 static char repeated_meta_color[COLOR_MAXLEN];
54 static int coloring_mode;
55 +static struct string_list ignore_revs_file_list = STRING_LIST_INIT_NODUP;
56
57 static struct date_mode blame_date_mode = { DATE_ISO8601 };
58 static size_t blame_date_width;
@@ -695,6 +696,16 @@ static int git_blame_config(const char *var, const char *value, void *cb)
696 parse_date_format(value, &blame_date_mode);
697 return 0;
698 }
699 + if (!strcmp(var, "blame.ignorerevsfile")) {
700 + const char *str;
701 + int ret;
702 +
703 + ret = git_config_pathname(&str, var, value);
704 + if (ret)
705 + return ret;
706 + string_list_insert(&ignore_revs_file_list, str);
707 + return 0;
708 + }
709 if (!strcmp(var, "color.blame.repeatedlines")) {
710 if (color_parse_mem(value, strlen(value), repeated_meta_color))
711 warning(_("invalid color '%s' in color.blame.repeatedLines"),
@@ -774,6 +785,27 @@ static int is_a_rev(const char *name)
785 return OBJ_NONE < oid_object_info(the_repository, &oid, NULL);
786 }
787
788 +static void build_ignorelist(struct blame_scoreboard *sb,
789 + struct string_list *ignore_revs_file_list,
790 + struct string_list *ignore_rev_list)
791 +{
792 + struct string_list_item *i;
793 + struct object_id oid;
794 +
795 + oidset_init(&sb->ignore_list, 0);
796 + for_each_string_list_item(i, ignore_revs_file_list) {
797 + if (!strcmp(i->string, ""))
798 + oidset_clear(&sb->ignore_list);
799 + else
800 + oidset_parse_file(&sb->ignore_list, i->string);
801 + }
802 + for_each_string_list_item(i, ignore_rev_list) {
803 + if (get_oid_committish(i->string, &oid))
804 + die(_("cannot find revision %s to ignore"), i->string);
805 + oidset_insert(&sb->ignore_list, &oid);
806 + }
807 +}
808 +
809 int cmd_blame(int argc, const char **argv, const char *prefix)
810 {
811 struct rev_info revs;
@@ -785,6 +817,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
817 struct progress_info pi = { NULL, 0 };
818
819 struct string_list range_list = STRING_LIST_INIT_NODUP;
820 + struct string_list ignore_rev_list = STRING_LIST_INIT_NODUP;
821 int output_option = 0, opt = 0;
822 int show_stats = 0;
823 const char *revs_file = NULL;
@@ -806,6 +839,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
839 OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
840 OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
841 OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
842 + OPT_STRING_LIST(0, "ignore-rev", &ignore_rev_list, N_("rev"), N_("Ignore <rev> when blaming")),
843 + OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("Ignore revisions from <file>")),
844 OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
845 OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
846
@@ -995,6 +1030,9 @@ parse_done:
1030 sb.contents_from = contents_from;
1031 sb.reverse = reverse;
1032 sb.repo = the_repository;
1033 + build_ignorelist(&sb, &ignore_revs_file_list, &ignore_rev_list);
1034 + string_list_clear(&ignore_revs_file_list, 0);
1035 + string_list_clear(&ignore_rev_list, 0);
1036 setup_scoreboard(&sb, path, &o);
1037 lno = sb.num_lines;
1038
t/t8013-blame-ignore-revs.sh new
+203
@@ -0,0 +1,203 @@
1 +#!/bin/sh
2 +
3 +test_description='ignore revisions when blaming'
4 +. ./test-lib.sh
5 +
6 +# Creates:
7 +# A--B--X
8 +# A added line 1 and B added line 2. X makes changes to those lines. Sanity
9 +# check that X is blamed for both lines.
10 +test_expect_success setup '
11 + test_commit A file line1 &&
12 +
13 + echo line2 >>file &&
14 + git add file &&
15 + test_tick &&
16 + git commit -m B &&
17 + git tag B &&
18 +
19 + test_write_lines line-one line-two >file &&
20 + git add file &&
21 + test_tick &&
22 + git commit -m X &&
23 + git tag X &&
24 +
25 + git blame --line-porcelain file >blame_raw &&
26 +
27 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
28 + git rev-parse X >expect &&
29 + test_cmp expect actual &&
30 +
31 + grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
32 + git rev-parse X >expect &&
33 + test_cmp expect actual
34 + '
35 +
36 +# Ignore X, make sure A is blamed for line 1 and B for line 2.
37 +test_expect_success ignore_rev_changing_lines '
38 + git blame --line-porcelain --ignore-rev X file >blame_raw &&
39 +
40 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
41 + git rev-parse A >expect &&
42 + test_cmp expect actual &&
43 +
44 + grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
45 + git rev-parse B >expect &&
46 + test_cmp expect actual
47 + '
48 +
49 +# For ignored revs that have added 'unblamable' lines, attribute those to the
50 +# ignored commit.
51 +# A--B--X--Y
52 +# Where Y changes lines 1 and 2, and adds lines 3 and 4. The added lines ought
53 +# to have nothing in common with "line-one" or "line-two", to keep any
54 +# heuristics from matching them with any lines in the parent.
55 +test_expect_success ignore_rev_adding_unblamable_lines '
56 + test_write_lines line-one-change line-two-changed y3 y4 >file &&
57 + git add file &&
58 + test_tick &&
59 + git commit -m Y &&
60 + git tag Y &&
61 +
62 + git rev-parse Y >expect &&
63 + git blame --line-porcelain file --ignore-rev Y >blame_raw &&
64 +
65 + grep -E "^[0-9a-f]+ [0-9]+ 3" blame_raw | sed -e "s/ .*//" >actual &&
66 + test_cmp expect actual &&
67 +
68 + grep -E "^[0-9a-f]+ [0-9]+ 4" blame_raw | sed -e "s/ .*//" >actual &&
69 + test_cmp expect actual
70 + '
71 +
72 +# Ignore X and Y, both in separate files. Lines 1 == A, 2 == B.
73 +test_expect_success ignore_revs_from_files '
74 + git rev-parse X >ignore_x &&
75 + git rev-parse Y >ignore_y &&
76 + git blame --line-porcelain file --ignore-revs-file ignore_x --ignore-revs-file ignore_y >blame_raw &&
77 +
78 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
79 + git rev-parse A >expect &&
80 + test_cmp expect actual &&
81 +
82 + grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
83 + git rev-parse B >expect &&
84 + test_cmp expect actual
85 + '
86 +
87 +# Ignore X from the config option, Y from a file.
88 +test_expect_success ignore_revs_from_configs_and_files '
89 + git config --add blame.ignoreRevsFile ignore_x &&
90 + git blame --line-porcelain file --ignore-revs-file ignore_y >blame_raw &&
91 +
92 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
93 + git rev-parse A >expect &&
94 + test_cmp expect actual &&
95 +
96 + grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
97 + git rev-parse B >expect &&
98 + test_cmp expect actual
99 + '
100 +
101 +# Override blame.ignoreRevsFile (ignore_x) with an empty string. X should be
102 +# blamed now for lines 1 and 2, since we are no longer ignoring X.
103 +test_expect_success override_ignore_revs_file '
104 + git blame --line-porcelain file --ignore-revs-file "" --ignore-revs-file ignore_y >blame_raw &&
105 + git rev-parse X >expect &&
106 +
107 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
108 + test_cmp expect actual &&
109 +
110 + grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
111 + test_cmp expect actual
112 + '
113 +test_expect_success bad_files_and_revs '
114 + test_must_fail git blame file --ignore-rev NOREV 2>err &&
115 + test_i18ngrep "cannot find revision NOREV to ignore" err &&
116 +
117 + test_must_fail git blame file --ignore-revs-file NOFILE 2>err &&
118 + test_i18ngrep "could not open.*: NOFILE" err &&
119 +
120 + echo NOREV >ignore_norev &&
121 + test_must_fail git blame file --ignore-revs-file ignore_norev 2>err &&
122 + test_i18ngrep "invalid object name: NOREV" err
123 + '
124 +# The heuristic called by guess_line_blames() tries to find the size of a
125 +# blame_entry 'e' in the parent's address space. Those calculations need to
126 +# check for negative or zero values for when a blame entry is completely outside
127 +# the window of the parent's version of a file.
128 +#
129 +# This happens when one commit adds several lines (commit B below). A later
130 +# commit (C) changes one line in the middle of B's change. Commit C gets blamed
131 +# for its change, and that breaks up B's change into multiple blame entries.
132 +# When processing B, one of the blame_entries is outside A's window (which was
133 +# zero - it had no lines added on its side of the diff).
134 +#
135 +# A--B--C, ignore B to test the ignore heuristic's boundary checks.
136 +test_expect_success ignored_chunk_negative_parent_size '
137 + rm -rf .git/ &&
138 + git init &&
139 +
140 + test_write_lines L1 L2 L7 L8 L9 >file &&
141 + git add file &&
142 + test_tick &&
143 + git commit -m A &&
144 + git tag A &&
145 +
146 + test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
147 + git add file &&
148 + test_tick &&
149 + git commit -m B &&
150 + git tag B &&
151 +
152 + test_write_lines L1 L2 L3 L4 xxx L6 L7 L8 L9 >file &&
153 + git add file &&
154 + test_tick &&
155 + git commit -m C &&
156 + git tag C &&
157 +
158 + git blame file --ignore-rev B >blame_raw
159 + '
160 +
161 +# Resetting the repo and creating:
162 +#
163 +# A--B--M
164 +# \ /
165 +# C-+
166 +#
167 +# 'A' creates a file. B changes line 1, and C changes line 9. M merges.
168 +test_expect_success ignore_merge '
169 + rm -rf .git/ &&
170 + git init &&
171 +
172 + test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
173 + git add file &&
174 + test_tick &&
175 + git commit -m A &&
176 + git tag A &&
177 +
178 + test_write_lines BB L2 L3 L4 L5 L6 L7 L8 L9 >file &&
179 + git add file &&
180 + test_tick &&
181 + git commit -m B &&
182 + git tag B &&
183 +
184 + git reset --hard A &&
185 + test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 CC >file &&
186 + git add file &&
187 + test_tick &&
188 + git commit -m C &&
189 + git tag C &&
190 +
191 + test_merge M B &&
192 + git blame --line-porcelain file --ignore-rev M >blame_raw &&
193 +
194 + grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
195 + git rev-parse B >expect &&
196 + test_cmp expect actual &&
197 +
198 + grep -E "^[0-9a-f]+ [0-9]+ 9" blame_raw | sed -e "s/ .*//" >actual &&
199 + git rev-parse C >expect &&
200 + test_cmp expect actual
201 + '
202 +
203 +test_done