{"thread":{"id":"3133","subject":"[RFC] 1st pass at git-annotate (not-quite functional, however)","startedAt":"2006-01-23T08:05:33Z","lastAt":"2006-01-23T08:24:32Z","messageCount":2,"participants":["Ryan Anderson","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"15065","messageId":"20060123080532.GD19212@mythryan2.michonline.com","threadId":"3133","inReplyTo":null,"subject":"[RFC] 1st pass at git-annotate (not-quite functional, however)","fromName":"Ryan Anderson","fromEmail":"ryan@michonline.com","sentAt":"2006-01-23T08:05:33Z","receivedAt":"2006-01-23T08:05:33Z","isPatch":false,"sender":{"key":"ryan@michonline.com","avatar":null},"body":"\nHere's my stab at getting git-annotate working.  This requires the\n--remove-empty addition to git-rev-list Linus sent out a few days ago.\n\nThis code is not complete, but on my test case (git-format-mbox.sh) it\ndoes track through renames, and does figure out authorship of about half\nthe lines (rough guess).\n\nI have learned that parsing an arbritrary length series of diffs is\nhard, mostly because trying to infer line numbers from them is tricky\n(especially when you contemplate tracking state down multiple merges\nat once.)\n\nI currently have a very naive approach of tracking offsets in here,\nbuilt as the diff is parsed.  I know it's wrong, but it's not too wrong,\nif you ignore merges.\n\nI haven't figured out exactly how I want to handle annotating across\nmerges, but I think the general idea I have here will work, it just\nneeds to be ripped out of global variables and stuck into some\nstructures that get passed around/split/merged up/etc.\n\nI'm posting this tonight because I won't have time to look at this again\nfor a few days (probably the weekend), and someone else might see a way\nto make this work a bit easier.\n\nWithout further ado, git-annotate:\n\n\n Makefile          |    2 \n git-annotate.perl |  309 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 310 insertions(+), 1 deletions(-)\n create mode 100755 git-annotate.perl\n\ndf7b8b7bf682411485aefa31c6dc24a4b8adf316\ndiff --git a/Makefile b/Makefile\nindex 6517204..505b353 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -118,7 +118,7 @@ SCRIPT_PERL = \\\n \tgit-archimport.perl git-cvsimport.perl git-relink.perl \\\n \tgit-shortlog.perl git-fmt-merge-msg.perl \\\n \tgit-svnimport.perl git-mv.perl git-cvsexportcommit.perl \\\n-\tgit-graft-ripple.perl\n+\tgit-graft-ripple.perl git-annotate.perl\n \n SCRIPT_PYTHON = \\\n \tgit-merge-recursive.py\ndiff --git a/git-annotate.perl b/git-annotate.perl\nnew file mode 100755\nindex 0000000..044828f\n--- /dev/null\n+++ b/git-annotate.perl\n@@ -0,0 +1,309 @@\n+#!/usr/bin/perl\n+# Copyright 2006, Ryan Anderson <ryan@michonline.com>\n+#\n+# GPL v2 (See COPYING)\n+#\n+# This file is licensed under the GPL v2, or a later version\n+# at the discretion of Linus Torvalds.\n+\n+use warnings;\n+use strict;\n+\n+use Data::Dumper;\n+\n+my $filename = shift @ARGV;\n+\n+\n+my @stack = (\n+\t{\n+\t\t'rev' => \"HEAD\",\n+\t\t'filename' => $filename,\n+\t},\n+);\n+\n+our (@lineoffsets, @pendinglineoffsets);\n+my @filelines = ();\n+open(F,\"<\",$filename)\n+\tor die \"Failed to open filename: $!\";\n+\n+while(<F>) {\n+\tchomp;\n+\tpush @filelines, $_;\n+}\n+close(F);\n+\n+my $revsprocessed = 0;\n+W: while (my $bound = pop @stack) {\n+\tmy @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});\n+\tforeach my $revinst (@revisions) {\n+\t\tmy ($rev, @parents) = @$revinst;\n+\t\t#last W if $revsprocessed++ > 3;\n+\n+\t\tif (scalar @parents > 0) {\n+\t\t\tgit_diff_parse($parents[0], $rev, $bound->{'filename'});\n+\t\t\tnext;\n+\t\t}\n+\n+\t\tprintf(\"Boundary = %s\\n\", $rev);\n+\t\tmy $newbound = find_parent_renames($rev, $bound->{'filename'});\n+\t\tif ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {\n+\t\t\tpush @stack, $newbound;\n+\t\t}\n+\t}\n+}\n+\n+my $i = 0;\n+foreach my $l (@filelines) {\n+\tmy ($output, $rev, $committer, $date);\n+\tif (ref $l eq 'ARRAY') {\n+\t\t($output, $rev, $committer, $date) = @$l;\n+\t\tif (length($rev) > 8) {\n+\t\t\t$rev = substr($rev,0,8);\n+\t\t}\n+\t} else {\n+\t\t$output = $l;\n+\t\t($rev, $committer, $date) = ('unknonw', 'unknown', 'unknown');\n+\t}\n+\n+\tprintf(\"(%8s %10s %10s %d)%s\\n\", $rev, $committer, $date, $i++, $output);\n+}\n+\n+#print Data::Dumper->Dump([\\@lineoffsets],[qw(*lineoffsets)]);\n+\n+\n+sub git_rev_list {\n+\tmy ($rev, $file) = @_;\n+\t#printf(\"grl = %s, %s\\n\", $rev, $file);\n+\n+\topen(P,\"-|\",\"git-rev-list\",\"--parents\",\"--remove-empty\",$rev,\"--\",$file)\n+\t\tor die \"Failed to exec git-rev-list: $!\";\n+\n+\tmy @revs;\n+\twhile(my $line = <P>) {\n+\t\tchomp $line;\n+\t\tmy ($rev, @parents) = split /\\s+/, $line;\n+\t\tpush @revs, [ $rev, @parents ];\n+\t}\n+\tclose(P);\n+\n+\treturn @revs;\n+}\n+\n+sub find_parent_renames {\n+\tmy ($rev, $file) = @_;\n+\n+\topen(P,\"-|\",\"git-diff\", \"-r\",\"--name-status\", \"-z\",\"$rev^1..$rev\")\n+\t\tor die \"Failed to exec git-diff: $!\";\n+\n+\tlocal $/ = \"\\0\";\n+\tmy %bound;\n+\twhile (my $change = <P>) {\n+\t\tchomp $change;\n+\t\tmy $filename = <P>;\n+\t\tchomp $filename;\n+\n+\t\tif ($change =~ m/^[AMD]$/ ) {\n+\t\t\tnext;\n+\t\t} elsif ($change =~ m/^R/ ) {\n+\t\t\tmy $oldfilename = $filename;\n+\t\t\t$filename = <P>;\n+\t\t\tchomp $filename;\n+\t\t\tif ( $file eq $filename ) {\n+\t\t\t\tmy $parent = git_find_parent($rev);\n+\t\t\t\t#printf(\"Found rename at boundary: %s-%s, %s\\n\", $rev, $parent, $oldfilename);\n+\t\t\t\t@bound{'rev','filename'} = ($parent, $oldfilename);\n+\n+\t\t\t\tlast;\n+\t\t\t} else {\n+\t\t\t\t#printf(\"Found unknown rename of %s => %s\\n\", $oldfilename, $filename);\n+\t\t\t}\n+\t\t} else {\n+\t\t\t#printf(\"Unknown name-status type of '%s'\\n\", $change);\n+\t\t}\n+\n+\n+\n+\t}\n+\tclose(P);\n+\n+\treturn \\%bound;\n+}\n+\n+\n+sub git_find_parent {\n+\tmy ($rev) = @_;\n+\n+\topen(REVPARENT,\"-|\",\"git-rev-list\",\"--parents\",\"$rev^1..$rev\")\n+\t\tor die \"Failed to open git-rev-list to find a single parent: $!\";\n+\n+\tmy $parentline = <REVPARENT>;\n+\tchomp $parentline;\n+\tmy ($revfound,$parent) = split m/\\s+/, $parentline;\n+\n+\tclose(REVPARENT);\n+\n+\treturn $parent;\n+}\n+\n+\n+# Get a diff between the current revision and a parent.\n+# Record the commit information that results.\n+sub git_diff_parse {\n+\tmy ($parent, $rev, $filename) = @_;\n+\n+\tmy %revinfo = git_commit_info($rev);\n+\n+\t# To make this slightly simpler, we're going to do things backwards.\n+\t# git-diff $rev..$parent gets us line numbers that pertain to $rev, not\n+\t# $parent, and we can work with those easier than the reverse.\n+\topen(DIFF,\"-|\",\"git-diff\",\"$rev..$parent\",$filename)\n+\t\tor die \"Failed to call git-diff for annotation: $!\";\n+\n+\tmy ($gotheader) = (0);\n+\tmy ($remstart, $remlength, $addstart, $addlength);\n+\tmy ($hunk_actual_start, $hunk_index, $hunk_adds);\n+\twhile(<DIFF>) {\n+\t\tprint;\n+\t\tif (m/^@@ -(\\d+),(\\d+) \\+(\\d+),(\\d+)/) {\n+\t\t\tif (0 && $gotheader) {\n+\t\t\t\tprint \"Filelines had:\\n\";\n+\t\t\t\tfor (my $i = $remstart ; $i < $remstart + $remlength; $i++) {\n+\t\t\t\t\tprint $filelines[$i];\n+\n+\t\t\t\t}\n+\t\t\t\tprint \"===================================\\n\";\n+\t\t\t}\n+\t\t\t($remstart, $remlength, $addstart, $addlength) = ($1, $2, $3, $4);\n+\t\t\t# Adjust for 0-based arrays\n+\t\t\t$remstart--;\n+\t\t\t$addstart--;\n+\n+\t\t\t# Record (but don't put into use right away) these offsets.\n+\t\t\tinsert_into_lineoffsets( $remstart + $remlength, $remlength - $addlength );\n+\t\t\t$hunk_index = 0;\n+\t\t\t$hunk_adds = 0;\n+\t\t\t$hunk_actual_start = find_actual_start($remstart);\n+\t\t\t$gotheader = 1;\n+\t\t\tnext;\n+\t\t} elsif (!$gotheader) {\n+\t\t\tnext;\n+\t\t}\n+\n+\t\tif (m/^\\+/) {\n+\t\t\tnext;\n+\n+\t\t} elsif (m/^-(.*)$/) {\n+\t\t\tmy $line = $1;\n+\t\t\tmy $floffset = $hunk_actual_start + $hunk_index;\n+\n+\t\t\tprintf(\"\\tline = %s\\n\\tfilelines entry = %s*\\thas = %d, hi = %d\\n\",\n+\t\t\t\t$line, $filelines[$floffset],\n+\t\t\t\t$hunk_actual_start, $hunk_index) if 0;\n+\n+\t\t\tif (ref $filelines[$floffset] eq '' && $filelines[$floffset] eq $line) {\n+\t\t\t\tmy $oline = $filelines[$floffset];\n+\t\t\t\t$filelines[$floffset] =\n+\t\t\t\t\t[ $oline, $rev, $revinfo{'committer'}, $revinfo{'committer_date'} ];\n+\t\t\t}\n+\t\t}\n+\n+\t\t$hunk_index++;\n+\t}\n+\tclose(DIFF);\n+\n+\t# Store these offsets for use when processing the next diff.\n+\tsync_line_offsets();\n+}\n+\n+sub insert_into_lineoffsets {\n+\tmy ($start, $offset) = @_;\n+\n+\tpush @pendinglineoffsets, [$start, $offset];\n+\n+}\n+\n+sub sync_line_offsets {\n+\tcheck_line_offsets(0);\n+\t#print Data::Dumper->Dump([\\@pendinglineoffsets],[qw(*pending)]);\n+\tforeach my $o (@pendinglineoffsets) {\n+\t\t_insert_into_lineoffsets($$o[0],$$o[1]);\n+\t}\n+\t@pendinglineoffsets = ();\n+\n+\t#print Data::Dumper->Dump([\\@lineoffsets], [qw(*lineoffsets)]);\n+\tcheck_line_offsets(1);\n+\n+\tif (defined $lineoffsets[0] && !defined $lineoffsets[0][0]) {\n+\t\tshift @lineoffsets;\n+\t}\n+}\n+\n+sub check_line_offsets {\n+\tmy $v = shift;\n+\tfor(my $i = 0; $i < @lineoffsets; $i++) {\n+\t\twarn \"lineoffsets[$i] undef at $v\" if (!defined $lineoffsets[$i]);\n+\t\twarn \"lineoffsets[$i][0] undef at $v\" if (!defined $lineoffsets[$i][0]);\n+\t}\n+}\n+\n+my $calls_to_lineoffsets = 1;\n+sub _insert_into_lineoffsets {\n+\tmy ($start, $offset) = @_;\n+\t#warn sprintf(\"lineoffsets has %d elements (%d)\", scalar @lineoffsets, $calls_to_lineoffsets++);\n+\n+\tif (@lineoffsets == 0) {\n+\t\tpush @lineoffsets, [ $start, $offset ];\n+\t\treturn;\n+\t}\n+\n+\t# replace this with a binary search later ## FIXME ##\n+\tmy $i = 0;\n+\twhile ($i < @lineoffsets && $start < $lineoffsets[$i][0]) {\n+\t\t$i++;\n+\t}\n+\n+\tif ($i < @lineoffsets && $start == $lineoffsets[$i][0]) {\n+\t\t$lineoffsets[$i][1] += $offset;\n+\t} else {\n+\t\tpush @lineoffsets, [ $start, $offset ];\n+\t\t@lineoffsets = sort { $a->[0] <=> $b->[0] } @lineoffsets;\n+\t}\n+}\n+\n+sub find_actual_start {\n+\tmy ($start) = @_;\n+\tmy $offset = 0;\n+\n+\tfor(my $i = 0; $i < @lineoffsets && $start < $lineoffsets[$i][0] ; $i++) {\n+\t\t$offset += $lineoffsets[$i][1];\n+\t}\n+\n+\tprintf(\"hunk_actual_start = %d (%d + %d)\\n\", $start + $offset, $start, $offset);\n+\treturn $start + $offset;\n+}\n+\n+\n+sub git_commit_info {\n+\tmy ($rev) = @_;\n+\topen(COMMIT, \"-|\",\"git-cat-file\", \"commit\", $rev)\n+\t\tor die \"Failed to call git-cat-file: $!\";\n+\n+\tmy %info;\n+\twhile(<COMMIT>) {\n+\t\tchomp;\n+\t\tlast if (length $_ == 0);\n+\n+\t\tif (m/^author (.*) <(.*)> (.*)$/) {\n+\t\t\t$info{'author'} = $1;\n+\t\t\t$info{'author_email'} = $2;\n+\t\t\t$info{'author_date'} = $3;\n+\t\t} elsif (m/^committer (.*) <(.*)> (.*)$/) {\n+\t\t\t$info{'committer'} = $1;\n+\t\t\t$info{'committer_email'} = $2;\n+\t\t\t$info{'committer_date'} = $3;\n+\t\t}\n+\t}\n+\tclose(COMMIT);\n+\n+\treturn %info;\n+}\n-- \n1.1.4.g31e9b\n\n\n-- \n\nRyan Anderson\n  sometimes Pug Majere\n"},{"id":"15066","messageId":"7vslrfz70f.fsf@assigned-by-dhcp.cox.net","threadId":"3133","inReplyTo":"20060123080532.GD19212@mythryan2.michonline.com","subject":"Re: [RFC] 1st pass at git-annotate (not-quite functional, however)","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2006-01-23T08:24:32Z","receivedAt":"2006-01-23T08:24:32Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ryan Anderson <ryan@michonline.com> writes:\n\n> I haven't figured out exactly how I want to handle annotating across\n> merges,...\n\nYou might want to take a look at the \"passing blames around\"\nalgorithm I did for \"git blame\" loooooooooooooooong time ago.\n\n    http://article.gmane.org/gmane.comp.version-control.git/5483\n"}]}