git-annotate.perlon commit Merge branch 'jc/sha1' (b296990)
   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","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                if (exists $revs{$rev}{parents} &&
 158                    scalar @{$revs{$rev}{parents}} != 0) {
 159
 160                        git_diff_parse($revs{$rev}{'parents'}, $rev, %revinfo);
 161                        push @revqueue, @{$revs{$rev}{'parents'}};
 162
 163                } else {
 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 ($parents, $rev, %revinfo) = @_;
 255
 256        my @filenames = ( $revs{$rev}{'filename'} );
 257        foreach my $parent (@$parents) {
 258                push @filenames, $revs{$parent}{'filename'};
 259        }
 260
 261        my $diff = open_pipe("git-diff-tree","-M","-p","-c",$rev,"--",
 262                                @filenames )
 263                or die "Failed to call git-diff for annotation: $!";
 264
 265        _git_diff_parse($diff, $parents, $rev, %revinfo);
 266
 267        close($diff);
 268}
 269
 270sub _git_diff_parse {
 271        my ($diff, $parents, $rev, %revinfo) = @_;
 272
 273        my $ri = 0;
 274
 275        my $slines = $revs{$rev}{'lines'};
 276        my (%plines, %pi);
 277
 278        my $gotheader = 0;
 279        my ($remstart);
 280        my $parent_count = @$parents;
 281
 282        my $diff_header_regexp = "^@";
 283        $diff_header_regexp .= "@" x @$parents;
 284        $diff_header_regexp .= ' -\d+,\d+' x @$parents;
 285        $diff_header_regexp .= ' \+(\d+),\d+';
 286
 287        my %claim_regexps;
 288        my $allparentplus = '^' . '\\+' x @$parents . '(.*)$';
 289
 290        {
 291                my $i = 0;
 292                foreach my $parent (@$parents) {
 293
 294                        $pi{$parent} = 0;
 295                        my $r = '^' . '.' x @$parents . '(.*)$';
 296                        my $p = $r;
 297                        substr($p,$i+1, 1) = '\\+';
 298
 299                        my $m = $r;
 300                        substr($m,$i+1, 1) = '-';
 301
 302                        $claim_regexps{$parent}{plus} = $p;
 303                        $claim_regexps{$parent}{minus} = $m;
 304
 305                        $plines{$parent} = [];
 306
 307                        $i++;
 308                }
 309        }
 310
 311        DIFF:
 312        while(<$diff>) {
 313                chomp;
 314                if (m/$diff_header_regexp/) {
 315                        $remstart = $1 - 1;
 316                        # (0-based arrays)
 317
 318                        $gotheader = 1;
 319
 320                        printf("Copying from %d to %d\n", $ri, $remstart);
 321                        foreach my $parent (@$parents) {
 322                                for (my $i = $ri; $i < $remstart; $i++) {
 323                                        $plines{$parent}[$pi{$parent}++] = $slines->[$i];
 324                                }
 325                        }
 326                        $ri = $remstart;
 327
 328                        next DIFF;
 329
 330                } elsif (!$gotheader) {
 331                        # Skip over the leadin.
 332                        next DIFF;
 333                }
 334
 335                if (m/^\\/) {
 336                        ;
 337                        # Skip \No newline at end of file.
 338                        # But this can be internationalized, so only look
 339                        # for an initial \
 340
 341                } else {
 342                        my %claims = ();
 343                        my $negclaim = 0;
 344                        my $allclaimed = 0;
 345                        my $line;
 346
 347                        if (m/$allparentplus/) {
 348                                claim_line($ri, $rev, $slines, %revinfo);
 349                                $allclaimed = 1;
 350
 351                        }
 352
 353                        PARENT:
 354                        foreach my $parent (keys %claim_regexps) {
 355                                my $m = $claim_regexps{$parent}{minus};
 356                                my $p = $claim_regexps{$parent}{plus};
 357
 358                                if (m/$m/) {
 359                                        $line = $1;
 360                                        $plines{$parent}[$pi{$parent}++] = [ $line, '', '', '', 0 ];
 361                                        $negclaim++;
 362
 363                                } elsif (m/$p/) {
 364                                        $line = $1;
 365                                        if (get_line($slines, $ri) eq $line) {
 366                                                # Found a match, claim
 367                                                $claims{$parent}++;
 368
 369                                        } else {
 370                                                die sprintf("Sync error: %d\n|%s\n|%s\n%s => %s\n",
 371                                                                $ri, $line,
 372                                                                get_line($slines, $ri),
 373                                                                $rev, $parent);
 374                                        }
 375                                }
 376                        }
 377
 378                        if (%claims) {
 379                                foreach my $parent (@$parents) {
 380                                        next if $claims{$parent} || $allclaimed;
 381                                        $plines{$parent}[$pi{$parent}++] = $slines->[$ri];
 382                                            #[ $line, '', '', '', 0 ];
 383                                }
 384                                $ri++;
 385
 386                        } elsif ($negclaim) {
 387                                next DIFF;
 388
 389                        } else {
 390                                if (substr($_,scalar @$parents) ne get_line($slines,$ri) ) {
 391                                        foreach my $parent (@$parents) {
 392                                                printf("parent %s is on line %d\n", $parent, $pi{$parent});
 393                                        }
 394
 395                                        die sprintf("Line %d, does not match:\n|%s|\n|%s|\n%s\n",
 396                                                    $ri,
 397                                                substr($_,scalar @$parents),
 398                                                get_line($slines,$ri), $rev);
 399                                }
 400                                foreach my $parent (@$parents) {
 401                                        $plines{$parent}[$pi{$parent}++] = $slines->[$ri];
 402                                }
 403                                $ri++;
 404                        }
 405                }
 406        }
 407
 408        for (my $i = $ri; $i < @{$slines} ; $i++) {
 409                foreach my $parent (@$parents) {
 410                        push @{$plines{$parent}}, $slines->[$ri];
 411                }
 412                $ri++;
 413        }
 414
 415        foreach my $parent (@$parents) {
 416                $revs{$parent}{lines} = $plines{$parent};
 417        }
 418
 419        return;
 420}
 421
 422sub get_line {
 423        my ($lines, $index) = @_;
 424
 425        return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
 426}
 427
 428sub git_cat_file {
 429        my ($rev, $filename) = @_;
 430        return () unless defined $rev && defined $filename;
 431
 432        my $blob = git_ls_tree($rev, $filename);
 433        die "Failed to find a blob for $filename in rev $rev\n" if !defined $blob;
 434
 435        my $catfile = open_pipe("git","cat-file", "blob", $blob)
 436                or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!;
 437
 438        my @lines;
 439        while(<$catfile>) {
 440                chomp;
 441                push @lines, $_;
 442        }
 443        close($catfile);
 444
 445        return @lines;
 446}
 447
 448sub git_ls_tree {
 449        my ($rev, $filename) = @_;
 450
 451        my $lstree = open_pipe("git","ls-tree",$rev,$filename)
 452                or die "Failed to call git ls-tree: $!";
 453
 454        my ($mode, $type, $blob, $tfilename);
 455        while(<$lstree>) {
 456                chomp;
 457                ($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4);
 458                last if ($tfilename eq $filename);
 459        }
 460        close($lstree);
 461
 462        return $blob if ($tfilename eq $filename);
 463        die "git-ls-tree failed to find blob for $filename";
 464
 465}
 466
 467
 468
 469sub claim_line {
 470        my ($floffset, $rev, $lines, %revinfo) = @_;
 471        my $oline = get_line($lines, $floffset);
 472        @{$lines->[$floffset]} = ( $oline, $rev,
 473                $revinfo{'author'}, $revinfo{'author_date'} );
 474        #printf("Claiming line %d with rev %s: '%s'\n",
 475        #               $floffset, $rev, $oline) if 1;
 476}
 477
 478sub git_commit_info {
 479        my ($rev) = @_;
 480        my $commit = open_pipe("git-cat-file", "commit", $rev)
 481                or die "Failed to call git-cat-file: $!";
 482
 483        my %info;
 484        while(<$commit>) {
 485                chomp;
 486                last if (length $_ == 0);
 487
 488                if (m/^author (.*) <(.*)> (.*)$/) {
 489                        $info{'author'} = $1;
 490                        $info{'author_email'} = $2;
 491                        $info{'author_date'} = $3;
 492                } elsif (m/^committer (.*) <(.*)> (.*)$/) {
 493                        $info{'committer'} = $1;
 494                        $info{'committer_email'} = $2;
 495                        $info{'committer_date'} = $3;
 496                }
 497        }
 498        close($commit);
 499
 500        return %info;
 501}
 502
 503sub format_date {
 504        if ($rawtime) {
 505                return $_[0];
 506        }
 507        my ($timestamp, $timezone) = split(' ', $_[0]);
 508        my $minutes = abs($timezone);
 509        $minutes = int($minutes / 100) * 60 + ($minutes % 100);
 510        if ($timezone < 0) {
 511            $minutes = -$minutes;
 512        }
 513        my $t = $timestamp + $minutes * 60;
 514        return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($t));
 515}
 516
 517# Copied from git-send-email.perl - We need a Git.pm module..
 518sub gitvar {
 519    my ($var) = @_;
 520    my $fh;
 521    my $pid = open($fh, '-|');
 522    die "$!" unless defined $pid;
 523    if (!$pid) {
 524        exec('git-var', $var) or die "$!";
 525    }
 526    my ($val) = <$fh>;
 527    close $fh or die "$!";
 528    chomp($val);
 529    return $val;
 530}
 531
 532sub gitvar_name {
 533    my ($name) = @_;
 534    my $val = gitvar($name);
 535    my @field = split(/\s+/, $val);
 536    return join(' ', @field[0...(@field-4)]);
 537}
 538
 539sub open_pipe {
 540        if ($^O eq '##INSERT_ACTIVESTATE_STRING_HERE##') {
 541                return open_pipe_activestate(@_);
 542        } else {
 543                return open_pipe_normal(@_);
 544        }
 545}
 546
 547sub open_pipe_activestate {
 548        tie *fh, "Git::ActiveStatePipe", @_;
 549        return *fh;
 550}
 551
 552sub open_pipe_normal {
 553        my (@execlist) = @_;
 554
 555        my $pid = open my $kid, "-|";
 556        defined $pid or die "Cannot fork: $!";
 557
 558        unless ($pid) {
 559                exec @execlist;
 560                die "Cannot exec @execlist: $!";
 561        }
 562
 563        return $kid;
 564}
 565
 566package Git::ActiveStatePipe;
 567use strict;
 568
 569sub TIEHANDLE {
 570        my ($class, @params) = @_;
 571        my $cmdline = join " ", @params;
 572        my  @data = qx{$cmdline};
 573        bless { i => 0, data => \@data }, $class;
 574}
 575
 576sub READLINE {
 577        my $self = shift;
 578        if ($self->{i} >= scalar @{$self->{data}}) {
 579                return undef;
 580        }
 581        return $self->{'data'}->[ $self->{i}++ ];
 582}
 583
 584sub CLOSE {
 585        my $self = shift;
 586        delete $self->{data};
 587        delete $self->{i};
 588}
 589
 590sub EOF {
 591        my $self = shift;
 592        return ($self->{i} >= scalar @{$self->{data}});
 593}