gitweb.cgion commit v125 (42f7eb9)
   1#!/usr/bin/perl
   2
   3# gitweb.pl - simple web interface to track changes in git repositories
   4#
   5# (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
   6# (C) 2005, Christian Gierke <ch@gierke.de>
   7#
   8# This program is licensed under the GPL v2, or a later version
   9
  10use strict;
  11use warnings;
  12use CGI qw(:standard :escapeHTML);
  13use CGI::Carp qw(fatalsToBrowser);
  14use Fcntl ':mode';
  15
  16my $cgi = new CGI;
  17my $version =           "125";
  18my $my_url =            $cgi->url();
  19my $my_uri =            $cgi->url(-absolute => 1);
  20my $rss_link = "";
  21
  22# absolute fs-path which will be prepended to the project path
  23my $projectroot =       "/pub/scm";
  24
  25# location of the git-core binaries
  26my $gitbin =            "/usr/bin";
  27
  28# location for temporary files needed for diffs
  29my $gittmp =            "/tmp/gitweb";
  30
  31# target of the home link on top of all pages
  32my $home_link =         "/git";
  33
  34# source of projects list
  35#my $projects_list = $projectroot;
  36my $projects_list = "index/index.txt";
  37
  38# input validation and dispatch
  39my $action = $cgi->param('a');
  40if (defined $action) {
  41        if ($action =~ m/[^0-9a-zA-Z\.\-]+/) {
  42                undef $action;
  43                die_error(undef, "Invalid action parameter.");
  44        }
  45        if ($action eq "git-logo.png") {
  46                git_logo();
  47                exit;
  48        }
  49} else {
  50        $action = "log";
  51}
  52
  53my $project = $cgi->param('p');
  54if (defined $project) {
  55        if ($project =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
  56                undef $project;
  57                die_error(undef, "Non-canonical project parameter.");
  58        }
  59        if ($project =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~]/) {
  60                undef $project;
  61                die_error(undef, "Invalid character in project parameter.");
  62        }
  63        if (!(-d "$projectroot/$project")) {
  64                undef $project;
  65                die_error(undef, "No such directory.");
  66        }
  67        if (!(-e "$projectroot/$project/HEAD")) {
  68                undef $project;
  69                die_error(undef, "No such project.");
  70        }
  71        $rss_link = "<link rel=\"alternate\" title=\"$project log\" href=\"$my_uri?p=$project;a=rss\" type=\"application/rss+xml\"/>";
  72        $ENV{'SHA1_FILE_DIRECTORY'} = "$projectroot/$project/objects";
  73} else {
  74        git_project_list($projects_list);
  75        exit;
  76}
  77
  78my $file_name = $cgi->param('f');
  79if (defined $file_name) {
  80        if ($file_name =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
  81                undef $file_name;
  82                die_error(undef, "Non-canonical file parameter.");
  83        }
  84        if ($file_name =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~\:\!]/) {
  85                undef $file_name;
  86                die_error(undef, "Invalid character in file parameter.");
  87        }
  88}
  89
  90my $hash = $cgi->param('h');
  91if (defined $hash && !($hash =~ m/^[0-9a-fA-F]{40}$/)) {
  92        undef $hash;
  93        die_error(undef, "Invalid hash parameter.");
  94}
  95
  96my $hash_parent = $cgi->param('hp');
  97if (defined $hash_parent && !($hash_parent =~ m/^[0-9a-fA-F]{40}$/)) {
  98        undef $hash_parent;
  99        die_error(undef, "Invalid hash_parent parameter.");
 100}
 101
 102my $hash_base = $cgi->param('hb');
 103if (defined $hash_base && !($hash_base =~ m/^[0-9a-fA-F]{40}$/)) {
 104        undef $hash_base;
 105        die_error(undef, "Invalid parent hash parameter.");
 106}
 107
 108my $time_back = $cgi->param('t');
 109if (defined $time_back) {
 110        if ($time_back =~ m/^[^0-9]+$/) {
 111                undef $time_back;
 112                die_error(undef, "Invalid time parameter.");
 113        }
 114}
 115
 116if ($action eq "blob") {
 117        git_blob();
 118        exit;
 119} elsif ($action eq "tree") {
 120        git_tree();
 121        exit;
 122} elsif ($action eq "rss") {
 123        git_rss();
 124        exit;
 125} elsif ($action eq "commit") {
 126        git_commit();
 127        exit;
 128} elsif ($action eq "log") {
 129        git_log();
 130        exit;
 131} elsif ($action eq "blobdiff") {
 132        git_blobdiff();
 133        exit;
 134} elsif ($action eq "commitdiff") {
 135        git_commitdiff();
 136        exit;
 137} elsif ($action eq "history") {
 138        git_history();
 139        exit;
 140} else {
 141        undef $action;
 142        die_error(undef, "Unknown action.");
 143        exit;
 144}
 145
 146sub git_header_html {
 147        my $status = shift || "200 OK";
 148
 149        my $title = "git";
 150        if (defined $project) {
 151                $title .= " - $project";
 152                if (defined $action) {
 153                        $title .= "/$action";
 154                }
 155        }
 156        print $cgi->header(-type=>'text/html',  -charset => 'utf-8', -status=> $status);
 157        print <<EOF;
 158<?xml version="1.0" encoding="utf-8"?>
 159<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 160<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 161<!-- git web interface v$version, (C) 2005, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke <ch\@gierke.de> -->
 162<head>
 163<title>$title</title>
 164$rss_link
 165<style type="text/css">
 166body { font-family: sans-serif; font-size: 12px; margin:0px; }
 167a { color:#0000cc; }
 168a:hover { color:#880000; }
 169a:visited { color:#880000; }
 170a:active { color:#880000; }
 171div.page_header {
 172        margin:15px 15px 0px; height:25px; padding:8px;
 173        font-size:18px; font-weight:bold; background-color:#d9d8d1;
 174}
 175div.page_header a:visited { color:#0000cc; }
 176div.page_header a:hover { color:#880000; }
 177div.page_nav { margin:0px 15px; padding:8px; border:solid #d9d8d1; border-width:0px 1px; }
 178div.page_nav a:visited { color:#0000cc; }
 179div.page_path { font-weight:bold; margin:0px 15px; padding:8px; border:solid #d9d8d1; border-width:0px 1px 1px}
 180div.page_footer { margin:0px 15px 15px; height:17px; padding:4px; padding-left:8px; background-color: #d9d8d1; }
 181div.page_footer_text { float:left; color:#555555; font-style:italic; }
 182div.page_body { margin:0px 15px; padding:8px; border:solid #d9d8d1; border-width:0px 1px; }
 183div.title, a.title {
 184        display:block; margin:0px 15px; padding:6px 8px;
 185        font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
 186}
 187a.title:hover { background-color: #d9d8d1; }
 188div.title_text { margin:0px 15px; padding:6px 8px; border: solid #d9d8d1; border-width:0px 1px 1px; }
 189div.log_body { margin:0px 15px; padding:8px; padding-left:150px; border:solid #d9d8d1; border-width:0px 1px; }
 190span.log_age { position:relative; float:left; width:142px; font-style:italic; }
 191div.log_link { font-size:10px; font-family:sans-serif; font-style:normal; position:relative; float:left; width:142px; }
 192div.list {
 193        display:block; margin:0px 15px; padding:4px 6px 2px; border:solid #d9d8d1; border-width:0px 1px;
 194        font-weight:bold;
 195}
 196div.list_head {
 197        display:block; margin:0px 15px; padding:6px 6px 4px; border:solid #d9d8d1; border-width:0px 1px 1px;
 198        font-style:italic;
 199}
 200div.list a { text-decoration:none; color:#000000; }
 201div.list a:hover { color:#880000; }
 202div.link {
 203        margin:0px 15px; padding:4px 6px 6px; border:solid #d9d8d1; border-width:0px 1px 1px;
 204        font-family:sans-serif; font-size:10px;
 205}
 206td { padding:5px 15px 0px 0px; font-size:12px; }
 207th { padding-right:10px; font-size:12px; text-align:left; }
 208td.link { font-family:sans-serif; font-size:10px; }
 209td.pre { font-family:monospace; font-size:12px; white-space:pre; padding:2px 15px 0px 0px; }
 210span.diff_info { color:#000099; background-color:#edece6; font-style:italic; }
 211a.rss_logo { float:right; padding:3px 0px; width:35px; line-height:10px;
 212        border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
 213        color:#ffffff; background-color:#ff6600;
 214        font-weight:bold; font-family:sans-serif; font-size:10px;
 215        text-align:center; text-decoration:none;
 216}
 217a.rss_logo:hover { background-color:#ee5500; }
 218</style>
 219</head>
 220<body>
 221EOF
 222        print "<div class=\"page_header\">\n" .
 223              "<a href=\"http://kernel.org/pub/software/scm/cogito\">" .
 224              "<img src=\"$my_uri?a=git-logo.png\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/></a>";
 225        print $cgi->a({-href => $home_link}, "projects") . " / ";
 226        if (defined $project) {
 227                print $cgi->a({-href => "$my_uri?p=$project;a=log"}, escapeHTML($project));
 228                if (defined $action) {
 229                        print " / $action";
 230                }
 231        }
 232        print "</div>\n";
 233}
 234
 235sub git_footer_html {
 236        print "<div class=\"page_footer\">\n";
 237        if (defined $project) {
 238                my $descr = git_read_description($project);
 239                if (defined $descr) {
 240                        print "<div class=\"page_footer_text\">" . escapeHTML($descr) . "</div>\n";
 241                }
 242                print $cgi->a({-href => "$my_uri?p=$project;a=rss", -class => "rss_logo"}, "RSS") . "\n";
 243        }
 244        print "</div>\n" .
 245              "</body>\n" .
 246              "</html>";
 247}
 248
 249sub die_error {
 250        my $status = shift || "403 Forbidden";
 251        my $error = shift || "Malformed query, file missing or permission denied"; 
 252
 253        git_header_html($status);
 254        print "<div class=\"page_body\">\n" .
 255              "<br/><br/>\n";
 256        print "$status - $error\n";
 257        print "<br/></div>\n";
 258        git_footer_html();
 259        exit;
 260}
 261
 262sub git_get_type {
 263        my $hash = shift;
 264
 265        open my $fd, "-|", "$gitbin/git-cat-file -t $hash" || return;
 266        my $type = <$fd>;
 267        close $fd;
 268        chomp $type;
 269        return $type;
 270}
 271
 272sub git_read_head {
 273        my $path = shift;
 274
 275        open my $fd, "$projectroot/$path/HEAD" || return undef;
 276        my $head = <$fd>;
 277        close $fd;
 278        chomp $head;
 279        if ($head =~ m/^[0-9a-fA-F]{40}$/) {
 280                return $head;
 281        } else {
 282                return undef;
 283        }
 284}
 285
 286sub git_read_description {
 287        my $path = shift;
 288
 289        open my $fd, "$projectroot/$path/description" || return undef;
 290        my $descr = <$fd>;
 291        close $fd;
 292        chomp $descr;
 293        return $descr;
 294}
 295
 296sub git_read_commit {
 297        my $commit = shift;
 298        my %co;
 299        my @parents;
 300
 301        open my $fd, "-|", "$gitbin/git-cat-file commit $commit" || return;
 302        while (my $line = <$fd>) {
 303                last if $line eq "\n";
 304                chomp $line;
 305                if ($line =~ m/^tree (.*)$/) {
 306                        $co{'tree'} = $1;
 307                } elsif ($line =~ m/^parent (.*)$/) {
 308                        push @parents, $1;
 309                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 310                        $co{'author'} = $1;
 311                        $co{'author_epoch'} = $2;
 312                        $co{'author_tz'} = $3;
 313                        $co{'author_name'} = $co{'author'};
 314                        $co{'author_name'} =~ s/ <.*//;
 315                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
 316                        $co{'committer'} = $1;
 317                        $co{'committer_epoch'} = $2;
 318                        $co{'committer_tz'} = $3;
 319                        $co{'committer_name'} = $co{'committer'};
 320                        $co{'committer_name'} =~ s/ <.*//;
 321                }
 322        }
 323        $co{'parents'} = \@parents;
 324        $co{'parent'} = $parents[0];
 325        my (@comment) = map { chomp; $_ } <$fd>;
 326        $co{'comment'} = \@comment;
 327        $comment[0] =~ m/^(.{0,60}[^ ]*)/;
 328        $co{'title'} = $1;
 329        if ($comment[0] ne $co{'title'}) {
 330                $co{'title'} .= " [...]";
 331        }
 332        close $fd || return;
 333        if (!defined $co{'tree'}) {
 334                return undef
 335        };
 336
 337        my $age = time - $co{'committer_epoch'};
 338        $co{'age'} = $age;
 339        if ($age > 60*60*24*365*2) {
 340                $co{'age_string'} = (int $age/60/60/24/365);
 341                $co{'age_string'} .= " years ago";
 342        } elsif ($age > 60*60*24*365/12*2) {
 343                $co{'age_string'} = int $age/60/60/24/365/12;
 344                $co{'age_string'} .= " months ago";
 345        } elsif ($age > 60*60*24*7*2) {
 346                $co{'age_string'} = int $age/60/60/24/7;
 347                $co{'age_string'} .= " weeks ago";
 348        } elsif ($age > 60*60*24*2) {
 349                $co{'age_string'} = int $age/60/60/24;
 350                $co{'age_string'} .= " days ago";
 351        } elsif ($age > 60*60*2) {
 352                $co{'age_string'} = int $age/60/60;
 353                $co{'age_string'} .= " hours ago";
 354        } elsif ($age > 60*2) {
 355                $co{'age_string'} = int $age/60;
 356                $co{'age_string'} .= " minutes ago";
 357        } elsif ($age > 2) {
 358                $co{'age_string'} = int $age;
 359                $co{'age_string'} .= " seconds ago";
 360        } else {
 361                $co{'age_string'} .= " right now";
 362        }
 363        return %co;
 364}
 365
 366sub git_diff_html {
 367        my $from = shift;
 368        my $from_name = shift;
 369        my $to = shift;
 370        my $to_name = shift;
 371
 372        my $from_tmp = "/dev/null";
 373        my $to_tmp = "/dev/null";
 374        my $pid = $$;
 375
 376        # create tmp from-file
 377        if (defined $from) {
 378                $from_tmp = "$gittmp/gitweb_" . $$ . "_from";
 379                open my $fd2, "> $from_tmp";
 380                open my $fd, "-|", "$gitbin/git-cat-file blob $from";
 381                my @file = <$fd>;
 382                print $fd2 @file;
 383                close $fd2;
 384                close $fd;
 385        }
 386
 387        # create tmp to-file
 388        if (defined $to) {
 389                $to_tmp = "$gittmp/gitweb_" . $$ . "_to";
 390                open my $fd2, "> $to_tmp";
 391                open my $fd, "-|", "$gitbin/git-cat-file blob $to";
 392                my @file = <$fd>;
 393                print $fd2 @file;
 394                close $fd2;
 395                close $fd;
 396        }
 397
 398        open my $fd, "-|", "/usr/bin/diff -u -p -L $from_name -L $to_name $from_tmp $to_tmp";
 399        while (my $line = <$fd>) {
 400                my $char = substr($line,0,1);
 401                # skip errors
 402                next if $char eq '\\';
 403                # color the diff
 404                print '<span style="color: #008800;">' if $char eq '+';
 405                print '<span style="color: #CC0000;">' if $char eq '-';
 406                print '<span style="color: #990099;">' if $char eq '@';
 407                print escapeHTML($line);
 408                print '</span>' if $char eq '+' or $char eq '-' or $char eq '@';
 409        }
 410        close $fd;
 411
 412        if (defined $from) {
 413                unlink($from_tmp);
 414        }
 415        if (defined $to) {
 416                unlink($to_tmp);
 417        }
 418}
 419
 420sub mode_str {
 421        my $mode = oct shift;
 422
 423        if (S_ISDIR($mode & S_IFMT)) {
 424                return 'drwxr-xr-x';
 425        } elsif (S_ISLNK($mode)) {
 426                return 'lrwxrwxrwx';
 427        } elsif (S_ISREG($mode)) {
 428                # git cares only about the executable bit
 429                if ($mode & S_IXUSR) {
 430                        return '-rwxr-xr-x';
 431                } else {
 432                        return '-rw-r--r--';
 433                };
 434        } else {
 435                return '----------';
 436        }
 437}
 438
 439sub file_type {
 440        my $mode = oct shift;
 441
 442        if (S_ISDIR($mode & S_IFMT)) {
 443                return "directory";
 444        } elsif (S_ISLNK($mode)) {
 445                return "symlink";
 446        } elsif (S_ISREG($mode)) {
 447                return "file";
 448        } else {
 449                return "unknown";
 450        }
 451}
 452
 453sub date_str {
 454        my $epoch = shift;
 455        my $tz = shift || "-0000";
 456
 457        my %date;
 458        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
 459        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
 460        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
 461        $date{'hour'} = $hour;
 462        $date{'minute'} = $min;
 463        $date{'mday'} = $mday;
 464        $date{'day'} = $days[$wday];
 465        $date{'month'} = $months[$mon];
 466        $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
 467        $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
 468
 469        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
 470        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
 471        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
 472        $date{'hour_local'} = $hour;
 473        $date{'minute_local'} = $min;
 474        $date{'tz_local'} = $tz;
 475        return %date;
 476}
 477
 478# git-logo (cached in browser for one day)
 479sub git_logo {
 480        print $cgi->header(-type => 'image/png', -expires => '+1d');
 481        # cat git-logo.png | hexdump -e '16/1 " %02x"  "\n"' | sed 's/ /\\x/g'
 482        print   "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" .
 483                "\x00\x00\x00\x48\x00\x00\x00\x1b\x04\x03\x00\x00\x00\x2d\xd9\xd4" .
 484                "\x2d\x00\x00\x00\x18\x50\x4c\x54\x45\xff\xff\xff\x60\x60\x5d\xb0" .
 485                "\xaf\xaa\x00\x80\x00\xce\xcd\xc7\xc0\x00\x00\xe8\xe8\xe6\xf7\xf7" .
 486                "\xf6\x95\x0c\xa7\x47\x00\x00\x00\x73\x49\x44\x41\x54\x28\xcf\x63" .
 487                "\x48\x67\x20\x04\x4a\x5c\x18\x0a\x08\x2a\x62\x53\x61\x20\x02\x08" .
 488                "\x0d\x69\x45\xac\xa1\xa1\x01\x30\x0c\x93\x60\x36\x26\x52\x91\xb1" .
 489                "\x01\x11\xd6\xe1\x55\x64\x6c\x6c\xcc\x6c\x6c\x0c\xa2\x0c\x70\x2a" .
 490                "\x62\x06\x2a\xc1\x62\x1d\xb3\x01\x02\x53\xa4\x08\xe8\x00\x03\x18" .
 491                "\x26\x56\x11\xd4\xe1\x20\x97\x1b\xe0\xb4\x0e\x35\x24\x71\x29\x82" .
 492                "\x99\x30\xb8\x93\x0a\x11\xb9\x45\x88\xc1\x8d\xa0\xa2\x44\x21\x06" .
 493                "\x27\x41\x82\x40\x85\xc1\x45\x89\x20\x70\x01\x00\xa4\x3d\x21\xc5" .
 494                "\x12\x1c\x9a\xfe\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
 495}
 496
 497sub git_project_list {
 498        my $project_list = shift;
 499        my @list;
 500
 501        if (-d $project_list) {
 502                # search in directory
 503                my $dir = $project_list;
 504                opendir my $dh, $dir || return undef;
 505                while (my $dir = readdir($dh)) {
 506                        if (-e "$projectroot/$dir/HEAD") {
 507                                push @list, $dir;
 508                        }
 509                }
 510                closedir($dh);
 511        } elsif (-e $project_list) {
 512                # read from file
 513                open my $fd , $project_list || return undef;
 514                while (my $line = <$fd>) {
 515                        chomp $line;
 516                        if (-e "$projectroot/$line/HEAD") {
 517                                push @list, $line;
 518                        }
 519                }
 520                close $fd;
 521        }
 522
 523        if (!@list) {
 524                die_error(undef, "No project found.");
 525        }
 526        @list = sort @list;
 527
 528        git_header_html();
 529        print "<div class=\"page_body\"><br/>\n";
 530        print "<table cellspacing=\"0\">\n";
 531        print "<tr>\n" .
 532              "<th>Project</th>\n" .
 533              "<th>Description</th>\n" .
 534              "<th>Owner</th>\n" .
 535              "<th>last change</th>\n" .
 536              "</tr>\n";
 537        foreach my $proj (@list) {
 538                my $head = git_read_head($proj);
 539                if (!defined $head) {
 540                        next;
 541                }
 542                $ENV{'SHA1_FILE_DIRECTORY'} = "$projectroot/$proj/objects";
 543                my %co = git_read_commit($head);
 544                if (!%co) {
 545                        next;
 546                }
 547                my $descr = git_read_description($proj) || "";
 548                my $owner = "";
 549                my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat("$projectroot/$proj");
 550                my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
 551                if (defined $gcos) {
 552                        $owner = $gcos;
 553                        $owner =~ s/[,;].*$//;
 554                }
 555                print "<tr>\n" .
 556                      "<td>" . $cgi->a({-href => "$my_uri?p=$proj;a=log"}, escapeHTML($proj)) . "</td>\n" .
 557                      "<td>$descr</td>\n" .
 558                      "<td><i>$owner</i></td>\n";
 559                if ($co{'age'} < 60*60*2) {
 560                        print "<td><span style =\"color: #009900;\"><b><i>" . $co{'age_string'} . "</i></b></span></td>\n";
 561                } elsif ($co{'age'} < 60*60*24*2) {
 562                        print "<td><span style =\"color: #009900;\"><i>" . $co{'age_string'} . "</i></span></td>\n";
 563                } else {
 564                        print "<td><i>" . $co{'age_string'} . "</i></td>\n";
 565                }
 566                print "</tr>\n";
 567        }
 568        print "</table>\n" .
 569              "<br/>\n" .
 570              "</div>\n";
 571        git_footer_html();
 572}
 573
 574sub git_get_hash_by_path {
 575        my $base = shift;
 576        my $path = shift;
 577
 578        my $tree = $base;
 579        my @parts = split '/', $path;
 580        while (my $part = shift @parts) {
 581                open my $fd, "-|", "$gitbin/git-ls-tree $tree" || die_error(undef, "Open git-ls-tree failed.");
 582                my (@entries) = map { chomp; $_ } <$fd>;
 583                close $fd || return undef;
 584                foreach my $line (@entries) {
 585                        #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
 586                        $line =~ m/^([0-9]+)\t(.*)\t(.*)\t(.*)$/;
 587                        my $t_mode = $1;
 588                        my $t_type = $2;
 589                        my $t_hash = $3;
 590                        my $t_name = $4;
 591                        if ($t_name eq $part) {
 592                                if (!(@parts)) {
 593                                        return $t_hash;
 594                                }
 595                                if ($t_type eq "tree") {
 596                                        $tree = $t_hash;
 597                                }
 598                                last;
 599                        }
 600                }
 601        }
 602}
 603
 604sub git_blob {
 605        if (!defined $hash && defined $file_name) {
 606                my $base = $hash_base || git_read_head($project);
 607                $hash = git_get_hash_by_path($base, $file_name, "blob");
 608        }
 609        open my $fd, "-|", "$gitbin/git-cat-file blob $hash" || die_error(undef, "Open failed.");
 610        my $base = $file_name || "";
 611        git_header_html();
 612        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
 613                print "<div class=\"page_nav\">\n" .
 614                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base"}, "commit") .
 615                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash_base"}, "commitdiff") .
 616                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash_base"}, "tree");
 617                if (defined $file_name) {
 618                        print " | " . $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash_base;f=$file_name"}, "history");
 619                }
 620                print "<br/><br/>\n" .
 621                      "</div>\n";
 622                print "<div>\n" .
 623                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base", -class => "title"}, escapeHTML($co{'title'})) . "\n";
 624        } else {
 625                print "<div class=\"page_nav\">\n" .
 626                      "<br/><br/></div>\n" .
 627                      "<div class=\"title\">$hash</div>\n";
 628        }
 629        if (defined $file_name) {
 630                print "<div class=\"page_path\">/$file_name</div>\n";
 631        }
 632        print "<div class=\"page_body\"><pre>\n";
 633        my $nr;
 634        while (my $line = <$fd>) {
 635                $nr++;
 636                printf "<span style =\"color: #999999;\">%4i\t</span>%s", $nr, escapeHTML($line);;
 637        }
 638        close $fd || print "Reading blob failed.\n";
 639        print "</pre>\n";
 640        print "</div>";
 641        git_footer_html();
 642}
 643
 644sub git_tree {
 645        if (!defined $hash) {
 646                $hash = git_read_head($project);
 647                if (defined $file_name) {
 648                        my $base = $hash_base || git_read_head($project);
 649                        $hash = git_get_hash_by_path($base, $file_name, "tree");
 650                }
 651        }
 652        open my $fd, "-|", "$gitbin/git-ls-tree $hash" || die_error(undef, "Open git-ls-tree failed.");
 653        my (@entries) = map { chomp; $_ } <$fd>;
 654        close $fd || die_error(undef, "Reading tree failed.");
 655
 656        git_header_html();
 657        my $base_key = "";
 658        my $file_key = "";
 659        my $base = "";
 660        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
 661                $base_key = ";hb=$hash_base";
 662                print "<div class=\"page_nav\">\n" .
 663                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base"}, "commit") . " | " .
 664                      $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash_base"}, "commitdiff") . " | " .
 665                      $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash_base"}, "tree") .
 666                      "<br/><br/>\n" .
 667                      "</div>\n";
 668                print "<div>\n" .
 669                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
 670                      "</div>\n";
 671        } else {
 672                print "<div class=\"page_nav\">\n";
 673                print "<br/><br/></div>\n";
 674                print "<div class=\"title\">$hash</div>\n";
 675        }
 676        if (defined $file_name) {
 677                $base = "$file_name/";
 678                print "<div class=\"page_path\">/$file_name</div>\n";
 679        } else {
 680                print "<div class=\"page_path\">/</div>\n";
 681        }
 682        print "<div class=\"page_body\">\n";
 683        print "<table cellspacing=\"0\">\n";
 684        foreach my $line (@entries) {
 685                #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
 686                $line =~ m/^([0-9]+)\t(.*)\t(.*)\t(.*)$/;
 687                my $t_mode = $1;
 688                my $t_type = $2;
 689                my $t_hash = $3;
 690                my $t_name = $4;
 691                $file_key = ";f=$base$t_name";
 692                print "<tr>\n" .
 693                      "<td class=\"pre\">" . mode_str($t_mode) . "</td>\n";
 694                if ($t_type eq "blob") {
 695                        print "<td class=\"pre\">$t_name</td>\n";
 696                        print "<td class=\"link\">" .
 697                              $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$t_hash" . $base_key . $file_key}, "file") .
 698                              " | " . $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash_base" . $file_key}, "history") .
 699                              "</td>\n";
 700                } elsif ($t_type eq "tree") {
 701                        print "<td class=\"pre\">" .
 702                              $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$t_hash" . $base_key . $file_key}, $t_name) .
 703                              "</td>\n";
 704                }
 705                print "</tr>\n";
 706        }
 707        print "</table>\n" .
 708              "</div>";
 709        git_footer_html();
 710}
 711
 712sub git_rss {
 713        open my $fd, "-|", "$gitbin/git-rev-list --max-count=20 " . git_read_head($project) || die_error(undef, "Open failed.");
 714        my (@revlist) = map { chomp; $_ } <$fd>;
 715        close $fd || die_error(undef, "Reading rev-list failed.");
 716
 717        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
 718        print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
 719              "<rss version=\"0.91\">\n";
 720        print "<channel>\n";
 721        print "<title>$project</title>\n".
 722              "<link> " . $my_url . "/$project/log</link>\n".
 723              "<description>$project log</description>\n".
 724              "<language>en</language>\n";
 725
 726        foreach my $commit (@revlist) {
 727                my %co = git_read_commit($commit);
 728                my %ad = date_str($co{'author_epoch'});
 729                print "<item>\n" .
 730                      "\t<title>" . sprintf("%d %s %02d:%02d", $ad{'mday'}, $ad{'month'}, $ad{'hour'}, $ad{'minute'}) . " - " . escapeHTML($co{'title'}) . "</title>\n" .
 731                      "\t<link> " . $my_url . "?p=$project;a=commit;h=$commit</link>\n" .
 732                      "\t<description>";
 733                my $comment = $co{'comment'};
 734                foreach my $line (@$comment) {
 735                        print escapeHTML($line) . "<br/>\n";
 736                }
 737                print "\t</description>\n" .
 738                      "</item>\n";
 739        }
 740        print "</channel></rss>";
 741}
 742
 743sub git_log {
 744        my $head = git_read_head($project);
 745        my $limit_option = "";
 746        if (!defined $time_back) {
 747                $limit_option = "--max-count=10";
 748        } elsif ($time_back > 0) {
 749                my $date = time - $time_back*24*60*60;
 750                $limit_option = "--max-age=$date";
 751        }
 752        open my $fd, "-|", "$gitbin/git-rev-list $limit_option $head" || die_error(undef, "Open failed.");
 753        my (@revlist) = map { chomp; $_ } <$fd>;
 754        close $fd || die_error(undef, "Reading rev-list failed.");
 755
 756        git_header_html();
 757        print "<div class=\"page_nav\">\n";
 758        print $cgi->a({-href => "$my_uri?p=$project;a=log"}, "last 10") . " | " .
 759              $cgi->a({-href => "$my_uri?p=$project;a=log;t=1"}, "day") . " | " .
 760              $cgi->a({-href => "$my_uri?p=$project;a=log;t=7"}, "week") . " | " .
 761              $cgi->a({-href => "$my_uri?p=$project;a=log;t=31"}, "month") . " | " .
 762              $cgi->a({-href => "$my_uri?p=$project;a=log;t=365"}, "year") . " | " .
 763              $cgi->a({-href => "$my_uri?p=$project;a=log;t=0"}, "all") . "<br/>\n";
 764        print "<br/><br/>\n" .
 765              "</div>\n";
 766
 767        if (!@revlist) {
 768                my %co = git_read_commit($head);
 769                print "<div class=\"page_body\"> Last change " . $co{'age_string'} . ".<br/><br/></div>\n";
 770        }
 771
 772        foreach my $commit (@revlist) {
 773                my %co = git_read_commit($commit);
 774                next if !%co;
 775                my %ad = date_str($co{'author_epoch'});
 776                print "<div>\n" .
 777                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit", -class => "title"},
 778                      "<span class=\"log_age\">" . $co{'age_string'} . "</span>" . escapeHTML($co{'title'})) . "\n" .
 779                      "</div>\n";
 780                print "<div class=\"title_text\">\n" .
 781                      "<div class=\"log_link\">\n" .
 782                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") .
 783                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$commit"}, "commitdiff") .
 784                      "<br/>\n" .
 785                      "</div>\n" .
 786                      "<i>" . escapeHTML($co{'author_name'}) .  " [" . $ad{'rfc2822'} . "]</i><br/>\n" .
 787                      "</div>\n" .
 788                      "<div class=\"log_body\">\n";
 789                my $comment = $co{'comment'};
 790                my $empty = 0;
 791                foreach my $line (@$comment) {
 792                        if ($line =~ m/^(signed[ \-]off[\-]by[ :]|acked[\-]by[ \:]|cc[ :])/i) {
 793                                next;
 794                        }
 795                        if ($line eq "") {
 796                                if ($empty) {
 797                                        next;
 798                                }
 799                                $empty = 1;
 800                        } else {
 801                                $empty = 0;
 802                        }
 803                        print escapeHTML($line) . "<br/>\n";
 804                }
 805                if (!$empty) {
 806                        print "<br/>\n";
 807                }
 808                print "</div>\n";
 809        }
 810        git_footer_html();
 811}
 812
 813sub git_commit {
 814        my %co = git_read_commit($hash);
 815        if (!%co) {
 816                die_error(undef, "Unknown commit object.");
 817        }
 818        my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
 819        my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
 820
 821        my @difftree;
 822        if (defined $co{'parent'}) {
 823                open my $fd, "-|", "$gitbin/git-diff-tree -r " . $co{'parent'} . " $hash" || die_error(undef, "Open failed.");
 824                @difftree = map { chomp; $_ } <$fd>;
 825                close $fd || die_error(undef, "Reading diff-tree failed.");
 826        } else {
 827                # fake git-diff-tree output for initial revision
 828                open my $fd, "-|", "$gitbin/git-ls-tree -r $hash" || die_error(undef, "Open failed.");
 829                @difftree = map { chomp;  "+" . $_ } <$fd>;
 830                close $fd || die_error(undef, "Reading ls-tree failed.");
 831        }
 832        git_header_html();
 833        print "<div class=\"page_nav\">\n" .
 834              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit");
 835        if (defined $co{'parent'}) {
 836                print " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "commitdiff");
 837        }
 838        print " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash"}, "tree") . "\n" .
 839              "<br/><br/></div>\n";
 840        if (defined $co{'parent'}) {
 841                print "<div>\n" .
 842                      $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
 843                      "</div>\n";
 844        } else {
 845                print "<div>\n" .
 846                      $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
 847                      "</div>\n";
 848        }
 849        print "<div class=\"title_text\">\n" .
 850              "<table cellspacing=\"0\">\n";
 851        print "<tr><td>author</td><td>" . escapeHTML($co{'author'}) . "</td></tr>\n".
 852              "<tr><td></td><td> " . $ad{'rfc2822'};
 853        if ($ad{'hour_local'} < 6) {
 854                printf(" (<span style=\"color: #cc0000;\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 855        } else {
 856                printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 857        }
 858        print "</td></tr>\n";
 859        print "<tr><td>committer</td><td>" . escapeHTML($co{'committer'}) . "</td></tr>\n";
 860        print "<tr><td></td><td> " . $cd{'rfc2822'} . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
 861        print "<tr><td>commit</td><td style=\"font-family: monospace;\">$hash</td></tr>\n";
 862        print "<tr><td>tree</td><td style=\"font-family: monospace;\">" .
 863              $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=" . $hash}, $co{'tree'}) . "</td></tr>\n";
 864        my $parents  = $co{'parents'};
 865        foreach my $par (@$parents) {
 866                print "<tr><td>parent</td><td style=\"font-family: monospace;\">" .
 867                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$par"}, $par) . "</td></tr>\n";
 868        }
 869        print "</table>". 
 870              "</div>\n";
 871        print "<div class=\"page_body\">\n";
 872        my $comment = $co{'comment'};
 873        my $empty = 0;
 874        my $signed = 0;
 875        foreach my $line (@$comment) {
 876                # print only one empty line
 877                if ($line eq "") {
 878                        if ($empty || $signed) {
 879                                next;
 880                        }
 881                        $empty = 1;
 882                } else {
 883                        $empty = 0;
 884                }
 885                if ($line =~ m/^(signed[ \-]off[\-]by[ :]|acked[\-]by[ \:]|cc[ :])/i) {
 886                        $signed = 1;
 887                        print "<span style=\"color: #888888\">" . escapeHTML($line) . "</span><br/>\n";
 888                } else {
 889                        $signed = 0;
 890                        print escapeHTML($line) . "<br/>\n";
 891                }
 892        }
 893        print "</div>\n";
 894        print "<div class=\"list_head\">\n";
 895        if ($#difftree > 10) {
 896                print(($#difftree + 1) . " files changed:\n");
 897        }
 898        print "</div>\n";
 899        foreach my $line (@difftree) {
 900                # '*100644->100644      blob    9f91a116d91926df3ba936a80f020a6ab1084d2b->bb90a0c3a91eb52020d0db0e8b4f94d30e02d596      net/ipv4/route.c'
 901                # '+100644      blob    4a83ab6cd565d21ab0385bac6643826b83c2fcd4        arch/arm/lib/bitops.h'
 902                # '*100664->100644      blob    b1a8e3dd5556b61dd771d32307c6ee5d7150fa43->b1a8e3dd5556b61dd771d32307c6ee5d7150fa43      show-files.c'
 903                # '*100664->100644      blob    d08e895238bac36d8220586fdc28c27e1a7a76d3->d08e895238bac36d8220586fdc28c27e1a7a76d3      update-cache.c'
 904                $line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/;
 905                my $op = $1;
 906                my $mode = $2;
 907                my $type = $3;
 908                my $id = $4;
 909                my $file = $5;
 910                if ($type eq "blob") {
 911                        if ($op eq "+") {
 912                                my $mode_chng = "";
 913                                if (S_ISREG(oct $mode)) {
 914                                        $mode_chng = sprintf(" with mode: %04o", (oct $mode) & 0777);
 915                                }
 916                                print "<div class=\"list\">\n" .
 917                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"},
 918                                      escapeHTML($file) . " <span style=\"color: #008000;\">[new " . file_type($mode) . $mode_chng . "]</span>") . "\n" .
 919                                      "</div>";
 920                                print "<div class=\"link\">\n" .
 921                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"}, "blob") . "<br/>\n" .
 922                                      "</div>\n";
 923                        } elsif ($op eq "-") {
 924                                print "<div class=\"list\">\n" .
 925                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"},
 926                                      escapeHTML($file) .  " <span style=\"color: #c00000;\">[deleted " . file_type($mode) . "]</span>") . "\n" .
 927                                      "</div>";
 928                                print "<div class=\"link\">\n" .
 929                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"}, "blob") . " | " .
 930                                      $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash;f=$file"}, "history") . "<br/>\n" .
 931                                      "</div>\n";
 932                        } elsif ($op eq "*") {
 933                                $id =~ m/([0-9a-fA-F]+)->([0-9a-fA-F]+)/;
 934                                my $from_id = $1;
 935                                my $to_id = $2;
 936                                $mode =~ m/^([0-7]{6})->([0-7]{6})$/;
 937                                my $from_mode = $1;
 938                                my $to_mode = $2;
 939                                my $mode_chnge = "";
 940                                if ($from_mode != $to_mode) {
 941                                        $mode_chnge = " <span style=\"color: #888888;\">[changed";
 942                                        if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
 943                                                $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
 944                                        }
 945                                        if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
 946                                                if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
 947                                                        $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
 948                                                } elsif (S_ISREG($to_mode)) {
 949                                                        $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
 950                                                }
 951                                        }
 952                                        $mode_chnge .= "]</span>\n";
 953                                }
 954                                print "<div class=\"list\">\n";
 955                                if ($to_id ne $from_id) {
 956                                        print $cgi->a({-href => "$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file"},
 957                                              escapeHTML($file) . $mode_chnge) . "\n" .
 958                                              "</div>\n";
 959                                } else {
 960                                        print $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"},
 961                                              escapeHTML($file) . $mode_chnge) . "\n" .
 962                                              "</div>\n";
 963                                }
 964                                print "<div class=\"link\">\n";
 965                                if ($to_id ne $from_id) {
 966                                        print $cgi->a({-href => "$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file"}, "diff") . " | ";
 967                                }
 968                                print $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"}, "blob") . " | " .
 969                                      $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash;f=$file"}, "history") . "<br/>\n" .
 970                                      "</div>\n";
 971                        }
 972                }
 973        }
 974        git_footer_html();
 975}
 976
 977sub git_blobdiff {
 978        mkdir($gittmp, 0700);
 979        git_header_html();
 980        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
 981                print "<div class=\"page_nav\">\n" .
 982                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base"}, "commit") .
 983                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash_base"}, "commitdiff") .
 984                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash_base"}, "tree");
 985                        if (defined $file_name) {
 986                                print " | " . $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash_base;f=$file_name"}, "history");
 987                        }
 988                print "<br/><br/>\n" .
 989                      "</div>\n";
 990                print "<div>\n" .
 991                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
 992                      "</div>\n";
 993        } else {
 994                print "<div class=\"page_nav\">\n" .
 995                      "<br/><br/></div>\n" .
 996                      "<div class=\"title\">$hash vs $hash_parent</div>\n";
 997        }
 998        if (defined $file_name) {
 999                print "<div class=\"page_path\">\n" .
1000                      "/$file_name\n" .
1001                      "</div>\n";
1002        }
1003        print "<div class=\"page_body\">\n" .
1004              "<pre>\n";
1005        print "<span class=\"diff_info\">blob:" .
1006              $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name"}, $hash_parent) .
1007              " -> blob:" .
1008              $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name"}, $hash) .
1009              "</span>\n";
1010        git_diff_html($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
1011        print "</pre>\n" .
1012              "</div>";
1013        git_footer_html();
1014}
1015
1016sub git_commitdiff {
1017        mkdir($gittmp, 0700);
1018        my %co = git_read_commit($hash);
1019        if (!%co) {
1020                die_error(undef, "Unknown commit object.");
1021        }
1022        open my $fd, "-|", "$gitbin/git-diff-tree -r " . $co{'parent'} . " $hash" || die_error(undef, "Open failed.");
1023        my (@difftree) = map { chomp; $_ } <$fd>;
1024        close $fd || die_error(undef, "Reading diff-tree failed.");
1025
1026        git_header_html();
1027        print "<div class=\"page_nav\">\n" .
1028              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit") . " | \n" .
1029              $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "commitdiff") . " | \n" .
1030              $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" .  $co{'tree'} . ";hb=$hash"}, "tree") . "\n" .
1031              "<br/><br/></div>\n";
1032        print "<div>\n" .
1033              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1034              "</div>\n";
1035        print "<div class=\"page_body\">\n" .
1036              "<pre>\n";
1037        foreach my $line (@difftree) {
1038                # '*100644->100644      blob    8e5f9bbdf4de94a1bc4b4da8cb06677ce0a57716->8da3a306d0c0c070d87048d14a033df02f40a154      Makefile'
1039                $line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/;
1040                my $op = $1;
1041                my $mode = $2;
1042                my $type = $3;
1043                my $id = $4;
1044                my $file = $5;
1045                if ($type eq "blob") {
1046                        if ($op eq "+") {
1047                                print "<span class=\"diff_info\">" .  file_type($mode) . ":" .
1048                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"}, $id) . "(new)" .
1049                                      "</span>\n";
1050                                git_diff_html(undef, "/dev/null", $id, "b/$file");
1051                        } elsif ($op eq "-") {
1052                                print "<span class=\"diff_info\">" . file_type($mode) . ":" .
1053                                      $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id;hb=$hash;f=$file"}, $id) . "(deleted)" .
1054                                      "</span>\n";
1055                                git_diff_html($id, "a/$file", undef, "/dev/null");
1056                        } elsif ($op eq "*") {
1057                                $id =~ m/([0-9a-fA-F]+)->([0-9a-fA-F]+)/;
1058                                my $from_id = $1;
1059                                my $to_id = $2;
1060                                $mode =~ m/([0-7]+)->([0-7]+)/;
1061                                my $from_mode = $1;
1062                                my $to_mode = $2;
1063                                if ($from_id ne $to_id) {
1064                                        print "<span class=\"diff_info\">" .
1065                                              file_type($from_mode) . ":" . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$file"}, $from_id) .
1066                                              " -> " .
1067                                              file_type($to_mode) . ":" . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"}, $to_id);
1068                                        print "</span>\n";
1069                                        git_diff_html($from_id, "a/$file",  $to_id, "b/$file");
1070                                }
1071                        }
1072                }
1073        }
1074        print "</pre><br/>\n";
1075        print "</div>";
1076        git_footer_html();
1077}
1078
1079sub git_history {
1080        if (!defined $hash) {
1081                $hash = git_read_head($project);
1082        }
1083        my %co = git_read_commit($hash);
1084        if (!%co) {
1085                die_error(undef, "Unknown commit object.");
1086        }
1087        git_header_html();
1088        print "<div class=\"page_nav\">\n" .
1089              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit") . " | " .
1090              $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "commitdiff") . " | " .
1091              $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" . $co{'tree'} . ";hb=$hash"}, "tree") .
1092              "<br/><br/>\n" .
1093              "</div>\n";
1094        print "<div>\n" .
1095              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1096              "</div>\n";
1097        print "<div class=\"page_path\">\n" .
1098              "/$file_name<br/>\n";
1099        print "</div>\n";
1100        open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin $file_name";
1101        my $commit;
1102        while (my $line = <$fd>) {
1103                if ($line =~ m/^([0-9a-fA-F]{40}) /){
1104                        $commit = $1;
1105                        next;
1106                }
1107                if ($line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/ && (defined $commit)) {
1108                        my $type = $3;
1109                        my $file = $5;
1110                        if ($file ne $file_name || $type ne "blob") {
1111                                next;
1112                        }
1113                        my %co = git_read_commit($commit);
1114                        if (!%co) {
1115                                next;
1116                        }
1117                        print "<div class=\"list\">\n" .
1118                              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"},
1119                              "<span class=\"log_age\">" . $co{'age_string'} . "</span>" . escapeHTML($co{'title'})) . "\n" .
1120                              "</div>\n";
1121                        print "<div class=\"link\">\n" .
1122                              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") .
1123                              " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=" .  $co{'tree'} . ";hb=$commit"}, "tree") .
1124                              " | " . $cgi->a({-href => "$my_uri?p=$project;a=blob;hb=$commit;f=" . $file}, "blob");
1125                        my $blob = git_get_hash_by_path($hash, $file_name);
1126                        my $blob_parent = git_get_hash_by_path($commit, $file_name);
1127                        if (defined $blob && defined $blob_parent && $blob ne $blob_parent) {
1128                                print " | " . $cgi->a({-href => "$my_uri?p=$project;a=blobdiff;h=$blob;hp=$blob_parent;hb=$commit;f=" . $file}, "diff");
1129                        }
1130                        print "<br/>\n" .
1131                              "</div>\n";
1132                        undef $commit;
1133                }
1134        }
1135        close $fd;
1136        git_footer_html();
1137}