Merge branch 'br/blame-ignore'

"git blame" learned to "ignore" commits in the history, whose
effects (as well as their presence) get ignored.

* br/blame-ignore:
  t8014: remove unnecessary braces
  blame: drop some unused function parameters
  blame: add a test to cover blame_coalesce()
  blame: use the fingerprint heuristic to match ignored lines
  blame: add a fingerprint heuristic to match ignored lines
  blame: optionally track line fingerprints during fill_blame_origin()
  blame: add config options for the output of ignored or unblamable lines
  blame: add the ability to ignore commits and their changes
  blame: use a helper function in blame_chunk()
  Move oidset_parse_file() to oidset.c
  fsck: rename and touch up init_skiplist()
This commit is contained in:
Junio C Hamano 2019-07-19 11:30:20 -07:00
commit 209f075593
13 changed files with 1855 additions and 99 deletions

View File

@ -110,5 +110,24 @@ commit. And the default value is 40. If there are more than one
`-C` options given, the <num> argument of the last `-C` will
take effect.
--ignore-rev <rev>::
Ignore changes made by the revision when assigning blame, as if the
change never happened. Lines that were changed or added by an ignored
commit will be blamed on the previous commit that changed that line or
nearby lines. This option may be specified multiple times to ignore
more than one revision. If the `blame.markIgnoredLines` config option
is set, then lines that were changed by an ignored commit and attributed to
another commit will be marked with a `?` in the blame output. If the
`blame.markUnblamableLines` config option is set, then those lines touched
by an ignored commit that we could not attribute to another revision are
marked with a '*'.
--ignore-revs-file <file>::
Ignore revisions listed in `file`, which must be in the same format as an
`fsck.skipList`. This option may be repeated, and these files will be
processed after any files specified with the `blame.ignoreRevsFile` config
option. An empty file name, `""`, will clear the list of revs from
previously processed files.
-h::
Show help message.

View File

@ -19,3 +19,19 @@ blame.showEmail::
blame.showRoot::
Do not treat root commits as boundaries in linkgit:git-blame[1].
This option defaults to false.
blame.ignoreRevsFile::
Ignore revisions listed in the file, one unabbreviated object name per
line, in linkgit:git-blame[1]. Whitespace and comments beginning with
`#` are ignored. This option may be repeated multiple times. Empty
file names will reset the list of ignored revisions. This option will
be handled before the command line option `--ignore-revs-file`.
blame.markUnblamables::
Mark lines that were changed by an ignored revision that we could not
attribute to another commit with a '*' in the output of
linkgit:git-blame[1].
blame.markIgnoredLines::
Mark lines that were changed by an ignored revision that we attributed to
another commit with a '?' in the output of linkgit:git-blame[1].

View File

