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