{"thread":{"id":"40674","subject":"[PATCH v5 2/3] blame: extract find_single_final","startedAt":"2015-10-30T05:01:50Z","lastAt":"2015-10-30T22:30:38Z","messageCount":5,"participants":["Max Kirillov","Junio C Hamano"],"isPatch":true,"patchVersion":5,"patchTotal":3},"messages":[{"id":"272517","messageId":"1446181313-15094-1-git-send-email-max@max630.net","threadId":"40674","inReplyTo":"1445837217-4252-1-git-send-email-max@max630.net","subject":"[PATCH v5 0/3] blame: allow blame --reverse --first-parent when it makes sense","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2015-10-30T05:01:50Z","receivedAt":"2015-10-30T05:01:50Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Update the test:\n* Fix style notes in tests\n* Remove the non-first-parent case, because it's more like fature request, and is not fixed in this batch\n* Rewrite the commit message, hopely now it answers better to \"why\"\n\nMax Kirillov (3):\n  blame: add test to describe use of blame --reverse --first-parent\n  blame: extract find_single_final\n  blame: allow blame --reverse --first-parent when it makes sense\n\n builtin/blame.c                   | 61 ++++++++++++++++++++++++++++++---------\n t/t8009-blame-vs-topicbranches.sh | 36 +++++++++++++++++++++++\n 2 files changed, 83 insertions(+), 14 deletions(-)\n create mode 100755 t/t8009-blame-vs-topicbranches.sh\n\n-- \n2.3.4.2801.g3d0809b\n"},{"id":"272516","messageId":"1446181313-15094-2-git-send-email-max@max630.net","threadId":"40674","inReplyTo":"1446181313-15094-1-git-send-email-max@max630.net","subject":"[PATCH v5 1/3] blame: test to describe use of blame --reverse --first-parent","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2015-10-30T05:01:51Z","receivedAt":"2015-10-30T05:01:51Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Reverse blame can be used to locate removal of lines which does not\nchange adjacent lines. Such edits do not appear in non-reverse blame,\nbecause the adjacent lines last changed commit is older history, before\nthe edit.\n\nFor a big and active project which uses topic branches, or analogous\nfeature, for example pull-requests, the history can contain many\nconcurrent branches, and even after an edit merged into the target\nbranch, there are still many (sometimes several tens or even hundreds)\ntopic branch which do not contain it:\n\n a0--a1-----*a2-*a3-a4...-*a100\n |\\         /   /         /\n | b0-B1..bN   /         /\n |\\           /         /\n | c0..   ..cN         /\n \\                    /\n  z0..            ..zN\n\nHere, the '*'s mark the first parent in merge, and uppercase B1 - the\ncommit where the line being blamed for was removed. Since commits cN-zN\ndo not contain the B1, the still have the line removed in B1, and\nreverce blame can report that the last commit for the line was zN\n(meaning that it was removed in a100). In fact it really does return\nsome very late commit, and this makes it unusable for finding the B1\ncommit.\n\nThe search could be done by blame --reverse --first-parent. For range\na0..a100 it would return a1, and then only one additional blame along\nthe a0..bN will return the desired commit b0. But combining --reverse\nand --first-parent was forbidden in 95a4fb0eac, because incorrectly\nspecified range could produce unexpected and meaningless result.\n\nAdd test which describes the expected behavior of\n`blame --reverse --first-parent` in the case described above.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n t/t8009-blame-vs-topicbranches.sh | 36 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 36 insertions(+)\n create mode 100755 t/t8009-blame-vs-topicbranches.sh\n\ndiff --git a/t/t8009-blame-vs-topicbranches.sh b/t/t8009-blame-vs-topicbranches.sh\nnew file mode 100755\nindex 0000000..175ad37\n--- /dev/null\n+++ b/t/t8009-blame-vs-topicbranches.sh\n@@ -0,0 +1,36 @@\n+#!/bin/sh\n+\n+test_description='blaming trough history with topic branches'\n+. ./test-lib.sh\n+\n+# Creates the history shown below. '*'s mark the first parent in the merges.\n+# The only line of file.t is changed in commit B2\n+#\n+#        +---C1\n+#       /      \\\n+# A0--A1--*A2--*A3\n+#   \\     /\n+#    B1-B2\n+#\n+test_expect_success setup '\n+\ttest_commit A0 file.t line0 &&\n+\ttest_commit A1 &&\n+\tgit reset --hard A0 &&\n+\ttest_commit B1 &&\n+\ttest_commit B2 file.t line0changed &&\n+\tgit reset --hard A1 &&\n+\ttest_merge A2 B2 &&\n+\tgit reset --hard A1 &&\n+\ttest_commit C1 &&\n+\tgit reset --hard A2 &&\n+\ttest_merge A3 C1\n+\t'\n+\n+test_expect_failure 'blame --reverse --first-parent finds A1' '\n+\tgit blame --porcelain --reverse --first-parent A0..A3 -- file.t >actual_full &&\n+\thead -n 1 <actual_full | sed -e \"s/ .*//\" >actual &&\n+\tgit rev-parse A1 >expect &&\n+\ttest_cmp expect actual\n+\t'\n+\n+test_done\n-- \n2.3.4.2801.g3d0809b\n"},{"id":"272515","messageId":"1446181313-15094-3-git-send-email-max@max630.net","threadId":"40674","inReplyTo":"1446181313-15094-1-git-send-email-max@max630.net","subject":"[PATCH v5 2/3] blame: extract find_single_final","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2015-10-30T05:01:52Z","receivedAt":"2015-10-30T05:01:52Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Signed-off-by: Max Kirillov <max@max630.net>\n---\n builtin/blame.c | 29 +++++++++++++++++------------\n 1 file changed, 17 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 295ce92..38f6267 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -2401,16 +2401,11 @@ static struct commit *fake_working_tree_commit(struct diff_options *opt,\n \treturn commit;\n }\n \n-static char *prepare_final(struct scoreboard *sb)\n+static struct object_array_entry *find_single_final(struct rev_info *revs)\n {\n \tint i;\n-\tconst char *final_commit_name = NULL;\n-\tstruct rev_info *revs = sb->revs;\n+\tstruct object_array_entry *found = NULL;\n \n-\t/*\n-\t * There must be one and only one positive commit in the\n-\t * revs->pending array.\n-\t */\n \tfor (i = 0; i < revs->pending.nr; i++) {\n \t\tstruct object *obj = revs->pending.objects[i].item;\n \t\tif (obj->flags & UNINTERESTING)\n@@ -2419,14 +2414,24 @@ static char *prepare_final(struct scoreboard *sb)\n \t\t\tobj = deref_tag(obj, NULL, 0);\n \t\tif (obj->type != OBJ_COMMIT)\n \t\t\tdie(\"Non commit %s?\", revs->pending.objects[i].name);\n-\t\tif (sb->final)\n+\t\tif (found)\n \t\t\tdie(\"More than one commit to dig from %s and %s?\",\n \t\t\t    revs->pending.objects[i].name,\n-\t\t\t    final_commit_name);\n-\t\tsb->final = (struct commit *) obj;\n-\t\tfinal_commit_name = revs->pending.objects[i].name;\n+\t\t\t    found->name);\n+\t\tfound = &(revs->pending.objects[i]);\n+\t}\n+\treturn found;\n+}\n+\n+static char *prepare_final(struct scoreboard *sb)\n+{\n+\tstruct object_array_entry *found = find_single_final(sb->revs);\n+\tif (found) {\n+\t\tsb->final = (struct commit *) found->item;\n+\t\treturn xstrdup(found->name);\n+\t} else {\n+\t\treturn NULL;\n \t}\n-\treturn xstrdup_or_null(final_commit_name);\n }\n \n static char *prepare_initial(struct scoreboard *sb)\n-- \n2.3.4.2801.g3d0809b\n"},{"id":"272518","messageId":"1446181313-15094-4-git-send-email-max@max630.net","threadId":"40674","inReplyTo":"1446181313-15094-1-git-send-email-max@max630.net","subject":"[PATCH v5 3/3] blame: allow blame --reverse --first-parent when it makes sense","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2015-10-30T05:01:53Z","receivedAt":"2015-10-30T05:01:53Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Allow combining --reverse and --first-parent if initial commit of\nspecified range is at the first-parent chain starting from the final\ncommit. Disable the prepare_revision_walk()'s builtin children\ncollection, instead picking only the ones which are along the first\nparent chain.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n builtin/blame.c                   | 32 ++++++++++++++++++++++++++++++--\n t/t8009-blame-vs-topicbranches.sh |  2 +-\n 2 files changed, 31 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 38f6267..98b1810 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -2507,6 +2507,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \tlong dashdash_pos, lno;\n \tchar *final_commit_name = NULL;\n \tenum object_type type;\n+\tstruct commit *final_commit = NULL;\n \n \tstatic struct string_list range_list;\n \tstatic int output_option = 0, opt = 0;\n@@ -2697,11 +2698,11 @@ parse_done:\n \t}\n \telse if (contents_from)\n \t\tdie(\"--contents and --children do not blend well.\");\n-\telse if (revs.first_parent_only)\n-\t\tdie(\"combining --first-parent and --reverse is not supported\");\n \telse {\n \t\tfinal_commit_name = prepare_initial(&sb);\n \t\tsb.commits.compare = compare_commits_by_reverse_commit_date;\n+\t\tif (revs.first_parent_only)\n+\t\t\trevs.children.name = NULL;\n \t}\n \n \tif (!sb.final) {\n@@ -2718,6 +2719,14 @@ parse_done:\n \telse if (contents_from)\n \t\tdie(\"Cannot use --contents with final commit object name\");\n \n+\tif (reverse && revs.first_parent_only) {\n+\t\tstruct object_array_entry *entry = find_single_final(sb.revs);\n+\t\tif (!entry)\n+\t\t\tdie(\"--reverse and --first-parent together require specified latest commit\");\n+\t\telse\n+\t\t\tfinal_commit = (struct commit*) entry->item;\n+\t}\n+\n \t/*\n \t * If we have bottom, this will mark the ancestors of the\n \t * bottom commits we would reach while traversing as\n@@ -2726,6 +2735,25 @@ parse_done:\n \tif (prepare_revision_walk(&revs))\n \t\tdie(_(\"revision walk setup failed\"));\n \n+\tif (reverse && revs.first_parent_only) {\n+\t\tstruct commit *c = final_commit;\n+\n+\t\tsb.revs->children.name = \"children\";\n+\t\twhile (c->parents &&\n+\t\t       hashcmp(c->object.sha1, sb.final->object.sha1)) {\n+\t\t\tstruct commit_list *l = xcalloc(1, sizeof(*l));\n+\n+\t\t\tl->item = c;\n+\t\t\tif (add_decoration(&sb.revs->children,\n+\t\t\t\t\t   &c->parents->item->object, l))\n+\t\t\t\tdie(\"BUG: not unique item in first-parent chain\");\n+\t\t\tc = c->parents->item;\n+\t\t}\n+\n+\t\tif (hashcmp(c->object.sha1, sb.final->object.sha1))\n+\t\t\tdie(\"--reverse --first-parent together require range along first-parent chain\");\n+\t}\n+\n \tif (is_null_sha1(sb.final->object.sha1)) {\n \t\to = sb.final->util;\n \t\tsb.final_buf = xmemdupz(o->file.ptr, o->file.size);\ndiff --git a/t/t8009-blame-vs-topicbranches.sh b/t/t8009-blame-vs-topicbranches.sh\nindex 175ad37..72596e3 100755\n--- a/t/t8009-blame-vs-topicbranches.sh\n+++ b/t/t8009-blame-vs-topicbranches.sh\n@@ -26,7 +26,7 @@ test_expect_success setup '\n \ttest_merge A3 C1\n \t'\n \n-test_expect_failure 'blame --reverse --first-parent finds A1' '\n+test_expect_success 'blame --reverse --first-parent finds A1' '\n \tgit blame --porcelain --reverse --first-parent A0..A3 -- file.t >actual_full &&\n \thead -n 1 <actual_full | sed -e \"s/ .*//\" >actual &&\n \tgit rev-parse A1 >expect &&\n-- \n2.3.4.2801.g3d0809b\n"},{"id":"272592","messageId":"xmqq611oj8fl.fsf@gitster.mtv.corp.google.com","threadId":"40674","inReplyTo":"1446181313-15094-2-git-send-email-max@max630.net","subject":"Re: [PATCH v5 1/3] blame: test to describe use of blame --reverse --first-parent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2015-10-30T22:30:38Z","receivedAt":"2015-10-30T22:30:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Max Kirillov <max@max630.net> writes:\n\n> Reverse blame can be used to locate removal of lines which does not\n> change adjacent lines. Such edits do not appear in non-reverse blame,\n> because the adjacent lines last changed commit is older history, before\n> the edit.\n>\n> For a big and active project which uses topic branches, or analogous\n> feature, for example pull-requests, the history can contain many\n> concurrent branches, and even after an edit merged into the target\n> branch, there are still many (sometimes several tens or even hundreds)\n> topic branch which do not contain it:\n>\n>  a0--a1-----*a2-*a3-a4...-*a100\n>  |\\         /   /         /\n>  | b0-B1..bN   /         /\n>  |\\           /         /\n>  | c0..   ..cN         /\n>  \\                    /\n>   z0..            ..zN\n>\n> Here, the '*'s mark the first parent in merge, and uppercase B1 - the\n> commit where the line being blamed for was removed. Since commits cN-zN\n> do not contain the B1, the still have the line removed in B1, and\n> reverce blame can report that the last commit for the line was zN\n> (meaning that it was removed in a100). In fact it really does return\n> some very late commit, and this makes it unusable for finding the B1\n> commit.\n>\n> The search could be done by blame --reverse --first-parent. For range\n> a0..a100 it would return a1, and then only one additional blame along\n> the a0..bN will return the desired commit b0. But combining --reverse\n> and --first-parent was forbidden in 95a4fb0eac, because incorrectly\n> specified range could produce unexpected and meaningless result.\n>\n> Add test which describes the expected behavior of\n> `blame --reverse --first-parent` in the case described above.\n>\n> Signed-off-by: Max Kirillov <max@max630.net>\n> ---\n\nThere were a few obvious typos I spotted but other than that a very\nunderstandable description.  Will queue.\n\nThanks.\n\n>  t/t8009-blame-vs-topicbranches.sh | 36 ++++++++++++++++++++++++++++++++++++\n>  1 file changed, 36 insertions(+)\n>  create mode 100755 t/t8009-blame-vs-topicbranches.sh\n>\n> diff --git a/t/t8009-blame-vs-topicbranches.sh b/t/t8009-blame-vs-topicbranches.sh\n> new file mode 100755\n> index 0000000..175ad37\n> --- /dev/null\n> +++ b/t/t8009-blame-vs-topicbranches.sh\n> @@ -0,0 +1,36 @@\n> +#!/bin/sh\n> +\n> +test_description='blaming trough history with topic branches'\n> +. ./test-lib.sh\n> +\n> +# Creates the history shown below. '*'s mark the first parent in the merges.\n> +# The only line of file.t is changed in commit B2\n> +#\n> +#        +---C1\n> +#       /      \\\n> +# A0--A1--*A2--*A3\n> +#   \\     /\n> +#    B1-B2\n> +#\n> +test_expect_success setup '\n> +\ttest_commit A0 file.t line0 &&\n> +\ttest_commit A1 &&\n> +\tgit reset --hard A0 &&\n> +\ttest_commit B1 &&\n> +\ttest_commit B2 file.t line0changed &&\n> +\tgit reset --hard A1 &&\n> +\ttest_merge A2 B2 &&\n> +\tgit reset --hard A1 &&\n> +\ttest_commit C1 &&\n> +\tgit reset --hard A2 &&\n> +\ttest_merge A3 C1\n> +\t'\n> +\n> +test_expect_failure 'blame --reverse --first-parent finds A1' '\n> +\tgit blame --porcelain --reverse --first-parent A0..A3 -- file.t >actual_full &&\n> +\thead -n 1 <actual_full | sed -e \"s/ .*//\" >actual &&\n> +\tgit rev-parse A1 >expect &&\n> +\ttest_cmp expect actual\n> +\t'\n> +\n> +test_done\n"}]}