@ -10,6 +10,7 @@ SYNOPSIS
[verse]
'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental]
[-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
[--ignore-rev <rev>] [--ignore-revs-file <file>]
[--progress] [--abbrev=<n>] [<rev> | --contents <file> | --reverse <rev>..<rev>]
[--] <file>

1015
blame.c

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,8 @@ struct blame_origin {
*/
struct blame_entry *suspects;
mmfile_t file;
int num_lines;
void *fingerprints;
struct object_id blob_oid;
unsigned short mode;
/* guilty gets set when shipping any suspects to the final
@ -92,6 +94,8 @@ struct blame_entry {
* scanning the lines over and over.
*/
unsigned score;
int ignored;
int unblamable;
};
/*
@ -117,6 +121,8 @@ struct blame_scoreboard {
/* linked list of blames */
struct blame_entry *ent;
struct oidset ignore_list;
/* look-up a line in the final buffer */
int num_lines;
int *lineno;

View File

@ -53,6 +53,9 @@ static int no_whole_file_rename;
static int show_progress;
static char repeated_meta_color[COLOR_MAXLEN];
static int coloring_mode;
static struct string_list ignore_revs_file_list = STRING_LIST_INIT_NODUP;
static int mark_unblamable_lines;
static int mark_ignored_lines;
static struct date_mode blame_date_mode = { DATE_ISO8601 };
static size_t blame_date_width;
@ -480,6 +483,14 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
}
}
if (mark_unblamable_lines && ent->unblamable) {
length--;
putchar('*');
}
if (mark_ignored_lines && ent->ignored) {
length--;
putchar('?');
}
printf("%.*s", length, hex);
if (opt & OUTPUT_ANNOTATE_COMPAT) {
const char *name;
@ -696,6 +707,24 @@ static int git_blame_config(const char *var, const char *value, void *cb)
parse_date_format(value, &blame_date_mode);
return 0;
}
if (!strcmp(var, "blame.ignorerevsfile")) {
const char *str;
int ret;
ret = git_config_pathname(&str, var, value);
if (ret)
return ret;
string_list_insert(&ignore_revs_file_list, str);
return 0;
}
if (!strcmp(var, "blame.markunblamablelines")) {
mark_unblamable_lines = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "blame.markignoredlines")) {
mark_ignored_lines = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "color.blame.repeatedlines")) {
if (color_parse_mem(value, strlen(value), repeated_meta_color))
warning(_("invalid color '%s' in color.blame.repeatedLines"),
@ -775,6 +804,27 @@ static int is_a_rev(const char *name)
return OBJ_NONE < oid_object_info(the_repository, &oid, NULL);
}
static void build_ignorelist(struct blame_scoreboard *sb,
struct string_list *ignore_revs_file_list,
struct string_list *ignore_rev_list)
{
struct string_list_item *i;
struct object_id oid;
oidset_init(&sb->ignore_list, 0);
for_each_string_list_item(i, ignore_revs_file_list) {
if (!strcmp(i->string, ""))
oidset_clear(&sb->ignore_list);
else
oidset_parse_file(&sb->ignore_list, i->string);
}
for_each_string_list_item(i, ignore_rev_list) {
if (get_oid_committish(i->string, &oid))
die(_("cannot find revision %s to ignore"), i->string);
oidset_insert(&sb->ignore_list, &oid);
}
}
int cmd_blame(int argc, const char **argv, const char *prefix)
{
struct rev_info revs;
@ -786,6 +836,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
struct progress_info pi = { NULL, 0 };
struct string_list range_list = STRING_LIST_INIT_NODUP;
struct string_list ignore_rev_list = STRING_LIST_INIT_NODUP;
int output_option = 0, opt = 0;
int show_stats = 0;
const char *revs_file = NULL;
@ -807,6 +858,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
OPT_STRING_LIST(0, "ignore-rev", &ignore_rev_list, N_("rev"), N_("Ignore <rev> when blaming")),
OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("Ignore revisions from <file>")),
OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
@ -1012,6 +1065,9 @@ parse_done:
sb.contents_from = contents_from;
sb.reverse = reverse;
sb.repo = the_repository;
build_ignorelist(&sb, &ignore_revs_file_list, &ignore_rev_list);
string_list_clear(&ignore_revs_file_list, 0);
string_list_clear(&ignore_rev_list, 0);
setup_scoreboard(&sb, path, &o);
lno = sb.num_lines;

37
fsck.c
View File

@ -181,41 +181,6 @@ static int fsck_msg_type(enum fsck_msg_id msg_id,
return msg_type;
}
static void init_skiplist(struct fsck_options *options, const char *path)
{
FILE *fp;
struct strbuf sb = STRBUF_INIT;
struct object_id oid;
fp = fopen(path, "r");
if (!fp)
die("Could not open skip list: %s", path);
while (!strbuf_getline(&sb, fp)) {
const char *p;
const char *hash;
/*
* Allow trailing comments, leading whitespace
* (including before commits), and empty or whitespace
* only lines.
*/
hash = strchr(sb.buf, '#');
if (hash)
strbuf_setlen(&sb, hash - sb.buf);
strbuf_trim(&sb);
if (!sb.len)
continue;
if (parse_oid_hex(sb.buf, &oid, &p) || *p != '\0')
die("Invalid SHA-1: %s", sb.buf);
oidset_insert(&options->skiplist, &oid);
}
if (ferror(fp))
die_errno("Could not read '%s'", path);
fclose(fp);
strbuf_release(&sb);
}
static int parse_msg_type(const char *str)
{
if (!strcmp(str, "error"))
@ -284,7 +249,7 @@ void fsck_set_msg_types(struct fsck_options *options, const char *values)
if (!strcmp(buf, "skiplist")) {
if (equal == len)
die("skiplist requires a path");
init_skiplist(options, buf + equal + 1);
oidset_parse_file(&options->skiplist, buf + equal + 1);
buf += len + 1;
continue;
}

View File

@ -35,3 +35,38 @@ void oidset_clear(struct oidset *set)
kh_release_oid_set(&set->set);
oidset_init(set, 0);
}
void oidset_parse_file(struct oidset *set, const char *path)
{
FILE *fp;
struct strbuf sb = STRBUF_INIT;
struct object_id oid;
fp = fopen(path, "r");
if (!fp)
die("could not open object name list: %s", path);
while (!strbuf_getline(&sb, fp)) {
const char *p;
const char *name;
/*
* Allow trailing comments, leading whitespace
* (including before commits), and empty or whitespace
* only lines.
*/
name = strchr(sb.buf, '#');
if (name)
strbuf_setlen(&sb, name - sb.buf);
strbuf_trim(&sb);
if (!sb.len)
continue;
if (parse_oid_hex(sb.buf, &oid, &p) || *p != '\0')
die("invalid object name: %s", sb.buf);
oidset_insert(set, &oid);
}
if (ferror(fp))
die_errno("Could not read '%s'", path);
fclose(fp);
strbuf_release(&sb);
}

View File

@ -61,6 +61,14 @@ int oidset_remove(struct oidset *set, const struct object_id *oid);
*/
void oidset_clear(struct oidset *set);
/**
* Add the contents of the file 'path' to an initialized oidset. Each line is
* an unabbreviated object name. Comments begin with '#', and trailing comments
* are allowed. Leading whitespace and empty or white-space only lines are
* ignored.
*/
void oidset_parse_file(struct oidset *set, const char *path);
struct oidset_iter {
kh_oid_set_t *set;
khiter_t iter;

View File

@ -164,9 +164,9 @@ test_expect_success 'fsck with unsorted skipList' '
test_expect_success 'fsck with invalid or bogus skipList input' '
git -c fsck.skipList=/dev/null -c fsck.missingEmail=ignore fsck &&
test_must_fail git -c fsck.skipList=does-not-exist -c fsck.missingEmail=ignore fsck 2>err &&
test_i18ngrep "Could not open skip list: does-not-exist" err &&
test_i18ngrep "could not open.*: does-not-exist" err &&
test_must_fail git -c fsck.skipList=.git/config -c fsck.missingEmail=ignore fsck 2>err &&
test_i18ngrep "Invalid SHA-1: \[core\]" err
test_i18ngrep "invalid object name: \[core\]" err
'
test_expect_success 'fsck with other accepted skipList input (comments & empty lines)' '
@ -193,7 +193,7 @@ test_expect_success 'fsck no garbage output from comments & empty lines errors'
test_expect_success 'fsck with invalid abbreviated skipList input' '
echo $commit | test_copy_bytes 20 >SKIP.abbreviated &&
test_must_fail git -c fsck.skipList=SKIP.abbreviated fsck 2>err-abbreviated &&
test_i18ngrep "^fatal: Invalid SHA-1: " err-abbreviated
test_i18ngrep "^fatal: invalid object name: " err-abbreviated
'
test_expect_success 'fsck with exhaustive accepted skipList input (various types of comments etc.)' '
@ -226,10 +226,10 @@ test_expect_success 'push with receive.fsck.skipList' '
test_must_fail git push --porcelain dst bogus &&
git --git-dir=dst/.git config receive.fsck.skipList does-not-exist &&
test_must_fail git push --porcelain dst bogus 2>err &&
test_i18ngrep "Could not open skip list: does-not-exist" err &&
test_i18ngrep "could not open.*: does-not-exist" err &&
git --git-dir=dst/.git config receive.fsck.skipList config &&
test_must_fail git push --porcelain dst bogus 2>err &&
test_i18ngrep "Invalid SHA-1: \[core\]" err &&
test_i18ngrep "invalid object name: \[core\]" err &&
git --git-dir=dst/.git config receive.fsck.skipList SKIP &&
git push --porcelain dst bogus
@ -255,10 +255,10 @@ test_expect_success 'fetch with fetch.fsck.skipList' '
test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec &&
git --git-dir=dst/.git config fetch.fsck.skipList does-not-exist &&
test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err &&
test_i18ngrep "Could not open skip list: does-not-exist" err &&
test_i18ngrep "could not open.*: does-not-exist" err &&
git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/config &&
test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err &&
test_i18ngrep "Invalid SHA-1: \[core\]" err &&
test_i18ngrep "invalid object name: \[core\]" err &&
git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/SKIP &&
git --git-dir=dst/.git fetch "file://$(pwd)" $refspec

View File

@ -275,4 +275,40 @@ test_expect_success 'blame file with CRLF core.autocrlf=true' '
grep "A U Thor" actual
'
# Tests the splitting and merging of blame entries in blame_coalesce().
# The output of blame is the same, regardless of whether blame_coalesce() runs
# or not, so we'd likely only notice a problem if blame crashes or assigned
# blame to the "splitting" commit ('SPLIT' below).
test_expect_success 'blame coalesce' '
cat >giraffe <<-\EOF &&
ABC
DEF
EOF
git add giraffe &&
git commit -m "original file" &&
oid=$(git rev-parse HEAD) &&
cat >giraffe <<-\EOF &&
ABC
SPLIT
DEF
EOF
git add giraffe &&
git commit -m "interior SPLIT line" &&
cat >giraffe <<-\EOF &&
ABC
DEF
EOF
git add giraffe &&
git commit -m "same contents as original" &&
cat >expect <<-EOF &&
$oid 1) ABC
$oid 2) DEF
EOF
git -c core.abbrev=40 blame -s giraffe >actual &&
test_cmp expect actual
'
test_done

