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