From ec352401fb7f74939cbd1154a878e094f75d79ce Mon Sep 17 00:00:00 2001 From: Max Kirillov Date: Fri, 30 Oct 2015 07:01:51 +0200 Subject: blame: test to describe use of blame --reverse --first-parent Reverse blame can be used to locate removal of lines which does not change adjacent lines. Such edits do not appear in non-reverse blame, because the adjacent lines last changed commit is older history, before the edit. For a big and active project which uses topic branches, or analogous feature, for example pull-requests, the history can contain many concurrent branches, and even after an edit merged into the target branch, there are still many (sometimes several tens or even hundreds) topic branch which do not contain it: a0--a1-----*a2-*a3-a4...-*a100 |\ / / / | b0-B1..bN / / |\ / / | c0.. ..cN / \ / z0.. ..zN Here, the '*'s mark the first parent in merge, and uppercase B1 - the commit where the line being blamed for was removed. Since commits cN-zN do not contain B1, they still have the line removed in B1, and reverse blame can report that the last commit for the line was zN (meaning that it was removed in a100). In fact it really does return some very late commit, and this makes it unusable for finding the B1 commit. The search could be done by blame --reverse --first-parent. For range a0..a100 it would return a1, and then only one additional blame along the a0..bN will return the desired commit b0. But combining --reverse and --first-parent was forbidden in 95a4fb0eac, because incorrectly specified range could produce unexpected and meaningless result. Add test which describes the expected behavior of `blame --reverse --first-parent` in the case described above. Signed-off-by: Max Kirillov Signed-off-by: Junio C Hamano diff --git a/t/t8009-blame-vs-topicbranches.sh b/t/t8009-blame-vs-topicbranches.sh new file mode 100755 index 0000000..175ad37 --- /dev/null +++ b/t/t8009-blame-vs-topicbranches.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +test_description='blaming trough history with topic branches' +. ./test-lib.sh + +# Creates the history shown below. '*'s mark the first parent in the merges. +# The only line of file.t is changed in commit B2 +# +# +---C1 +# / \ +# A0--A1--*A2--*A3 +# \ / +# B1-B2 +# +test_expect_success setup ' + test_commit A0 file.t line0 && + test_commit A1 && + git reset --hard A0 && + test_commit B1 && + test_commit B2 file.t line0changed && + git reset --hard A1 && + test_merge A2 B2 && + git reset --hard A1 && + test_commit C1 && + git reset --hard A2 && + test_merge A3 C1 + ' + +test_expect_failure 'blame --reverse --first-parent finds A1' ' + git blame --porcelain --reverse --first-parent A0..A3 -- file.t >actual_full && + head -n 1 actual && + git rev-parse A1 >expect && + test_cmp expect actual + ' + +test_done -- cgit v0.10.2-6-g49f6 From 1b0d40000a511a8db81a20a6dab00984bdbc0d63 Mon Sep 17 00:00:00 2001 From: Max Kirillov Date: Fri, 30 Oct 2015 07:01:52 +0200 Subject: blame: extract find_single_final Signed-off-by: Max Kirillov Signed-off-by: Junio C Hamano diff --git a/builtin/blame.c b/builtin/blame.c index e024f43..7b07558 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -2396,16 +2396,11 @@ static struct commit *fake_working_tree_commit(struct diff_options *opt, return commit; } -static char *prepare_final(struct scoreboard *sb) +static struct object_array_entry *find_single_final(struct rev_info *revs) { int i; - const char *final_commit_name = NULL; - struct rev_info *revs = sb->revs; + struct object_array_entry *found = NULL; - /* - * There must be one and only one positive commit in the - * revs->pending array. - */ for (i = 0; i < revs->pending.nr; i++) { struct object *obj = revs->pending.objects[i].item; if (obj->flags & UNINTERESTING) @@ -2414,14 +2409,24 @@ static char *prepare_final(struct scoreboard *sb) obj = deref_tag(obj, NULL, 0); if (obj->type != OBJ_COMMIT) die("Non commit %s?", revs->pending.objects[i].name); - if (sb->final) + if (found) die("More than one commit to dig from %s and %s?", revs->pending.objects[i].name, - final_commit_name); - sb->final = (struct commit *) obj; - final_commit_name = revs->pending.objects[i].name; + found->name); + found = &(revs->pending.objects[i]); + } + return found; +} + +static char *prepare_final(struct scoreboard *sb) +{ + struct object_array_entry *found = find_single_final(sb->revs); + if (found) { + sb->final = (struct commit *) found->item; + return xstrdup(found->name); + } else { + return NULL; } - return xstrdup_or_null(final_commit_name); } static char *prepare_initial(struct scoreboard *sb) -- cgit v0.10.2-6-g49f6 From 700fd28e4f3ecd6e7b5f8f2098e62ffef9ab958b Mon Sep 17 00:00:00 2001 From: Max Kirillov Date: Fri, 30 Oct 2015 07:01:53 +0200 Subject: blame: allow blame --reverse --first-parent when it makes sense Allow combining --reverse and --first-parent if initial commit of specified range is at the first-parent chain starting from the final commit. Disable the prepare_revision_walk()'s builtin children collection, instead picking only the ones which are along the first parent chain. Signed-off-by: Max Kirillov Signed-off-by: Junio C Hamano diff --git a/builtin/blame.c b/builtin/blame.c index 7b07558..861a537 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -2502,6 +2502,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix) long dashdash_pos, lno; char *final_commit_name = NULL; enum object_type type; + struct commit *final_commit = NULL; static struct string_list range_list; static int output_option = 0, opt = 0; @@ -2689,11 +2690,11 @@ parse_done: } else if (contents_from) die("--contents and --children do not blend well."); - else if (revs.first_parent_only) - die("combining --first-parent and --reverse is not supported"); else { final_commit_name = prepare_initial(&sb); sb.commits.compare = compare_commits_by_reverse_commit_date; + if (revs.first_parent_only) + revs.children.name = NULL; } if (!sb.final) { @@ -2710,6 +2711,14 @@ parse_done: else if (contents_from) die("Cannot use --contents with final commit object name"); + if (reverse && revs.first_parent_only) { + struct object_array_entry *entry = find_single_final(sb.revs); + if (!entry) + die("--reverse and --first-parent together require specified latest commit"); + else + final_commit = (struct commit*) entry->item; + } + /* * If we have bottom, this will mark the ancestors of the * bottom commits we would reach while traversing as @@ -2718,6 +2727,25 @@ parse_done: if (prepare_revision_walk(&revs)) die(_("revision walk setup failed")); + if (reverse && revs.first_parent_only) { + struct commit *c = final_commit; + + sb.revs->children.name = "children"; + while (c->parents && + hashcmp(c->object.sha1, sb.final->object.sha1)) { + struct commit_list *l = xcalloc(1, sizeof(*l)); + + l->item = c; + if (add_decoration(&sb.revs->children, + &c->parents->item->object, l)) + die("BUG: not unique item in first-parent chain"); + c = c->parents->item; + } + + if (hashcmp(c->object.sha1, sb.final->object.sha1)) + die("--reverse --first-parent together require range along first-parent chain"); + } + if (is_null_sha1(sb.final->object.sha1)) { o = sb.final->util; sb.final_buf = xmemdupz(o->file.ptr, o->file.size); diff --git a/t/t8009-blame-vs-topicbranches.sh b/t/t8009-blame-vs-topicbranches.sh index 175ad37..72596e3 100755 --- a/t/t8009-blame-vs-topicbranches.sh +++ b/t/t8009-blame-vs-topicbranches.sh @@ -26,7 +26,7 @@ test_expect_success setup ' test_merge A3 C1 ' -test_expect_failure 'blame --reverse --first-parent finds A1' ' +test_expect_success 'blame --reverse --first-parent finds A1' ' git blame --porcelain --reverse --first-parent A0..A3 -- file.t >actual_full && head -n 1 actual && git rev-parse A1 >expect && -- cgit v0.10.2-6-g49f6