274
t/t8013-blame-ignore-revs.sh Executable file
View File

@ -0,0 +1,274 @@
#!/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
'
# For ignored revs that have added 'unblamable' lines, mark those lines with a
# '*'
# A--B--X--Y
# Lines 3 and 4 are from Y and unblamable. This was set up in
# ignore_rev_adding_unblamable_lines.
test_expect_success mark_unblamable_lines '
git config --add blame.markUnblamableLines true &&
git blame --ignore-rev Y file >blame_raw &&
echo "*" >expect &&
sed -n "3p" blame_raw | cut -c1 >actual &&
test_cmp expect actual &&
sed -n "4p" blame_raw | cut -c1 >actual &&
test_cmp expect actual
'
# Commit Z will touch the first two lines. Y touched all four.
# A--B--X--Y--Z
# The blame output when ignoring Z should be:
# ?Y ... 1)
# ?Y ... 2)
# Y ... 3)
# Y ... 4)
# We're checking only the first character
test_expect_success mark_ignored_lines '
git config --add blame.markIgnoredLines true &&
test_write_lines line-one-Z line-two-Z y3 y4 >file &&
git add file &&
test_tick &&
git commit -m Z &&
git tag Z &&
git blame --ignore-rev Z file >blame_raw &&
echo "?" >expect &&
sed -n "1p" blame_raw | cut -c1 >actual &&
test_cmp expect actual &&
sed -n "2p" blame_raw | cut -c1 >actual &&
test_cmp expect actual &&
sed -n "3p" blame_raw | cut -c1 >actual &&
! test_cmp expect actual &&
sed -n "4p" blame_raw | cut -c1 >actual &&
! test_cmp expect actual
'
# For ignored revs that added 'unblamable' lines and more recent commits changed
# the blamable lines, mark the unblamable lines with a
# '*'
# A--B--X--Y--Z
# Lines 3 and 4 are from Y and unblamable, as set up in
# ignore_rev_adding_unblamable_lines. Z changed lines 1 and 2.
test_expect_success mark_unblamable_lines_intermediate '
git config --add blame.markUnblamableLines true &&
git blame --ignore-rev Y file >blame_raw 2>stderr &&
echo "*" >expect &&
sed -n "3p" blame_raw | cut -c1 >actual &&
test_cmp expect actual &&
sed -n "4p" blame_raw | cut -c1 >actual &&
test_cmp expect actual
'
# 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

