git-annotate.perlon commit git-format-patch --start-number <n> (fa0f02d)
   1#!/usr/bin/perl
   2# Copyright 2006, Ryan Anderson <ryan@michonline.com>
   3#
   4# GPL v2 (See COPYING)
   5#
   6# This file is licensed under the GPL v2, or a later version
   7# at the discretion of Linus Torvalds.
   8
   9use warnings;
  10use strict;
  11use Getopt::Long;
  12use POSIX qw(strftime gmtime);
  13use File::Basename qw(basename dirname);
  14
  15sub usage() {
  16        print STDERR "Usage: ${\basename $0} [-s] [-S revs-file] file [ revision ]
  17        -l, --long
  18                        Show long rev (Defaults off)
  19        -t, --time
  20                        Show raw timestamp (Defaults off)
  21        -r, --rename
  22                        Follow renames (Defaults on).
  23        -S, --rev-file revs-file
  24                        Use revs from revs-file instead of calling git-rev-list
  25        -h, --help
  26                        This message.
  27";
  28
  29        exit(1);
  30}
  31
  32our ($help, $longrev, $rename, $rawtime, $starting_rev, $rev_file) = (0, 0, 1);
  33
  34my $rc = GetOptions(    "long|l" => \$longrev,
  35                        "time|t" => \$rawtime,
  36                        "help|h" => \$help,
  37                        "rename|r" => \$rename,
  38                        "rev-file|S=s" => \$rev_file);
  39if (!$rc or $help or !@ARGV) {
  40        usage();
  41}
  42
  43my $filename = shift @ARGV;
  44if (@ARGV) {
  45        $starting_rev = shift @ARGV;
  46}
  47
  48my @stack = (
  49        {
  50                'rev' => defined $starting_rev ? $starting_rev : "HEAD",
  51                'filename' => $filename,
  52        },
  53);
  54
  55our @filelines = ();
  56
  57if (defined $starting_rev) {
  58        @filelines = git_cat_file($starting_rev, $filename);
  59} else {
  60        open(F,"<",$filename)
  61                or die "Failed to open filename: $!";
  62
  63        while(<F>) {
  64                chomp;
  65                push @filelines, $_;
  66        }
  67        close(F);
  68
  69}
  70
  71our %revs;
  72our @revqueue;
  73our $head;
  74
  75my $revsprocessed = 0;
  76while (my $bound = pop @stack) {
  77        my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});
  78        foreach my $revinst (@revisions) {
  79                my ($rev, @parents) = @$revinst;
  80                $head ||= $rev;
  81
  82                if (!defined($rev)) {
  83                        $rev = "";
  84                }
  85                $revs{$rev}{'filename'} = $bound->{'filename'};
  86                if (scalar @parents > 0) {
  87                        $revs{$rev}{'parents'} = \@parents;
  88                        next;
  89                }
  90
  91                if (!$rename) {
  92                        next;
  93                }
  94
  95                my $newbound = find_parent_renames($rev, $bound->{'filename'});
  96                if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {
  97                        push @stack, $newbound;
  98                        $revs{$rev}{'parents'} = [$newbound->{'rev'}];
  99                }
 100        }
 101}
 102push @revqueue, $head;
 103init_claim( defined $starting_rev ? $head : 'dirty');
 104unless (defined $starting_rev) {
 105        my $diff = open_pipe("git","diff","-R", "HEAD", "--",$filename)
 106                or die "Failed to call git diff to check for dirty state: $!";
 107
 108        _git_diff_parse($diff, $head, "dirty", (
 109                                'author' => gitvar_name("GIT_AUTHOR_IDENT"),
 110                                'author_date' => sprintf("%s +0000",time()),
 111                                )
 112                        );
 113        close($diff);
 114}
 115handle_rev();
 116
 117
 118my $i = 0;
 119foreach my $l (@filelines) {
 120        my ($output, $rev, $committer, $date);
 121        if (ref $l eq 'ARRAY') {
 122                ($output, $rev, $committer, $date) = @$l;
 123                if (!$longrev && length($rev) > 8) {
 124                        $rev = substr($rev,0,8);
 125                }
 126        } else {
 127                $output = $l;
 128                ($rev, $committer, $date) = ('unknown', 'unknown', 'unknown');
 129        }
 130
 131        printf("%s\t(%10s\t%10s\t%d)%s\n", $rev, $committer,
 132                format_date($date), ++$i, $output);
 133}
 134
 135sub init_claim {
 136        my ($rev) = @_;
 137        for (my $i = 0; $i < @filelines; $i++) {
 138                $filelines[$i] = [ $filelines[$i], '', '', '', 1];
 139                        # line,
 140                        # rev,
 141                        # author,
 142                        # date,
 143                        # 1 <-- belongs to the original file.
 144        }
 145        $revs{$rev}{'lines'} = \@filelines;
 146}
 147
 148
 149sub handle_rev {
 150        my $i = 0;
 151        my %seen;
 152        while (my $rev = shift @revqueue) {
 153                next if $seen{$rev}++;
 154
 155                my %revinfo = git_commit_info($rev);
 156
 157                foreach my $p (@{$revs{$rev}{'parents'}}) {
 158
 159                        git_diff_parse($p, $rev, %revinfo);
 160                        push @revqueue, $p;
 161                }
 162
 163
 164                if (scalar @{$revs{$rev}{parents}} == 0) {
 165                        # We must be at the initial rev here, so claim everything that is left.
 166                        for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) {
 167                                if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') {
 168                                        claim_line($i, $rev, $revs{$rev}{lines}, %revinfo);
 169                                }
 170                        }
 171                }
 172        }
 173}
 174
 175
 176sub git_rev_list {
 177        my ($rev, $file) = @_;
 178
 179        my $revlist;
 180        if ($rev_file) {
 181                open($revlist, '<' . $rev_file)
 182                    or die "Failed to open $rev_file : $!";
 183        } else {
 184                $revlist = open_pipe("git-rev-list","--parents","--remove-empty",$rev,"--",$file)
 185                        or die "Failed to exec git-rev-list: $!";
 186        }
 187
 188        my @revs;
 189        while(my $line = <$revlist>) {
 190                chomp $line;
 191                my ($rev, @parents) = split /\s+/, $line;
 192                push @revs, [ $rev, @parents ];
 193        }
 194        close($revlist);
 195
 196        printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0);
 197        return @revs;
 198}
 199
 200sub find_parent_renames {
 201        my ($rev, $file) = @_;
 202
 203        my $patch = open_pipe("git-diff-tree", "-M50", "-r","--name-status", "-z","$rev")
 204                or die "Failed to exec git-diff: $!";
 205
 206        local $/ = "\0";
 207        my %bound;
 208        my $junk = <$patch>;
 209        while (my $change = <$patch>) {
 210                chomp $change;
 211                my $filename = <$patch>;
 212                if (!defined $filename) {
 213                        next;
 214                }
 215                chomp $filename;
 216
 217                if ($change =~ m/^[AMD]$/ ) {
 218                        next;
 219                } elsif ($change =~ m/^R/ ) {
 220                        my $oldfilename = $filename;
 221                        $filename = <$patch>;
 222                        chomp $filename;
 223                        if ( $file eq $filename ) {
 224                                my $parent = git_find_parent($rev, $oldfilename);
 225                                @bound{'rev','filename'} = ($parent, $oldfilename);
 226                                last;
 227                        }
 228                }
 229        }
 230        close($patch);
 231
 232        return \%bound;
 233}
 234
 235
 236sub git_find_parent {
 237        my ($rev, $filename) = @_;
 238
 239        my $revparent = open_pipe("git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename)
 240                or die "Failed to open git-rev-list to find a single parent: $!";
 241
 242        my $parentline = <$revparent>;
 243        chomp $parentline;
 244        my ($revfound,$parent) = split m/\s+/, $parentline;
 245
 246        close($revparent);
 247
 248        return $parent;
 249}
 250
 251
 252# Get a diff between the current revision and a parent.
 253# Record the commit information that results.
 254sub git_diff_parse {
 255        my ($parent, $rev, %revinfo) = @_;
 256
 257        my $diff = open_pipe("git-diff-tree","-M","-p",$rev,$parent,"--",
 258                        $revs{$rev}{'filename'}, $revs{$parent}{'filename'})
 259                or die "Failed to call git-diff for annotation: $!";
 260
 261        _git_diff_parse($diff, $parent, $rev, %revinfo);
 262
 263        close($diff);
 264}
 265
 266sub _git_diff_parse {
 267        my ($diff, $parent, $rev, %revinfo) = @_;
 268
 269        my ($ri, $pi) = (0,0);
 270        my $slines = $revs{$rev}{'lines'};
 271        my @plines;
 272
 273        my $gotheader = 0;
 274        my ($remstart);
 275        my ($hunk_start, $hunk_index);
 276        while(<$diff>) {
 277                chomp;
 278                if (m/^@@ -(\d+),(\d+) \+(\d+),(\d+)/) {
 279                        $remstart = $1;
 280                        # Adjust for 0-based arrays
 281                        $remstart--;
 282                        # Reinit hunk tracking.
 283                        $hunk_start = $remstart;
 284                        $hunk_index = 0;
 285                        $gotheader = 1;
 286
 287                        for (my $i = $ri; $i < $remstart; $i++) {
 288                                $plines[$pi++] = $slines->[$i];
 289                                $ri++;
 290                        }
 291                        next;
 292                } elsif (!$gotheader) {
 293                        next;
 294                }
 295
 296                if (m/^\+(.*)$/) {
 297                        my $line = $1;
 298                        $plines[$pi++] = [ $line, '', '', '', 0 ];
 299                        next;
 300
 301                } elsif (m/^-(.*)$/) {
 302                        my $line = $1;
 303                        if (get_line($slines, $ri) eq $line) {
 304                                # Found a match, claim
 305                                claim_line($ri, $rev, $slines, %revinfo);
 306                        } else {
 307                                die sprintf("Sync error: %d/%d\n|%s\n|%s\n%s => %s\n",
 308                                                $ri, $hunk_start + $hunk_index,
 309                                                $line,
 310                                                get_line($slines, $ri),
 311                                                $rev, $parent);
 312                        }
 313                        $ri++;
 314
 315                } elsif (m/^\\/) {
 316                        ;
 317                        # Skip \No newline at end of file.
 318                        # But this can be internationalized, so only look
 319                        # for an initial \
 320
 321                } else {
 322                        if (substr($_,1) ne get_line($slines,$ri) ) {
 323                                die sprintf("Line %d (%d) does not match:\n|%s\n|%s\n%s => %s\n",
 324                                                $hunk_start + $hunk_index, $ri,
 325                                                substr($_,1),
 326                                                get_line($slines,$ri),
 327                                                $rev, $parent);
 328                        }
 329                        $plines[$pi++] = $slines->[$ri++];
 330                }
 331                $hunk_index++;
 332        }
 333        for (my $i = $ri; $i < @{$slines} ; $i++) {
 334                push @plines, $slines->[$ri++];
 335        }
 336
 337        $revs{$parent}{lines} = \@plines;
 338        return;
 339}
 340
 341sub get_line {
 342        my ($lines, $index) = @_;
 343
 344        return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
 345}
 346
 347sub git_cat_file {
 348        my ($rev, $filename) = @_;
 349        return () unless defined $rev && defined $filename;
 350
 351        my $blob = git_ls_tree($rev, $filename);
 352        die "Failed to find a blob for $filename in rev $rev\n" if !defined $blob;
 353
 354        my $catfile = open_pipe("git","cat-file", "blob", $blob)
 355                or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!;
 356
 357        my @lines;
 358        while(<$catfile>) {
 359                chomp;
 360                push @lines, $_;
 361        }
 362        close($catfile);
 363
 364        return @lines;
 365}
 366
 367sub git_ls_tree {
 368        my ($rev, $filename) = @_;
 369
 370        my $lstree = open_pipe("git","ls-tree",$rev,$filename)
 371                or die "Failed to call git ls-tree: $!";
 372
 373        my ($mode, $type, $blob, $tfilename);
 374        while(<$lstree>) {
 375                chomp;
 376                ($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4);
 377                last if ($tfilename eq $filename);
 378        }
 379        close($lstree);
 380
 381        return $blob if ($tfilename eq $filename);
 382        die "git-ls-tree failed to find blob for $filename";
 383
 384}
 385
 386
 387
 388sub claim_line {
 389        my ($floffset, $rev, $lines, %revinfo) = @_;
 390        my $oline = get_line($lines, $floffset);
 391        @{$lines->[$floffset]} = ( $oline, $rev,
 392                $revinfo{'author'}, $revinfo{'author_date'} );
 393        #printf("Claiming line %d with rev %s: '%s'\n",
 394        #               $floffset, $rev, $oline) if 1;
 395}
 396
 397sub git_commit_info {
 398        my ($rev) = @_;
 399        my $commit = open_pipe("git-cat-file", "commit", $rev)
 400                or die "Failed to call git-cat-file: $!";
 401
 402        my %info;
 403        while(<$commit>) {
 404                chomp;
 405                last if (length $_ == 0);
 406
 407                if (m/^author (.*) <(.*)> (.*)$/) {
 408                        $info{'author'} = $1;
 409                        $info{'author_email'} = $2;
 410                        $info{'author_date'} = $3;
 411                } elsif (m/^committer (.*) <(.*)> (.*)$/) {
 412                        $info{'committer'} = $1;
 413                        $info{'committer_email'} = $2;
 414                        $info{'committer_date'} = $3;
 415                }
 416        }
 417        close($commit);
 418
 419        return %info;
 420}
 421
 422sub format_date {
 423        if ($rawtime) {
 424                return $_[0];
 425        }
 426        my ($timestamp, $timezone) = split(' ', $_[0]);
 427        my $minutes = abs($timezone);
 428        $minutes = int($minutes / 100) * 60 + ($minutes % 100);
 429        if ($timezone < 0) {
 430            $minutes = -$minutes;
 431        }
 432        my $t = $timestamp + $minutes * 60;
 433        return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($t));
 434}
 435
 436# Copied from git-send-email.perl - We need a Git.pm module..
 437sub gitvar {
 438    my ($var) = @_;
 439    my $fh;
 440    my $pid = open($fh, '-|');
 441    die "$!" unless defined $pid;
 442    if (!$pid) {
 443        exec('git-var', $var) or die "$!";
 444    }
 445    my ($val) = <$fh>;
 446    close $fh or die "$!";
 447    chomp($val);
 448    return $val;
 449}
 450
 451sub gitvar_name {
 452    my ($name) = @_;
 453    my $val = gitvar($name);
 454    my @field = split(/\s+/, $val);
 455    return join(' ', @field[0...(@field-4)]);
 456}
 457
 458sub open_pipe {
 459        if ($^O eq '##INSERT_ACTIVESTATE_STRING_HERE##') {
 460                return open_pipe_activestate(@_);
 461        } else {
 462                return open_pipe_normal(@_);
 463        }
 464}
 465
 466sub open_pipe_activestate {
 467        tie *fh, "Git::ActiveStatePipe", @_;
 468        return *fh;
 469}
 470
 471sub open_pipe_normal {
 472        my (@execlist) = @_;
 473
 474        my $pid = open my $kid, "-|";
 475        defined $pid or die "Cannot fork: $!";
 476
 477        unless ($pid) {
 478                exec @execlist;
 479                die "Cannot exec @execlist: $!";
 480        }
 481
 482        return $kid;
 483}
 484
 485package Git::ActiveStatePipe;
 486use strict;
 487
 488sub TIEHANDLE {
 489        my ($class, @params) = @_;
 490        my $cmdline = join " ", @params;
 491        my  @data = qx{$cmdline};
 492        bless { i => 0, data => \@data }, $class;
 493}
 494
 495sub READLINE {
 496        my $self = shift;
 497        if ($self->{i} >= scalar @{$self->{data}}) {
 498                return undef;
 499        }
 500        return $self->{'data'}->[ $self->{i}++ ];
 501}
 502
 503sub CLOSE {
 504        my $self = shift;
 505        delete $self->{data};
 506        delete $self->{i};
 507}
 508
 509sub EOF {
 510        my $self = shift;
 511        return ($self->{i} >= scalar @{$self->{data}});
 512}