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