437
t/t8014-blame-ignore-fuzzy.sh Executable file
View File

@ -0,0 +1,437 @@
#!/bin/sh
test_description='git blame ignore fuzzy heuristic'
. ./test-lib.sh
pick_author='s/^[0-9a-f^]* *(\([^ ]*\) .*/\1/'
# Each test is composed of 4 variables:
# titleN - the test name
# aN - the initial content
# bN - the final content
# expectedN - the line numbers from aN that we expect git blame
# on bN to identify, or "Final" if bN itself should
# be identified as the origin of that line.
# We start at test 2 because setup will show as test 1
title2="Regression test for partially overlapping search ranges"
cat <<EOF >a2
1
2
3
abcdef
5
6
7
ijkl
9
10
11
pqrs
13
14
15
wxyz
17
18
19
EOF
cat <<EOF >b2
abcde
ijk
pqr
wxy
EOF
cat <<EOF >expected2
4
8
12
16
EOF
title3="Combine 3 lines into 2"
cat <<EOF >a3
if ((maxgrow==0) ||
( single_line_field && (field->dcols < maxgrow)) ||
(!single_line_field && (field->drows < maxgrow)))
EOF
cat <<EOF >b3
if ((maxgrow == 0) || (single_line_field && (field->dcols < maxgrow)) ||
(!single_line_field && (field->drows < maxgrow))) {
EOF
cat <<EOF >expected3
2
3
EOF
title4="Add curly brackets"
cat <<EOF >a4
if (rows) *rows = field->rows;
if (cols) *cols = field->cols;
if (frow) *frow = field->frow;
if (fcol) *fcol = field->fcol;
EOF
cat <<EOF >b4
if (rows) {
*rows = field->rows;
}
if (cols) {
*cols = field->cols;
}
if (frow) {
*frow = field->frow;
}
if (fcol) {
*fcol = field->fcol;
}
EOF
cat <<EOF >expected4
1
1
Final
2
2
Final
3
3
Final
4
4
Final
EOF
title5="Combine many lines and change case"
cat <<EOF >a5
for(row=0,pBuffer=field->buf;
row<height;
row++,pBuffer+=width )
{
if ((len = (int)( After_End_Of_Data( pBuffer, width ) - pBuffer )) > 0)
{
wmove( win, row, 0 );
waddnstr( win, pBuffer, len );
EOF
cat <<EOF >b5
for (Row = 0, PBuffer = field->buf; Row < Height; Row++, PBuffer += Width) {
if ((Len = (int)(afterEndOfData(PBuffer, Width) - PBuffer)) > 0) {
wmove(win, Row, 0);
waddnstr(win, PBuffer, Len);
EOF
cat <<EOF >expected5
1
5
7
8
EOF
title6="Rename and combine lines"
cat <<EOF >a6
bool need_visual_update = ((form != (FORM *)0) &&
(form->status & _POSTED) &&
(form->current==field));
if (need_visual_update)
Synchronize_Buffer(form);
if (single_line_field)
{
growth = field->cols * amount;
if (field->maxgrow)
growth = Minimum(field->maxgrow - field->dcols,growth);
field->dcols += growth;
if (field->dcols == field->maxgrow)
EOF
cat <<EOF >b6
bool NeedVisualUpdate = ((Form != (FORM *)0) && (Form->status & _POSTED) &&
(Form->current == field));
if (NeedVisualUpdate) {
synchronizeBuffer(Form);
}
if (SingleLineField) {
Growth = field->cols * amount;
if (field->maxgrow) {
Growth = Minimum(field->maxgrow - field->dcols, Growth);
}
field->dcols += Growth;
if (field->dcols == field->maxgrow) {
EOF
cat <<EOF >expected6
1
3
4
5
6
Final
7
8
10
11
12
Final
13
14
EOF
# Both lines match identically so position must be used to tie-break.
title7="Same line twice"
cat <<EOF >a7
abc
abc
EOF
cat <<EOF >b7
abcd
abcd
EOF
cat <<EOF >expected7
1
2
EOF
title8="Enforce line order"
cat <<EOF >a8
abcdef
ghijkl
ab
EOF
cat <<EOF >b8
ghijk
abcd
EOF
cat <<EOF >expected8
2
3
EOF
title9="Expand lines and rename variables"
cat <<EOF >a9
int myFunction(int ArgumentOne, Thing *ArgTwo, Blah XuglyBug) {
Squiggle FabulousResult = squargle(ArgumentOne, *ArgTwo,
XuglyBug) + EwwwGlobalWithAReallyLongNameYepTooLong;
return FabulousResult * 42;
}
EOF
cat <<EOF >b9
int myFunction(int argument_one, Thing *arg_asdfgh,
Blah xugly_bug) {
Squiggle fabulous_result = squargle(argument_one,
*arg_asdfgh, xugly_bug)
+ g_ewww_global_with_a_really_long_name_yep_too_long;
return fabulous_result * 42;
}
EOF
cat <<EOF >expected9
1
1
2
3
3
4
5
EOF
title10="Two close matches versus one less close match"
cat <<EOF >a10
abcdef
abcdef
ghijkl
EOF
cat <<EOF >b10
gh
abcdefx
EOF
cat <<EOF >expected10
Final
2
EOF
# The first line of b matches best with the last line of a, but the overall
# match is better if we match it with the the first line of a.
title11="Piggy in the middle"
cat <<EOF >a11
abcdefg
ijklmn
abcdefgh
EOF
cat <<EOF >b11
abcdefghx
ijklm
EOF
cat <<EOF >expected11
1
2
EOF
title12="No trailing newline"
printf "abc\ndef" >a12
printf "abx\nstu" >b12
cat <<EOF >expected12
1
Final
EOF
title13="Reorder includes"
cat <<EOF >a13
#include "c.h"
#include "b.h"
#include "a.h"
#include "e.h"
#include "d.h"
EOF
cat <<EOF >b13
#include "a.h"
#include "b.h"
#include "c.h"
#include "d.h"
#include "e.h"
EOF
cat <<EOF >expected13
3
2
1
5
4
EOF
last_test=13
test_expect_success setup '
for i in $(test_seq 2 $last_test)
do
# Append each line in a separate commit to make it easy to
# check which original line the blame output relates to.
line_count=0 &&
while IFS= read line
do
line_count=$((line_count+1)) &&
echo "$line" >>"$i" &&
git add "$i" &&
test_tick &&
GIT_AUTHOR_NAME="$line_count" git commit -m "$line_count"
done <"a$i"
done &&
for i in $(test_seq 2 $last_test)
do
# Overwrite the files with the final content.
cp b$i $i &&
git add $i
done &&
test_tick &&
# Commit the final content all at once so it can all be
# referred to with the same commit ID.
GIT_AUTHOR_NAME=Final git commit -m Final &&
IGNOREME=$(git rev-parse HEAD)
'
for i in $(test_seq 2 $last_test); do
eval title="\$title$i"
test_expect_success "$title" \
"git blame -M9 --ignore-rev $IGNOREME $i >output &&
sed -e \"$pick_author\" output >actual &&
test_cmp expected$i actual"
done
# This invoked a null pointer dereference when the chunk callback was called
# with a zero length parent chunk and there were no more suspects.
test_expect_success 'Diff chunks with no suspects' '
test_write_lines xy1 A B C xy1 >file &&
git add file &&
test_tick &&
GIT_AUTHOR_NAME=1 git commit -m 1 &&
test_write_lines xy2 A B xy2 C xy2 >file &&
git add file &&
test_tick &&
GIT_AUTHOR_NAME=2 git commit -m 2 &&
REV_2=$(git rev-parse HEAD) &&
test_write_lines xy3 A >file &&
git add file &&
test_tick &&
GIT_AUTHOR_NAME=3 git commit -m 3 &&
REV_3=$(git rev-parse HEAD) &&
test_write_lines 1 1 >expected &&
git blame --ignore-rev $REV_2 --ignore-rev $REV_3 file >output &&
sed -e "$pick_author" output >actual &&
test_cmp expected actual
'
test_expect_success 'position matching' '
test_write_lines abc def >file2 &&
git add file2 &&
test_tick &&
GIT_AUTHOR_NAME=1 git commit -m 1 &&
test_write_lines abc def abc def >file2 &&
git add file2 &&
test_tick &&
GIT_AUTHOR_NAME=2 git commit -m 2 &&
test_write_lines abcx defx abcx defx >file2 &&
git add file2 &&
test_tick &&
GIT_AUTHOR_NAME=3 git commit -m 3 &&
REV_3=$(git rev-parse HEAD) &&
test_write_lines abcy defy abcx defx >file2 &&
git add file2 &&
test_tick &&
GIT_AUTHOR_NAME=4 git commit -m 4 &&
REV_4=$(git rev-parse HEAD) &&
test_write_lines 1 1 2 2 >expected &&
git blame --ignore-rev $REV_3 --ignore-rev $REV_4 file2 >output &&
sed -e "$pick_author" output >actual &&
test_cmp expected actual
'
# This fails if each blame entry is processed independently instead of
# processing each diff change in full.
test_expect_success 'preserve order' '
test_write_lines bcde >file3 &&
git add file3 &&
test_tick &&
GIT_AUTHOR_NAME=1 git commit -m 1 &&
test_write_lines bcde fghij >file3 &&
git add file3 &&
test_tick &&
GIT_AUTHOR_NAME=2 git commit -m 2 &&
test_write_lines bcde fghij abcd >file3 &&
git add file3 &&
test_tick &&
GIT_AUTHOR_NAME=3 git commit -m 3 &&
test_write_lines abcdx fghijx bcdex >file3 &&
git add file3 &&
test_tick &&
GIT_AUTHOR_NAME=4 git commit -m 4 &&
REV_4=$(git rev-parse HEAD) &&
test_write_lines abcdx fghijy bcdex >file3 &&
git add file3 &&
test_tick &&
GIT_AUTHOR_NAME=5 git commit -m 5 &&
REV_5=$(git rev-parse HEAD) &&
test_write_lines 1 2 3 >expected &&
git blame --ignore-rev $REV_4 --ignore-rev $REV_5 file3 >output &&
sed -e "$pick_author" output >actual &&
test_cmp expected actual
'
test_done