summaryrefslogtreecommitdiff
path: root/t/t8013-blame-ignore-revs.sh
diff options
context:
space:
mode:
authorBarret Rhoden <brho@google.com>2019-05-15 21:44:59 (GMT)
committerJunio C Hamano <gitster@pobox.com>2019-05-16 02:36:23 (GMT)
commitae3f36dea16e51041c56ba9ed6b38380c8421816 (patch)
treefb04baec44c541f4c6f5c680f6be0388aab3a166 /t/t8013-blame-ignore-revs.sh
parent55f808fbc540c64d22393231bd55058fda22c3ce (diff)
downloadgit-ae3f36dea16e51041c56ba9ed6b38380c8421816.zip
git-ae3f36dea16e51041c56ba9ed6b38380c8421816.tar.gz
git-ae3f36dea16e51041c56ba9ed6b38380c8421816.tar.bz2
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>
Diffstat (limited to 't/t8013-blame-ignore-revs.sh')
-rwxr-xr-xt/t8013-blame-ignore-revs.sh203
1 files changed, 203 insertions, 0 deletions
diff --git a/t/t8013-blame-ignore-revs.sh b/t/t8013-blame-ignore-revs.sh
new file mode 100755
index 0000000..fdb2fa8
--- /dev/null
+++ b/t/t8013-blame-ignore-revs.sh
@@ -0,0 +1,203 @@
+#!/bin/sh
+
+test_description='ignore revisions when blaming'
+. ./test-lib.sh
+
+# Creates:
+# A--B--X
+# A added line 1 and B added line 2. X makes changes to those lines. Sanity
+# check that X is blamed for both lines.
+test_expect_success setup '
+ test_commit A file line1 &&
+
+ echo line2 >>file &&
+ git add file &&
+ test_tick &&
+ git commit -m B &&
+ git tag B &&
+
+ test_write_lines line-one line-two >file &&
+ git add file &&
+ test_tick &&
+ git commit -m X &&
+ git tag X &&
+
+ git blame --line-porcelain file >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse X >expect &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse X >expect &&
+ test_cmp expect actual
+ '
+
+# Ignore X, make sure A is blamed for line 1 and B for line 2.
+test_expect_success ignore_rev_changing_lines '
+ git blame --line-porcelain --ignore-rev X file >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse A >expect &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse B >expect &&
+ test_cmp expect actual
+ '
+
+# For ignored revs that have added 'unblamable' lines, attribute those to the
+# ignored commit.
+# A--B--X--Y
+# Where Y changes lines 1 and 2, and adds lines 3 and 4. The added lines ought
+# to have nothing in common with "line-one" or "line-two", to keep any
+# heuristics from matching them with any lines in the parent.
+test_expect_success ignore_rev_adding_unblamable_lines '
+ test_write_lines line-one-change line-two-changed y3 y4 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m Y &&
+ git tag Y &&
+
+ git rev-parse Y >expect &&
+ git blame --line-porcelain file --ignore-rev Y >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 3" blame_raw | sed -e "s/ .*//" >actual &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 4" blame_raw | sed -e "s/ .*//" >actual &&
+ test_cmp expect actual
+ '
+
+# Ignore X and Y, both in separate files. Lines 1 == A, 2 == B.
+test_expect_success ignore_revs_from_files '
+ git rev-parse X >ignore_x &&
+ git rev-parse Y >ignore_y &&
+ git blame --line-porcelain file --ignore-revs-file ignore_x --ignore-revs-file ignore_y >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse A >expect &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse B >expect &&
+ test_cmp expect actual
+ '
+
+# Ignore X from the config option, Y from a file.
+test_expect_success ignore_revs_from_configs_and_files '
+ git config --add blame.ignoreRevsFile ignore_x &&
+ git blame --line-porcelain file --ignore-revs-file ignore_y >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse A >expect &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse B >expect &&
+ test_cmp expect actual
+ '
+
+# Override blame.ignoreRevsFile (ignore_x) with an empty string. X should be
+# blamed now for lines 1 and 2, since we are no longer ignoring X.
+test_expect_success override_ignore_revs_file '
+ git blame --line-porcelain file --ignore-revs-file "" --ignore-revs-file ignore_y >blame_raw &&
+ git rev-parse X >expect &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+ test_cmp expect actual
+ '
+test_expect_success bad_files_and_revs '
+ test_must_fail git blame file --ignore-rev NOREV 2>err &&
+ test_i18ngrep "cannot find revision NOREV to ignore" err &&
+
+ test_must_fail git blame file --ignore-revs-file NOFILE 2>err &&
+ test_i18ngrep "could not open.*: NOFILE" err &&
+
+ echo NOREV >ignore_norev &&
+ test_must_fail git blame file --ignore-revs-file ignore_norev 2>err &&
+ test_i18ngrep "invalid object name: NOREV" err
+ '
+# The heuristic called by guess_line_blames() tries to find the size of a
+# blame_entry 'e' in the parent's address space. Those calculations need to
+# check for negative or zero values for when a blame entry is completely outside
+# the window of the parent's version of a file.
+#
+# This happens when one commit adds several lines (commit B below). A later
+# commit (C) changes one line in the middle of B's change. Commit C gets blamed
+# for its change, and that breaks up B's change into multiple blame entries.
+# When processing B, one of the blame_entries is outside A's window (which was
+# zero - it had no lines added on its side of the diff).
+#
+# A--B--C, ignore B to test the ignore heuristic's boundary checks.
+test_expect_success ignored_chunk_negative_parent_size '
+ rm -rf .git/ &&
+ git init &&
+
+ test_write_lines L1 L2 L7 L8 L9 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m A &&
+ git tag A &&
+
+ test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m B &&
+ git tag B &&
+
+ test_write_lines L1 L2 L3 L4 xxx L6 L7 L8 L9 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m C &&
+ git tag C &&
+
+ git blame file --ignore-rev B >blame_raw
+ '
+
+# Resetting the repo and creating:
+#
+# A--B--M
+# \ /
+# C-+
+#
+# 'A' creates a file. B changes line 1, and C changes line 9. M merges.
+test_expect_success ignore_merge '
+ rm -rf .git/ &&
+ git init &&
+
+ test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m A &&
+ git tag A &&
+
+ test_write_lines BB L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+ git add file &&
+ test_tick &&
+ git commit -m B &&
+ git tag B &&
+
+ git reset --hard A &&
+ test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 CC >file &&
+ git add file &&
+ test_tick &&
+ git commit -m C &&
+ git tag C &&
+
+ test_merge M B &&
+ git blame --line-porcelain file --ignore-rev M >blame_raw &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse B >expect &&
+ test_cmp expect actual &&
+
+ grep -E "^[0-9a-f]+ [0-9]+ 9" blame_raw | sed -e "s/ .*//" >actual &&
+ git rev-parse C >expect &&
+ test_cmp expect actual
+ '
+
+test_done