gitweb / gitweb.cgion commit gitweb: use a hash to lookup the sub for an action (8e85cdc)
   1#!/usr/bin/perl
   2
   3# gitweb - simple web interface to track changes in git repositories
   4#
   5# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
   6# (C) 2005, Christian Gierke
   7#
   8# This program is licensed under the GPLv2
   9
  10use strict;
  11use warnings;
  12use CGI qw(:standard :escapeHTML -nosticky);
  13use CGI::Util qw(unescape);
  14use CGI::Carp qw(fatalsToBrowser);
  15use Encode;
  16use Fcntl ':mode';
  17binmode STDOUT, ':utf8';
  18
  19our $cgi = new CGI;
  20our $version = "267";
  21our $my_url = $cgi->url();
  22our $my_uri = $cgi->url(-absolute => 1);
  23our $rss_link = "";
  24
  25# core git executable to use
  26# this can just be "git" if your webserver has a sensible PATH
  27our $GIT = "/usr/bin/git";
  28
  29# absolute fs-path which will be prepended to the project path
  30#our $projectroot = "/pub/scm";
  31our $projectroot = "/home/kay/public_html/pub/scm";
  32
  33# version of the core git binary
  34our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
  35
  36# location for temporary files needed for diffs
  37our $git_temp = "/tmp/gitweb";
  38if (! -d $git_temp) {
  39        mkdir($git_temp, 0700) || die_error("Couldn't mkdir $git_temp");
  40}
  41
  42# target of the home link on top of all pages
  43our $home_link = $my_uri;
  44
  45# name of your site or organization to appear in page titles
  46# replace this with something more descriptive for clearer bookmarks
  47our $site_name = $ENV{'SERVER_NAME'} || "Untitled";
  48
  49# html text to include at home page
  50our $home_text = "indextext.html";
  51
  52# URI of default stylesheet
  53our $stylesheet = "gitweb.css";
  54
  55# source of projects list
  56#our $projects_list = $projectroot;
  57our $projects_list = "index/index.aux";
  58
  59# default blob_plain mimetype and default charset for text/plain blob
  60our $default_blob_plain_mimetype = 'text/plain';
  61our $default_text_plain_charset  = undef;
  62
  63# file to use for guessing MIME types before trying /etc/mime.types
  64# (relative to the current git repository)
  65our $mimetypes_file = undef;
  66
  67# input validation and dispatch
  68our $action = $cgi->param('a');
  69if (defined $action) {
  70        if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
  71                undef $action;
  72                die_error(undef, "Invalid action parameter.");
  73        }
  74        if ($action eq "git-logo.png") {
  75                git_logo();
  76                exit;
  77        } elsif ($action eq "opml") {
  78                git_opml();
  79                exit;
  80        }
  81}
  82
  83our $order = $cgi->param('o');
  84if (defined $order) {
  85        if ($order =~ m/[^0-9a-zA-Z_]/) {
  86                undef $order;
  87                die_error(undef, "Invalid order parameter.");
  88        }
  89}
  90
  91our $project = ($cgi->param('p') || $ENV{'PATH_INFO'});
  92if (defined $project) {
  93        $project =~ s|^/||; $project =~ s|/$||;
  94        $project = validate_input($project);
  95        if (!defined($project)) {
  96                die_error(undef, "Invalid project parameter.");
  97        }
  98        if (!(-d "$projectroot/$project")) {
  99                undef $project;
 100                die_error(undef, "No such directory.");
 101        }
 102        if (!(-e "$projectroot/$project/HEAD")) {
 103                undef $project;
 104                die_error(undef, "No such project.");
 105        }
 106        $rss_link = "<link rel=\"alternate\" title=\"" . esc_param($project) . " log\" href=\"" .
 107                    "$my_uri?" . esc_param("p=$project;a=rss") . "\" type=\"application/rss+xml\"/>";
 108        $ENV{'GIT_DIR'} = "$projectroot/$project";
 109} else {
 110        git_project_list();
 111        exit;
 112}
 113
 114our $file_name = $cgi->param('f');
 115if (defined $file_name) {
 116        $file_name = validate_input($file_name);
 117        if (!defined($file_name)) {
 118                die_error(undef, "Invalid file parameter.");
 119        }
 120}
 121
 122our $hash = $cgi->param('h');
 123if (defined $hash) {
 124        $hash = validate_input($hash);
 125        if (!defined($hash)) {
 126                die_error(undef, "Invalid hash parameter.");
 127        }
 128}
 129
 130our $hash_parent = $cgi->param('hp');
 131if (defined $hash_parent) {
 132        $hash_parent = validate_input($hash_parent);
 133        if (!defined($hash_parent)) {
 134                die_error(undef, "Invalid hash parent parameter.");
 135        }
 136}
 137
 138our $hash_base = $cgi->param('hb');
 139if (defined $hash_base) {
 140        $hash_base = validate_input($hash_base);
 141        if (!defined($hash_base)) {
 142                die_error(undef, "Invalid hash base parameter.");
 143        }
 144}
 145
 146our $page = $cgi->param('pg');
 147if (defined $page) {
 148        if ($page =~ m/[^0-9]$/) {
 149                undef $page;
 150                die_error(undef, "Invalid page parameter.");
 151        }
 152}
 153
 154our $searchtext = $cgi->param('s');
 155if (defined $searchtext) {
 156        if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
 157                undef $searchtext;
 158                die_error(undef, "Invalid search parameter.");
 159        }
 160        $searchtext = quotemeta $searchtext;
 161}
 162
 163# dispatch
 164my %actions = (
 165        "blame" => \&git_blame2,
 166        "blobdiff" => \&git_blobdiff,
 167        "blobdiff_plain" => \&git_blobdiff_plain,
 168        "blob" => \&git_blob,
 169        "blob_plain" => \&git_blob_plain,
 170        "commitdiff" => \&git_commitdiff,
 171        "commitdiff_plain" => \&git_commitdiff_plain,
 172        "commit" => \&git_commit,
 173        "heads" => \&git_heads,
 174        "history" => \&git_history,
 175        "log" => \&git_log,
 176        "rss" => \&git_rss,
 177        "search" => \&git_search,
 178        "shortlog" => \&git_shortlog,
 179        "summary" => \&git_summary,
 180        "tag" => \&git_tag,
 181        "tags" => \&git_tags,
 182        "tree" => \&git_tree,
 183);
 184
 185$action = 'summary' if (!defined($action));
 186if (!defined($actions{$action})) {
 187        undef $action;
 188        die_error(undef, "Unknown action.");
 189}
 190$actions{$action}->();
 191exit;
 192
 193## ======================================================================
 194## validation, quoting/unquoting and escaping
 195
 196sub validate_input {
 197        my $input = shift;
 198
 199        if ($input =~ m/^[0-9a-fA-F]{40}$/) {
 200                return $input;
 201        }
 202        if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
 203                return undef;
 204        }
 205        if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) {
 206                return undef;
 207        }
 208        return $input;
 209}
 210
 211# quote unsafe chars, but keep the slash, even when it's not
 212# correct, but quoted slashes look too horrible in bookmarks
 213sub esc_param {
 214        my $str = shift;
 215        $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
 216        $str =~ s/\+/%2B/g;
 217        $str =~ s/ /\+/g;
 218        return $str;
 219}
 220
 221# replace invalid utf8 character with SUBSTITUTION sequence
 222sub esc_html {
 223        my $str = shift;
 224        $str = decode("utf8", $str, Encode::FB_DEFAULT);
 225        $str = escapeHTML($str);
 226        $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
 227        return $str;
 228}
 229
 230# git may return quoted and escaped filenames
 231sub unquote {
 232        my $str = shift;
 233        if ($str =~ m/^"(.*)"$/) {
 234                $str = $1;
 235                $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
 236        }
 237        return $str;
 238}
 239
 240## ----------------------------------------------------------------------
 241## HTML aware string manipulation
 242
 243sub chop_str {
 244        my $str = shift;
 245        my $len = shift;
 246        my $add_len = shift || 10;
 247
 248        # allow only $len chars, but don't cut a word if it would fit in $add_len
 249        # if it doesn't fit, cut it if it's still longer than the dots we would add
 250        $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
 251        my $body = $1;
 252        my $tail = $2;
 253        if (length($tail) > 4) {
 254                $tail = " ...";
 255                $body =~ s/&[^;]*$//; # remove chopped character entities
 256        }
 257        return "$body$tail";
 258}
 259
 260## ----------------------------------------------------------------------
 261## functions returning short strings
 262
 263# CSS class for given age value (in seconds)
 264sub age_class {
 265        my $age = shift;
 266
 267        if ($age < 60*60*2) {
 268                return "age0";
 269        } elsif ($age < 60*60*24*2) {
 270                return "age1";
 271        } else {
 272                return "age2";
 273        }
 274}
 275
 276# convert age in seconds to "nn units ago" string
 277sub age_string {
 278        my $age = shift;
 279        my $age_str;
 280
 281        if ($age > 60*60*24*365*2) {
 282                $age_str = (int $age/60/60/24/365);
 283                $age_str .= " years ago";
 284        } elsif ($age > 60*60*24*(365/12)*2) {
 285                $age_str = int $age/60/60/24/(365/12);
 286                $age_str .= " months ago";
 287        } elsif ($age > 60*60*24*7*2) {
 288                $age_str = int $age/60/60/24/7;
 289                $age_str .= " weeks ago";
 290        } elsif ($age > 60*60*24*2) {
 291                $age_str = int $age/60/60/24;
 292                $age_str .= " days ago";
 293        } elsif ($age > 60*60*2) {
 294                $age_str = int $age/60/60;
 295                $age_str .= " hours ago";
 296        } elsif ($age > 60*2) {
 297                $age_str = int $age/60;
 298                $age_str .= " min ago";
 299        } elsif ($age > 2) {
 300                $age_str = int $age;
 301                $age_str .= " sec ago";
 302        } else {
 303                $age_str .= " right now";
 304        }
 305        return $age_str;
 306}
 307
 308# convert file mode in octal to symbolic file mode string
 309sub mode_str {
 310        my $mode = oct shift;
 311
 312        if (S_ISDIR($mode & S_IFMT)) {
 313                return 'drwxr-xr-x';
 314        } elsif (S_ISLNK($mode)) {
 315                return 'lrwxrwxrwx';
 316        } elsif (S_ISREG($mode)) {
 317                # git cares only about the executable bit
 318                if ($mode & S_IXUSR) {
 319                        return '-rwxr-xr-x';
 320                } else {
 321                        return '-rw-r--r--';
 322                };
 323        } else {
 324                return '----------';
 325        }
 326}
 327
 328# convert file mode in octal to file type string
 329sub file_type {
 330        my $mode = oct shift;
 331
 332        if (S_ISDIR($mode & S_IFMT)) {
 333                return "directory";
 334        } elsif (S_ISLNK($mode)) {
 335                return "symlink";
 336        } elsif (S_ISREG($mode)) {
 337                return "file";
 338        } else {
 339                return "unknown";
 340        }
 341}
 342
 343## ----------------------------------------------------------------------
 344## functions returning short HTML fragments, or transforming HTML fragments
 345## which don't beling to other sections
 346
 347# format line of commit message or tag comment
 348sub format_log_line_html {
 349        my $line = shift;
 350
 351        $line = esc_html($line);
 352        $line =~ s/ /&nbsp;/g;
 353        if ($line =~ m/([0-9a-fA-F]{40})/) {
 354                my $hash_text = $1;
 355                if (git_get_type($hash_text) eq "commit") {
 356                        my $link = $cgi->a({-class => "text", -href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_text")}, $hash_text);
 357                        $line =~ s/$hash_text/$link/;
 358                }
 359        }
 360        return $line;
 361}
 362
 363# format marker of refs pointing to given object
 364sub git_get_referencing {
 365        my ($refs, $id) = @_;
 366
 367        if (defined $refs->{$id}) {
 368                return ' <span class="tag">' . esc_html($refs->{$id}) . '</span>';
 369        } else {
 370                return "";
 371        }
 372}
 373
 374## ----------------------------------------------------------------------
 375## git utility subroutines, invoking git commands
 376
 377# get HEAD ref of given project as hash
 378sub git_read_head {
 379        my $project = shift;
 380        my $oENV = $ENV{'GIT_DIR'};
 381        my $retval = undef;
 382        $ENV{'GIT_DIR'} = "$projectroot/$project";
 383        if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") {
 384                my $head = <$fd>;
 385                close $fd;
 386                if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
 387                        $retval = $1;
 388                }
 389        }
 390        if (defined $oENV) {
 391                $ENV{'GIT_DIR'} = $oENV;
 392        }
 393        return $retval;
 394}
 395
 396# get type of given object
 397sub git_get_type {
 398        my $hash = shift;
 399
 400        open my $fd, "-|", $GIT, "cat-file", '-t', $hash or return;
 401        my $type = <$fd>;
 402        close $fd or return;
 403        chomp $type;
 404        return $type;
 405}
 406
 407sub git_get_project_config {
 408        my $key = shift;
 409
 410        return unless ($key);
 411        $key =~ s/^gitweb\.//;
 412        return if ($key =~ m/\W/);
 413
 414        my $val = qx($GIT repo-config --get gitweb.$key);
 415        return ($val);
 416}
 417
 418sub git_get_project_config_bool {
 419        my $val = git_get_project_config (@_);
 420        if ($val and $val =~ m/true|yes|on/) {
 421                return (1);
 422        }
 423        return; # implicit false
 424}
 425
 426# get hash of given path at given ref
 427sub git_get_hash_by_path {
 428        my $base = shift;
 429        my $path = shift || return undef;
 430
 431        my $tree = $base;
 432
 433        open my $fd, "-|", $GIT, "ls-tree", $base, "--", $path
 434                or die_error(undef, "Open git-ls-tree failed.");
 435        my $line = <$fd>;
 436        close $fd or return undef;
 437
 438        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 439        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
 440        return $3;
 441}
 442
 443## ......................................................................
 444## git utility functions, directly accessing git repository
 445
 446# assumes that PATH is not symref
 447sub git_read_hash {
 448        my $path = shift;
 449
 450        open my $fd, "$projectroot/$path" or return undef;
 451        my $head = <$fd>;
 452        close $fd;
 453        chomp $head;
 454        if ($head =~ m/^[0-9a-fA-F]{40}$/) {
 455                return $head;
 456        }
 457}
 458
 459sub git_read_description {
 460        my $path = shift;
 461
 462        open my $fd, "$projectroot/$path/description" or return undef;
 463        my $descr = <$fd>;
 464        close $fd;
 465        chomp $descr;
 466        return $descr;
 467}
 468
 469sub git_read_projects {
 470        my @list;
 471
 472        if (-d $projects_list) {
 473                # search in directory
 474                my $dir = $projects_list;
 475                opendir my ($dh), $dir or return undef;
 476                while (my $dir = readdir($dh)) {
 477                        if (-e "$projectroot/$dir/HEAD") {
 478                                my $pr = {
 479                                        path => $dir,
 480                                };
 481                                push @list, $pr
 482                        }
 483                }
 484                closedir($dh);
 485        } elsif (-f $projects_list) {
 486                # read from file(url-encoded):
 487                # 'git%2Fgit.git Linus+Torvalds'
 488                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
 489                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
 490                open my ($fd), $projects_list or return undef;
 491                while (my $line = <$fd>) {
 492                        chomp $line;
 493                        my ($path, $owner) = split ' ', $line;
 494                        $path = unescape($path);
 495                        $owner = unescape($owner);
 496                        if (!defined $path) {
 497                                next;
 498                        }
 499                        if (-e "$projectroot/$path/HEAD") {
 500                                my $pr = {
 501                                        path => $path,
 502                                        owner => decode("utf8", $owner, Encode::FB_DEFAULT),
 503                                };
 504                                push @list, $pr
 505                        }
 506                }
 507                close $fd;
 508        }
 509        @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
 510        return @list;
 511}
 512
 513sub read_info_ref {
 514        my $type = shift || "";
 515        my %refs;
 516        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c      refs/tags/v2.6.11
 517        # c39ae07f393806ccf406ef966e9a15afc43cc36a      refs/tags/v2.6.11^{}
 518        open my $fd, "$projectroot/$project/info/refs" or return;
 519        while (my $line = <$fd>) {
 520                chomp $line;
 521                # attention: for $type == "" it saves only last path part of ref name
 522                # e.g. from 'refs/heads/jn/gitweb' it would leave only 'gitweb'
 523                if ($line =~ m/^([0-9a-fA-F]{40})\t.*$type\/([^\^]+)/) {
 524                        if (defined $refs{$1}) {
 525                                $refs{$1} .= " / $2";
 526                        } else {
 527                                $refs{$1} = $2;
 528                        }
 529                }
 530        }
 531        close $fd or return;
 532        return \%refs;
 533}
 534
 535## ----------------------------------------------------------------------
 536## parse to hash functions
 537
 538sub date_str {
 539        my $epoch = shift;
 540        my $tz = shift || "-0000";
 541
 542        my %date;
 543        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
 544        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
 545        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
 546        $date{'hour'} = $hour;
 547        $date{'minute'} = $min;
 548        $date{'mday'} = $mday;
 549        $date{'day'} = $days[$wday];
 550        $date{'month'} = $months[$mon];
 551        $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
 552        $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
 553
 554        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
 555        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
 556        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
 557        $date{'hour_local'} = $hour;
 558        $date{'minute_local'} = $min;
 559        $date{'tz_local'} = $tz;
 560        return %date;
 561}
 562
 563sub git_read_tag {
 564        my $tag_id = shift;
 565        my %tag;
 566        my @comment;
 567
 568        open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return;
 569        $tag{'id'} = $tag_id;
 570        while (my $line = <$fd>) {
 571                chomp $line;
 572                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
 573                        $tag{'object'} = $1;
 574                } elsif ($line =~ m/^type (.+)$/) {
 575                        $tag{'type'} = $1;
 576                } elsif ($line =~ m/^tag (.+)$/) {
 577                        $tag{'name'} = $1;
 578                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
 579                        $tag{'author'} = $1;
 580                        $tag{'epoch'} = $2;
 581                        $tag{'tz'} = $3;
 582                } elsif ($line =~ m/--BEGIN/) {
 583                        push @comment, $line;
 584                        last;
 585                } elsif ($line eq "") {
 586                        last;
 587                }
 588        }
 589        push @comment, <$fd>;
 590        $tag{'comment'} = \@comment;
 591        close $fd or return;
 592        if (!defined $tag{'name'}) {
 593                return
 594        };
 595        return %tag
 596}
 597
 598sub git_read_commit {
 599        my $commit_id = shift;
 600        my $commit_text = shift;
 601
 602        my @commit_lines;
 603        my %co;
 604
 605        if (defined $commit_text) {
 606                @commit_lines = @$commit_text;
 607        } else {
 608                $/ = "\0";
 609                open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id or return;
 610                @commit_lines = split '\n', <$fd>;
 611                close $fd or return;
 612                $/ = "\n";
 613                pop @commit_lines;
 614        }
 615        my $header = shift @commit_lines;
 616        if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
 617                return;
 618        }
 619        ($co{'id'}, my @parents) = split ' ', $header;
 620        $co{'parents'} = \@parents;
 621        $co{'parent'} = $parents[0];
 622        while (my $line = shift @commit_lines) {
 623                last if $line eq "\n";
 624                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
 625                        $co{'tree'} = $1;
 626                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 627                        $co{'author'} = $1;
 628                        $co{'author_epoch'} = $2;
 629                        $co{'author_tz'} = $3;
 630                        if ($co{'author'} =~ m/^([^<]+) </) {
 631                                $co{'author_name'} = $1;
 632                        } else {
 633                                $co{'author_name'} = $co{'author'};
 634                        }
 635                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
 636                        $co{'committer'} = $1;
 637                        $co{'committer_epoch'} = $2;
 638                        $co{'committer_tz'} = $3;
 639                        $co{'committer_name'} = $co{'committer'};
 640                        $co{'committer_name'} =~ s/ <.*//;
 641                }
 642        }
 643        if (!defined $co{'tree'}) {
 644                return;
 645        };
 646
 647        foreach my $title (@commit_lines) {
 648                $title =~ s/^    //;
 649                if ($title ne "") {
 650                        $co{'title'} = chop_str($title, 80, 5);
 651                        # remove leading stuff of merges to make the interesting part visible
 652                        if (length($title) > 50) {
 653                                $title =~ s/^Automatic //;
 654                                $title =~ s/^merge (of|with) /Merge ... /i;
 655                                if (length($title) > 50) {
 656                                        $title =~ s/(http|rsync):\/\///;
 657                                }
 658                                if (length($title) > 50) {
 659                                        $title =~ s/(master|www|rsync)\.//;
 660                                }
 661                                if (length($title) > 50) {
 662                                        $title =~ s/kernel.org:?//;
 663                                }
 664                                if (length($title) > 50) {
 665                                        $title =~ s/\/pub\/scm//;
 666                                }
 667                        }
 668                        $co{'title_short'} = chop_str($title, 50, 5);
 669                        last;
 670                }
 671        }
 672        # remove added spaces
 673        foreach my $line (@commit_lines) {
 674                $line =~ s/^    //;
 675        }
 676        $co{'comment'} = \@commit_lines;
 677
 678        my $age = time - $co{'committer_epoch'};
 679        $co{'age'} = $age;
 680        $co{'age_string'} = age_string($age);
 681        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
 682        if ($age > 60*60*24*7*2) {
 683                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
 684                $co{'age_string_age'} = $co{'age_string'};
 685        } else {
 686                $co{'age_string_date'} = $co{'age_string'};
 687                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
 688        }
 689        return %co;
 690}
 691
 692## ......................................................................
 693## parse to array of hashes functions
 694
 695sub git_read_refs {
 696        my $ref_dir = shift;
 697        my @reflist;
 698
 699        my @refs;
 700        opendir my $dh, "$projectroot/$project/$ref_dir";
 701        while (my $dir = readdir($dh)) {
 702                if ($dir =~ m/^\./) {
 703                        next;
 704                }
 705                if (-d "$projectroot/$project/$ref_dir/$dir") {
 706                        opendir my $dh2, "$projectroot/$project/$ref_dir/$dir";
 707                        my @subdirs = grep !m/^\./, readdir $dh2;
 708                        closedir($dh2);
 709                        foreach my $subdir (@subdirs) {
 710                                push @refs, "$dir/$subdir"
 711                        }
 712                        next;
 713                }
 714                push @refs, $dir;
 715        }
 716        closedir($dh);
 717        foreach my $ref_file (@refs) {
 718                my $ref_id = git_read_hash("$project/$ref_dir/$ref_file");
 719                my $type = git_get_type($ref_id) || next;
 720                my %ref_item;
 721                my %co;
 722                $ref_item{'type'} = $type;
 723                $ref_item{'id'} = $ref_id;
 724                $ref_item{'epoch'} = 0;
 725                $ref_item{'age'} = "unknown";
 726                if ($type eq "tag") {
 727                        my %tag = git_read_tag($ref_id);
 728                        $ref_item{'comment'} = $tag{'comment'};
 729                        if ($tag{'type'} eq "commit") {
 730                                %co = git_read_commit($tag{'object'});
 731                                $ref_item{'epoch'} = $co{'committer_epoch'};
 732                                $ref_item{'age'} = $co{'age_string'};
 733                        } elsif (defined($tag{'epoch'})) {
 734                                my $age = time - $tag{'epoch'};
 735                                $ref_item{'epoch'} = $tag{'epoch'};
 736                                $ref_item{'age'} = age_string($age);
 737                        }
 738                        $ref_item{'reftype'} = $tag{'type'};
 739                        $ref_item{'name'} = $tag{'name'};
 740                        $ref_item{'refid'} = $tag{'object'};
 741                } elsif ($type eq "commit"){
 742                        %co = git_read_commit($ref_id);
 743                        $ref_item{'reftype'} = "commit";
 744                        $ref_item{'name'} = $ref_file;
 745                        $ref_item{'title'} = $co{'title'};
 746                        $ref_item{'refid'} = $ref_id;
 747                        $ref_item{'epoch'} = $co{'committer_epoch'};
 748                        $ref_item{'age'} = $co{'age_string'};
 749                } else {
 750                        $ref_item{'reftype'} = $type;
 751                        $ref_item{'name'} = $ref_file;
 752                        $ref_item{'refid'} = $ref_id;
 753                }
 754
 755                push @reflist, \%ref_item;
 756        }
 757        # sort tags by age
 758        @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
 759        return \@reflist;
 760}
 761
 762## ----------------------------------------------------------------------
 763## filesystem-related functions
 764
 765sub get_file_owner {
 766        my $path = shift;
 767
 768        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
 769        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
 770        if (!defined $gcos) {
 771                return undef;
 772        }
 773        my $owner = $gcos;
 774        $owner =~ s/[,;].*$//;
 775        return decode("utf8", $owner, Encode::FB_DEFAULT);
 776}
 777
 778## ......................................................................
 779## mimetype related functions
 780
 781sub mimetype_guess_file {
 782        my $filename = shift;
 783        my $mimemap = shift;
 784        -r $mimemap or return undef;
 785
 786        my %mimemap;
 787        open(MIME, $mimemap) or return undef;
 788        while (<MIME>) {
 789                my ($mime, $exts) = split(/\t+/);
 790                my @exts = split(/\s+/, $exts);
 791                foreach my $ext (@exts) {
 792                        $mimemap{$ext} = $mime;
 793                }
 794        }
 795        close(MIME);
 796
 797        $filename =~ /\.(.*?)$/;
 798        return $mimemap{$1};
 799}
 800
 801sub mimetype_guess {
 802        my $filename = shift;
 803        my $mime;
 804        $filename =~ /\./ or return undef;
 805
 806        if ($mimetypes_file) {
 807                my $file = $mimetypes_file;
 808                #$file =~ m#^/# or $file = "$projectroot/$path/$file";
 809                $mime = mimetype_guess_file($filename, $file);
 810        }
 811        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
 812        return $mime;
 813}
 814
 815sub git_blob_plain_mimetype {
 816        my $fd = shift;
 817        my $filename = shift;
 818
 819        if ($filename) {
 820                my $mime = mimetype_guess($filename);
 821                $mime and return $mime;
 822        }
 823
 824        # just in case
 825        return $default_blob_plain_mimetype unless $fd;
 826
 827        if (-T $fd) {
 828                return 'text/plain' .
 829                       ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
 830        } elsif (! $filename) {
 831                return 'application/octet-stream';
 832        } elsif ($filename =~ m/\.png$/i) {
 833                return 'image/png';
 834        } elsif ($filename =~ m/\.gif$/i) {
 835                return 'image/gif';
 836        } elsif ($filename =~ m/\.jpe?g$/i) {
 837                return 'image/jpeg';
 838        } else {
 839                return 'application/octet-stream';
 840        }
 841}
 842
 843## ======================================================================
 844## functions printing HTML: header, footer, error page
 845
 846sub git_header_html {
 847        my $status = shift || "200 OK";
 848        my $expires = shift;
 849
 850        my $title = "$site_name git";
 851        if (defined $project) {
 852                $title .= " - $project";
 853                if (defined $action) {
 854                        $title .= "/$action";
 855                        if (defined $file_name) {
 856                                $title .= " - $file_name";
 857                                if ($action eq "tree" && $file_name !~ m|/$|) {
 858                                        $title .= "/";
 859                                }
 860                        }
 861                }
 862        }
 863        my $content_type;
 864        # require explicit support from the UA if we are to send the page as
 865        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
 866        # we have to do this because MSIE sometimes globs '*/*', pretending to
 867        # support xhtml+xml but choking when it gets what it asked for.
 868        if ($cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && $cgi->Accept('application/xhtml+xml') != 0) {
 869                $content_type = 'application/xhtml+xml';
 870        } else {
 871                $content_type = 'text/html';
 872        }
 873        print $cgi->header(-type=>$content_type, -charset => 'utf-8', -status=> $status, -expires => $expires);
 874        print <<EOF;
 875<?xml version="1.0" encoding="utf-8"?>
 876<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 877<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 878<!-- git web interface v$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
 879<!-- git core binaries version $git_version -->
 880<head>
 881<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
 882<meta name="robots" content="index, nofollow"/>
 883<title>$title</title>
 884<link rel="stylesheet" type="text/css" href="$stylesheet"/>
 885$rss_link
 886</head>
 887<body>
 888EOF
 889        print "<div class=\"page_header\">\n" .
 890              "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
 891              "<img src=\"$my_uri?" . esc_param("a=git-logo.png") . "\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
 892              "</a>\n";
 893        print $cgi->a({-href => esc_param($home_link)}, "projects") . " / ";
 894        if (defined $project) {
 895                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, esc_html($project));
 896                if (defined $action) {
 897                        print " / $action";
 898                }
 899                print "\n";
 900                if (!defined $searchtext) {
 901                        $searchtext = "";
 902                }
 903                my $search_hash;
 904                if (defined $hash_base) {
 905                        $search_hash = $hash_base;
 906                } elsif (defined $hash) {
 907                        $search_hash = $hash;
 908                } else {
 909                        $search_hash = "HEAD";
 910                }
 911                $cgi->param("a", "search");
 912                $cgi->param("h", $search_hash);
 913                print $cgi->startform(-method => "get", -action => $my_uri) .
 914                      "<div class=\"search\">\n" .
 915                      $cgi->hidden(-name => "p") . "\n" .
 916                      $cgi->hidden(-name => "a") . "\n" .
 917                      $cgi->hidden(-name => "h") . "\n" .
 918                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 919                      "</div>" .
 920                      $cgi->end_form() . "\n";
 921        }
 922        print "</div>\n";
 923}
 924
 925sub git_footer_html {
 926        print "<div class=\"page_footer\">\n";
 927        if (defined $project) {
 928                my $descr = git_read_description($project);
 929                if (defined $descr) {
 930                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
 931                }
 932                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=rss"), -class => "rss_logo"}, "RSS") . "\n";
 933        } else {
 934                print $cgi->a({-href => "$my_uri?" . esc_param("a=opml"), -class => "rss_logo"}, "OPML") . "\n";
 935        }
 936        print "</div>\n" .
 937              "</body>\n" .
 938              "</html>";
 939}
 940
 941sub die_error {
 942        my $status = shift || "403 Forbidden";
 943        my $error = shift || "Malformed query, file missing or permission denied";
 944
 945        git_header_html($status);
 946        print "<div class=\"page_body\">\n" .
 947              "<br/><br/>\n" .
 948              "$status - $error\n" .
 949              "<br/>\n" .
 950              "</div>\n";
 951        git_footer_html();
 952        exit;
 953}
 954
 955## ----------------------------------------------------------------------
 956## functions printing or outputting HTML: navigation
 957
 958sub git_page_nav {
 959        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
 960        $extra = '' if !defined $extra; # pager or formats
 961
 962        my @navs = qw(summary shortlog log commit commitdiff tree);
 963        if ($suppress) {
 964                @navs = grep { $_ ne $suppress } @navs;
 965        }
 966
 967        my %arg = map { $_, ''} @navs;
 968        if (defined $head) {
 969                for (qw(commit commitdiff)) {
 970                        $arg{$_} = ";h=$head";
 971                }
 972                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
 973                        for (qw(shortlog log)) {
 974                                $arg{$_} = ";h=$head";
 975                        }
 976                }
 977        }
 978        $arg{tree} .= ";h=$treehead" if defined $treehead;
 979        $arg{tree} .= ";hb=$treebase" if defined $treebase;
 980
 981        print "<div class=\"page_nav\">\n" .
 982                (join " | ",
 983                 map { $_ eq $current
 984                                         ? $_
 985                                         : $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$_$arg{$_}")}, "$_")
 986                                 }
 987                 @navs);
 988        print "<br/>\n$extra<br/>\n" .
 989              "</div>\n";
 990}
 991
 992sub git_get_paging_nav {
 993        my ($action, $hash, $head, $page, $nrevs) = @_;
 994        my $paging_nav;
 995
 996
 997        if ($hash ne $head || $page) {
 998                $paging_nav .= $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$action")}, "HEAD");
 999        } else {
1000                $paging_nav .= "HEAD";
1001        }
1002
1003        if ($page > 0) {
1004                $paging_nav .= " &sdot; " .
1005                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$action;h=$hash;pg=" . ($page-1)),
1006                                                         -accesskey => "p", -title => "Alt-p"}, "prev");
1007        } else {
1008                $paging_nav .= " &sdot; prev";
1009        }
1010
1011        if ($nrevs >= (100 * ($page+1)-1)) {
1012                $paging_nav .= " &sdot; " .
1013                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$action;h=$hash;pg=" . ($page+1)),
1014                                                         -accesskey => "n", -title => "Alt-n"}, "next");
1015        } else {
1016                $paging_nav .= " &sdot; next";
1017        }
1018
1019        return $paging_nav;
1020}
1021
1022## ......................................................................
1023## functions printing or outputting HTML: div
1024
1025sub git_header_div {
1026        my ($action, $title, $hash, $hash_base) = @_;
1027        my $rest = '';
1028
1029        $rest .= ";h=$hash" if $hash;
1030        $rest .= ";hb=$hash_base" if $hash_base;
1031
1032        print "<div class=\"header\">\n" .
1033              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$action$rest"),
1034                       -class => "title"}, $title ? $title : $action) . "\n" .
1035              "</div>\n";
1036}
1037
1038sub git_print_page_path {
1039        my $name = shift;
1040        my $type = shift;
1041
1042        if (!defined $name) {
1043                print "<div class=\"page_path\"><b>/</b></div>\n";
1044        } elsif ($type =~ "blob") {
1045                print "<div class=\"page_path\"><b>" .
1046                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;f=$file_name")}, esc_html($name)) . "</b><br/></div>\n";
1047        } else {
1048                print "<div class=\"page_path\"><b>" . esc_html($name) . "</b><br/></div>\n";
1049        }
1050}
1051
1052## ......................................................................
1053## functions printing large fragments of HTML
1054
1055sub git_shortlog_body {
1056        # uses global variable $project
1057        my ($revlist, $from, $to, $refs, $extra) = @_;
1058        $from = 0 unless defined $from;
1059        $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
1060
1061        print "<table class=\"shortlog\" cellspacing=\"0\">\n";
1062        my $alternate = 0;
1063        for (my $i = $from; $i <= $to; $i++) {
1064                my $commit = $revlist->[$i];
1065                #my $ref = defined $refs ? git_get_referencing($refs, $commit) : '';
1066                my $ref = git_get_referencing($refs, $commit);
1067                my %co = git_read_commit($commit);
1068                my %ad = date_str($co{'author_epoch'});
1069                if ($alternate) {
1070                        print "<tr class=\"dark\">\n";
1071                } else {
1072                        print "<tr class=\"light\">\n";
1073                }
1074                $alternate ^= 1;
1075                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
1076                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1077                      "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
1078                      "<td>";
1079                if (length($co{'title_short'}) < length($co{'title'})) {
1080                        print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"),
1081                                       -class => "list", -title => "$co{'title'}"},
1082                              "<b>" . esc_html($co{'title_short'}) . "$ref</b>");
1083                } else {
1084                        print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"),
1085                                       -class => "list"},
1086                              "<b>" . esc_html($co{'title'}) . "$ref</b>");
1087                }
1088                print "</td>\n" .
1089                      "<td class=\"link\">" .
1090                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") . " | " .
1091                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
1092                      "</td>\n" .
1093                      "</tr>\n";
1094        }
1095        if (defined $extra) {
1096                print "<tr>\n" .
1097                      "<td colspan=\"4\">$extra</td>\n" .
1098                      "</tr>\n";
1099        }
1100        print "</table>\n";
1101}
1102
1103sub git_tags_body {
1104        # uses global variable $project
1105        my ($taglist, $from, $to, $extra) = @_;
1106        $from = 0 unless defined $from;
1107        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
1108
1109        print "<table class=\"tags\" cellspacing=\"0\">\n";
1110        my $alternate = 0;
1111        for (my $i = $from; $i <= $to; $i++) {
1112                my $entry = $taglist->[$i];
1113                my %tag = %$entry;
1114                my $comment_lines = $tag{'comment'};
1115                my $comment = shift @$comment_lines;
1116                my $comment_short;
1117                if (defined $comment) {
1118                        $comment_short = chop_str($comment, 30, 5);
1119                }
1120                if ($alternate) {
1121                        print "<tr class=\"dark\">\n";
1122                } else {
1123                        print "<tr class=\"light\">\n";
1124                }
1125                $alternate ^= 1;
1126                print "<td><i>$tag{'age'}</i></td>\n" .
1127                      "<td>" .
1128                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}"),
1129                               -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") .
1130                      "</td>\n" .
1131                      "<td>";
1132                if (defined $comment) {
1133                        if (length($comment_short) < length($comment)) {
1134                                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}"),
1135                                               -class => "list", -title => $comment}, $comment_short);
1136                        } else {
1137                                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}"),
1138                                               -class => "list"}, $comment);
1139                        }
1140                }
1141                print "</td>\n" .
1142                      "<td class=\"selflink\">";
1143                if ($tag{'type'} eq "tag") {
1144                        print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}")}, "tag");
1145                } else {
1146                        print "&nbsp;";
1147                }
1148                print "</td>\n" .
1149                      "<td class=\"link\">" . " | " .
1150                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}")}, $tag{'reftype'});
1151                if ($tag{'reftype'} eq "commit") {
1152                        print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
1153                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'refid'}")}, "log");
1154                } elsif ($tag{'reftype'} eq "blob") {
1155                        print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$tag{'refid'}")}, "raw");
1156                }
1157                print "</td>\n" .
1158                      "</tr>";
1159        }
1160        if (defined $extra) {
1161                print "<tr>\n" .
1162                      "<td colspan=\"5\">$extra</td>\n" .
1163                      "</tr>\n";
1164        }
1165        print "</table>\n";
1166}
1167
1168sub git_heads_body {
1169        # uses global variable $project
1170        my ($taglist, $head, $from, $to, $extra) = @_;
1171        $from = 0 unless defined $from;
1172        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
1173
1174        print "<table class=\"heads\" cellspacing=\"0\">\n";
1175        my $alternate = 0;
1176        for (my $i = $from; $i <= $to; $i++) {
1177                my $entry = $taglist->[$i];
1178                my %tag = %$entry;
1179                my $curr = $tag{'id'} eq $head;
1180                if ($alternate) {
1181                        print "<tr class=\"dark\">\n";
1182                } else {
1183                        print "<tr class=\"light\">\n";
1184                }
1185                $alternate ^= 1;
1186                print "<td><i>$tag{'age'}</i></td>\n" .
1187                      ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
1188                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}"),
1189                               -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") .
1190                      "</td>\n" .
1191                      "<td class=\"link\">" .
1192                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") . " | " .
1193                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'name'}")}, "log") .
1194                      "</td>\n" .
1195                      "</tr>";
1196        }
1197        if (defined $extra) {
1198                print "<tr>\n" .
1199                      "<td colspan=\"3\">$extra</td>\n" .
1200                      "</tr>\n";
1201        }
1202        print "</table>\n";
1203}
1204
1205## ----------------------------------------------------------------------
1206## functions printing large fragments, format as one of arguments
1207
1208sub git_diff_print {
1209        my $from = shift;
1210        my $from_name = shift;
1211        my $to = shift;
1212        my $to_name = shift;
1213        my $format = shift || "html";
1214
1215        my $from_tmp = "/dev/null";
1216        my $to_tmp = "/dev/null";
1217        my $pid = $$;
1218
1219        # create tmp from-file
1220        if (defined $from) {
1221                $from_tmp = "$git_temp/gitweb_" . $$ . "_from";
1222                open my $fd2, "> $from_tmp";
1223                open my $fd, "-|", $GIT, "cat-file", "blob", $from;
1224                my @file = <$fd>;
1225                print $fd2 @file;
1226                close $fd2;
1227                close $fd;
1228        }
1229
1230        # create tmp to-file
1231        if (defined $to) {
1232                $to_tmp = "$git_temp/gitweb_" . $$ . "_to";
1233                open my $fd2, "> $to_tmp";
1234                open my $fd, "-|", $GIT, "cat-file", "blob", $to;
1235                my @file = <$fd>;
1236                print $fd2 @file;
1237                close $fd2;
1238                close $fd;
1239        }
1240
1241        open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp";
1242        if ($format eq "plain") {
1243                undef $/;
1244                print <$fd>;
1245                $/ = "\n";
1246        } else {
1247                while (my $line = <$fd>) {
1248                        chomp $line;
1249                        my $char = substr($line, 0, 1);
1250                        my $diff_class = "";
1251                        if ($char eq '+') {
1252                                $diff_class = " add";
1253                        } elsif ($char eq "-") {
1254                                $diff_class = " rem";
1255                        } elsif ($char eq "@") {
1256                                $diff_class = " chunk_header";
1257                        } elsif ($char eq "\\") {
1258                                # skip errors
1259                                next;
1260                        }
1261                        while ((my $pos = index($line, "\t")) != -1) {
1262                                if (my $count = (8 - (($pos-1) % 8))) {
1263                                        my $spaces = ' ' x $count;
1264                                        $line =~ s/\t/$spaces/;
1265                                }
1266                        }
1267                        print "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
1268                }
1269        }
1270        close $fd;
1271
1272        if (defined $from) {
1273                unlink($from_tmp);
1274        }
1275        if (defined $to) {
1276                unlink($to_tmp);
1277        }
1278}
1279
1280
1281## ======================================================================
1282## ======================================================================
1283## actions
1284
1285# git-logo (cached in browser for one day)
1286sub git_logo {
1287        binmode STDOUT, ':raw';
1288        print $cgi->header(-type => 'image/png', -expires => '+1d');
1289        # cat git-logo.png | hexdump -e '16/1 " %02x"  "\n"' | sed 's/ /\\x/g'
1290        print   "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" .
1291                "\x00\x00\x00\x48\x00\x00\x00\x1b\x04\x03\x00\x00\x00\x2d\xd9\xd4" .
1292                "\x2d\x00\x00\x00\x18\x50\x4c\x54\x45\xff\xff\xff\x60\x60\x5d\xb0" .
1293                "\xaf\xaa\x00\x80\x00\xce\xcd\xc7\xc0\x00\x00\xe8\xe8\xe6\xf7\xf7" .
1294                "\xf6\x95\x0c\xa7\x47\x00\x00\x00\x73\x49\x44\x41\x54\x28\xcf\x63" .
1295                "\x48\x67\x20\x04\x4a\x5c\x18\x0a\x08\x2a\x62\x53\x61\x20\x02\x08" .
1296                "\x0d\x69\x45\xac\xa1\xa1\x01\x30\x0c\x93\x60\x36\x26\x52\x91\xb1" .
1297                "\x01\x11\xd6\xe1\x55\x64\x6c\x6c\xcc\x6c\x6c\x0c\xa2\x0c\x70\x2a" .
1298                "\x62\x06\x2a\xc1\x62\x1d\xb3\x01\x02\x53\xa4\x08\xe8\x00\x03\x18" .
1299                "\x26\x56\x11\xd4\xe1\x20\x97\x1b\xe0\xb4\x0e\x35\x24\x71\x29\x82" .
1300                "\x99\x30\xb8\x93\x0a\x11\xb9\x45\x88\xc1\x8d\xa0\xa2\x44\x21\x06" .
1301                "\x27\x41\x82\x40\x85\xc1\x45\x89\x20\x70\x01\x00\xa4\x3d\x21\xc5" .
1302                "\x12\x1c\x9a\xfe\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
1303}
1304
1305sub git_project_list {
1306        my @list = git_read_projects();
1307        my @projects;
1308        if (!@list) {
1309                die_error(undef, "No project found.");
1310        }
1311        foreach my $pr (@list) {
1312                my $head = git_read_head($pr->{'path'});
1313                if (!defined $head) {
1314                        next;
1315                }
1316                $ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}";
1317                my %co = git_read_commit($head);
1318                if (!%co) {
1319                        next;
1320                }
1321                $pr->{'commit'} = \%co;
1322                if (!defined $pr->{'descr'}) {
1323                        my $descr = git_read_description($pr->{'path'}) || "";
1324                        $pr->{'descr'} = chop_str($descr, 25, 5);
1325                }
1326                if (!defined $pr->{'owner'}) {
1327                        $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
1328                }
1329                push @projects, $pr;
1330        }
1331        git_header_html();
1332        if (-f $home_text) {
1333                print "<div class=\"index_include\">\n";
1334                open (my $fd, $home_text);
1335                print <$fd>;
1336                close $fd;
1337                print "</div>\n";
1338        }
1339        print "<table class=\"project_list\">\n" .
1340              "<tr>\n";
1341        if (!defined($order) || (defined($order) && ($order eq "project"))) {
1342                @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
1343                print "<th>Project</th>\n";
1344        } else {
1345                print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=project")}, "Project") . "</th>\n";
1346        }
1347        if (defined($order) && ($order eq "descr")) {
1348                @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
1349                print "<th>Description</th>\n";
1350        } else {
1351                print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=descr")}, "Description") . "</th>\n";
1352        }
1353        if (defined($order) && ($order eq "owner")) {
1354                @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
1355                print "<th>Owner</th>\n";
1356        } else {
1357                print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=owner")}, "Owner") . "</th>\n";
1358        }
1359        if (defined($order) && ($order eq "age")) {
1360                @projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
1361                print "<th>Last Change</th>\n";
1362        } else {
1363                print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=age")}, "Last Change") . "</th>\n";
1364        }
1365        print "<th></th>\n" .
1366              "</tr>\n";
1367        my $alternate = 0;
1368        foreach my $pr (@projects) {
1369                if ($alternate) {
1370                        print "<tr class=\"dark\">\n";
1371                } else {
1372                        print "<tr class=\"light\">\n";
1373                }
1374                $alternate ^= 1;
1375                print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary"), -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
1376                      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
1377                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
1378                print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" . $pr->{'commit'}{'age_string'} . "</td>\n" .
1379                      "<td class=\"link\">" .
1380                      $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary")}, "summary") .
1381                      " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=shortlog")}, "shortlog") .
1382                      " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=log")}, "log") .
1383                      "</td>\n" .
1384                      "</tr>\n";
1385        }
1386        print "</table>\n";
1387        git_footer_html();
1388}
1389
1390sub git_summary {
1391        my $descr = git_read_description($project) || "none";
1392        my $head = git_read_head($project);
1393        my %co = git_read_commit($head);
1394        my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
1395
1396        my $owner;
1397        if (-f $projects_list) {
1398                open (my $fd , $projects_list);
1399                while (my $line = <$fd>) {
1400                        chomp $line;
1401                        my ($pr, $ow) = split ' ', $line;
1402                        $pr = unescape($pr);
1403                        $ow = unescape($ow);
1404                        if ($pr eq $project) {
1405                                $owner = decode("utf8", $ow, Encode::FB_DEFAULT);
1406                                last;
1407                        }
1408                }
1409                close $fd;
1410        }
1411        if (!defined $owner) {
1412                $owner = get_file_owner("$projectroot/$project");
1413        }
1414
1415        my $refs = read_info_ref();
1416        git_header_html();
1417        git_page_nav('summary','', $head);
1418
1419        print "<div class=\"title\">&nbsp;</div>\n";
1420        print "<table cellspacing=\"0\">\n" .
1421              "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
1422              "<tr><td>owner</td><td>$owner</td></tr>\n" .
1423              "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n" .
1424              "</table>\n";
1425
1426        open my $fd, "-|", $GIT, "rev-list", "--max-count=17", git_read_head($project)
1427                or die_error(undef, "Open git-rev-list failed.");
1428        my @revlist = map { chomp; $_ } <$fd>;
1429        close $fd;
1430        git_header_div('shortlog');
1431        git_shortlog_body(\@revlist, 0, 15, $refs,
1432                          $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "..."));
1433
1434        my $taglist = git_read_refs("refs/tags");
1435        if (defined @$taglist) {
1436                git_header_div('tags');
1437                git_tags_body($taglist, 0, 15,
1438                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tags")}, "..."));
1439        }
1440
1441        my $headlist = git_read_refs("refs/heads");
1442        if (defined @$headlist) {
1443                git_header_div('heads');
1444                git_heads_body($taglist, $head, 0, 15,
1445                               $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=heads")}, "..."));
1446        }
1447
1448        git_footer_html();
1449}
1450
1451sub git_tag {
1452        my $head = git_read_head($project);
1453        git_header_html();
1454        git_page_nav('','', $head,undef,$head);
1455        my %tag = git_read_tag($hash);
1456        git_header_div('commit', esc_html($tag{'name'}), $hash);
1457        print "<div class=\"title_text\">\n" .
1458              "<table cellspacing=\"0\">\n" .
1459              "<tr>\n" .
1460              "<td>object</td>\n" .
1461              "<td>" . $cgi->a({-class => "list", -href => "$my_uri?" . esc_param("p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'object'}) . "</td>\n" .
1462              "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'type'}) . "</td>\n" .
1463              "</tr>\n";
1464        if (defined($tag{'author'})) {
1465                my %ad = date_str($tag{'epoch'}, $tag{'tz'});
1466                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
1467                print "<tr><td></td><td>" . $ad{'rfc2822'} . sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . "</td></tr>\n";
1468        }
1469        print "</table>\n\n" .
1470              "</div>\n";
1471        print "<div class=\"page_body\">";
1472        my $comment = $tag{'comment'};
1473        foreach my $line (@$comment) {
1474                print esc_html($line) . "<br/>\n";
1475        }
1476        print "</div>\n";
1477        git_footer_html();
1478}
1479
1480sub git_blame2 {
1481        my $fd;
1482        my $ftype;
1483        die_error(undef, "Permission denied.") if (!git_get_project_config_bool ('blame'));
1484        die_error('404 Not Found', "File name not defined") if (!$file_name);
1485        $hash_base ||= git_read_head($project);
1486        die_error(undef, "Reading commit failed") unless ($hash_base);
1487        my %co = git_read_commit($hash_base)
1488                or die_error(undef, "Reading commit failed");
1489        if (!defined $hash) {
1490                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
1491                        or die_error(undef, "Error looking up file");
1492        }
1493        $ftype = git_get_type($hash);
1494        if ($ftype !~ "blob") {
1495                die_error("400 Bad Request", "object is not a blob");
1496        }
1497        open ($fd, "-|", $GIT, "blame", '-l', $file_name, $hash_base)
1498                or die_error(undef, "Open git-blame failed.");
1499        git_header_html();
1500        my $formats_nav =
1501                $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, "blob") .
1502                " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;f=$file_name")}, "head");
1503        git_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1504        git_header_div('commit', esc_html($co{'title'}), $hash_base);
1505        git_print_page_path($file_name, $ftype);
1506        my @rev_color = (qw(light dark));
1507        my $num_colors = scalar(@rev_color);
1508        my $current_color = 0;
1509        my $last_rev;
1510        print "<div class=\"page_body\">\n";
1511        print "<table class=\"blame\">\n";
1512        print "<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n";
1513        while (<$fd>) {
1514                /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
1515                my $full_rev = $1;
1516                my $rev = substr($full_rev, 0, 8);
1517                my $lineno = $2;
1518                my $data = $3;
1519
1520                if (!defined $last_rev) {
1521                        $last_rev = $full_rev;
1522                } elsif ($last_rev ne $full_rev) {
1523                        $last_rev = $full_rev;
1524                        $current_color = ++$current_color % $num_colors;
1525                }
1526                print "<tr class=\"$rev_color[$current_color]\">\n";
1527                print "<td class=\"sha1\">" .
1528                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$full_rev;f=$file_name")}, esc_html($rev)) . "</td>\n";
1529                print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" . esc_html($lineno) . "</a></td>\n";
1530                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
1531                print "</tr>\n";
1532        }
1533        print "</table>\n";
1534        print "</div>";
1535        close $fd or print "Reading blob failed\n";
1536        git_footer_html();
1537}
1538
1539sub git_blame {
1540        my $fd;
1541        die_error('403 Permission denied', "Permission denied.") if (!git_get_project_config_bool ('blame'));
1542        die_error('404 Not Found', "What file will it be, master?") if (!$file_name);
1543        $hash_base ||= git_read_head($project);
1544        die_error(undef, "Reading commit failed.") unless ($hash_base);
1545        my %co = git_read_commit($hash_base)
1546                or die_error(undef, "Reading commit failed.");
1547        if (!defined $hash) {
1548                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
1549                        or die_error(undef, "Error lookup file.");
1550        }
1551        open ($fd, "-|", $GIT, "annotate", '-l', '-t', '-r', $file_name, $hash_base)
1552                or die_error(undef, "Open git-annotate failed.");
1553        git_header_html();
1554        my $formats_nav =
1555                $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, "blob") .
1556                " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;f=$file_name")}, "head");
1557        git_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1558        git_header_div('commit', esc_html($co{'title'}), $hash_base);
1559        git_print_page_path($file_name);
1560        print "<div class=\"page_body\">\n";
1561        print <<HTML;
1562<table class="blame">
1563  <tr>
1564    <th>Commit</th>
1565    <th>Age</th>
1566    <th>Author</th>
1567    <th>Line</th>
1568    <th>Data</th>
1569  </tr>
1570HTML
1571        my @line_class = (qw(light dark));
1572        my $line_class_len = scalar (@line_class);
1573        my $line_class_num = $#line_class;
1574        while (my $line = <$fd>) {
1575                my $long_rev;
1576                my $short_rev;
1577                my $author;
1578                my $time;
1579                my $lineno;
1580                my $data;
1581                my $age;
1582                my $age_str;
1583                my $age_class;
1584
1585                chomp $line;
1586                $line_class_num = ($line_class_num + 1) % $line_class_len;
1587
1588                if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) {
1589                        $long_rev = $1;
1590                        $author   = $2;
1591                        $time     = $3;
1592                        $lineno   = $4;
1593                        $data     = $5;
1594                } else {
1595                        print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
1596                        next;
1597                }
1598                $short_rev  = substr ($long_rev, 0, 8);
1599                $age        = time () - $time;
1600                $age_str    = age_string ($age);
1601                $age_str    =~ s/ /&nbsp;/g;
1602                $age_class  = age_class($age);
1603                $author     = esc_html ($author);
1604                $author     =~ s/ /&nbsp;/g;
1605                # escape tabs
1606                while ((my $pos = index($data, "\t")) != -1) {
1607                        if (my $count = (8 - ($pos % 8))) {
1608                                my $spaces = ' ' x $count;
1609                                $data =~ s/\t/$spaces/;
1610                        }
1611                }
1612                $data = esc_html ($data);
1613
1614                print <<HTML;
1615  <tr class="$line_class[$line_class_num]">
1616    <td class="sha1"><a href="$my_uri?${\esc_param ("p=$project;a=commit;h=$long_rev")}" class="text">$short_rev..</a></td>
1617    <td class="$age_class">$age_str</td>
1618    <td>$author</td>
1619    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
1620    <td class="pre">$data</td>
1621  </tr>
1622HTML
1623        } # while (my $line = <$fd>)
1624        print "</table>\n\n";
1625        close $fd or print "Reading blob failed.\n";
1626        print "</div>";
1627        git_footer_html();
1628}
1629
1630sub git_tags {
1631        my $head = git_read_head($project);
1632        git_header_html();
1633        git_page_nav('','', $head,undef,$head);
1634        git_header_div('summary', $project);
1635
1636        my $taglist = git_read_refs("refs/tags");
1637        if (defined @$taglist) {
1638                git_tags_body($taglist);
1639        }
1640        git_footer_html();
1641}
1642
1643sub git_heads {
1644        my $head = git_read_head($project);
1645        git_header_html();
1646        git_page_nav('','', $head,undef,$head);
1647        git_header_div('summary', $project);
1648
1649        my $taglist = git_read_refs("refs/heads");
1650        my $alternate = 0;
1651        if (defined @$taglist) {
1652                git_heads_body($taglist, $head);
1653        }
1654        git_footer_html();
1655}
1656
1657sub git_blob_plain {
1658        if (!defined $hash) {
1659                if (defined $file_name) {
1660                        my $base = $hash_base || git_read_head($project);
1661                        $hash = git_get_hash_by_path($base, $file_name, "blob")
1662                                or die_error(undef, "Error lookup file.");
1663                } else {
1664                        die_error(undef, "No file name defined.");
1665                }
1666        }
1667        my $type = shift;
1668        open my $fd, "-|", $GIT, "cat-file", "blob", $hash
1669                or die_error("Couldn't cat $file_name, $hash");
1670
1671        $type ||= git_blob_plain_mimetype($fd, $file_name);
1672
1673        # save as filename, even when no $file_name is given
1674        my $save_as = "$hash";
1675        if (defined $file_name) {
1676                $save_as = $file_name;
1677        } elsif ($type =~ m/^text\//) {
1678                $save_as .= '.txt';
1679        }
1680
1681        print $cgi->header(-type => "$type", '-content-disposition' => "inline; filename=\"$save_as\"");
1682        undef $/;
1683        binmode STDOUT, ':raw';
1684        print <$fd>;
1685        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
1686        $/ = "\n";
1687        close $fd;
1688}
1689
1690sub git_blob {
1691        if (!defined $hash) {
1692                if (defined $file_name) {
1693                        my $base = $hash_base || git_read_head($project);
1694                        $hash = git_get_hash_by_path($base, $file_name, "blob")
1695                                or die_error(undef, "Error lookup file.");
1696                } else {
1697                        die_error(undef, "No file name defined.");
1698                }
1699        }
1700        my $have_blame = git_get_project_config_bool ('blame');
1701        open my $fd, "-|", $GIT, "cat-file", "blob", $hash
1702                or die_error(undef, "Couldn't cat $file_name, $hash.");
1703        my $mimetype = git_blob_plain_mimetype($fd, $file_name);
1704        if ($mimetype !~ m/^text\//) {
1705                close $fd;
1706                return git_blob_plain($mimetype);
1707        }
1708        git_header_html();
1709        my $formats_nav = '';
1710        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
1711                if (defined $file_name) {
1712                        if ($have_blame) {
1713                                $formats_nav .= $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$hash;hb=$hash_base;f=$file_name")}, "blame") . " | ";
1714                        }
1715                        $formats_nav .=
1716                                $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") .
1717                                " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=HEAD;f=$file_name")}, "head");
1718                } else {
1719                        $formats_nav .= $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash")}, "plain");
1720                }
1721                git_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1722                git_header_div('commit', esc_html($co{'title'}), $hash_base);
1723        } else {
1724                print "<div class=\"page_nav\">\n" .
1725                      "<br/><br/></div>\n" .
1726                      "<div class=\"title\">$hash</div>\n";
1727        }
1728        git_print_page_path($file_name, "blob");
1729        print "<div class=\"page_body\">\n";
1730        my $nr;
1731        while (my $line = <$fd>) {
1732                chomp $line;
1733                $nr++;
1734                while ((my $pos = index($line, "\t")) != -1) {
1735                        if (my $count = (8 - ($pos % 8))) {
1736                                my $spaces = ' ' x $count;
1737                                $line =~ s/\t/$spaces/;
1738                        }
1739                }
1740                printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n", $nr, $nr, $nr, esc_html($line);
1741        }
1742        close $fd or print "Reading blob failed.\n";
1743        print "</div>";
1744        git_footer_html();
1745}
1746
1747sub git_tree {
1748        if (!defined $hash) {
1749                $hash = git_read_head($project);
1750                if (defined $file_name) {
1751                        my $base = $hash_base || $hash;
1752                        $hash = git_get_hash_by_path($base, $file_name, "tree");
1753                }
1754                if (!defined $hash_base) {
1755                        $hash_base = $hash;
1756                }
1757        }
1758        $/ = "\0";
1759        open my $fd, "-|", $GIT, "ls-tree", '-z', $hash
1760                or die_error(undef, "Open git-ls-tree failed.");
1761        my @entries = map { chomp; $_ } <$fd>;
1762        close $fd or die_error(undef, "Reading tree failed.");
1763        $/ = "\n";
1764
1765        my $refs = read_info_ref();
1766        my $ref = git_get_referencing($refs, $hash_base);
1767        git_header_html();
1768        my $base_key = "";
1769        my $base = "";
1770        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
1771                $base_key = ";hb=$hash_base";
1772                git_page_nav('tree','', $hash_base);
1773                git_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
1774        } else {
1775                print "<div class=\"page_nav\">\n";
1776                print "<br/><br/></div>\n";
1777                print "<div class=\"title\">$hash</div>\n";
1778        }
1779        if (defined $file_name) {
1780                $base = esc_html("$file_name/");
1781        }
1782        git_print_page_path($file_name);
1783        print "<div class=\"page_body\">\n";
1784        print "<table cellspacing=\"0\">\n";
1785        my $alternate = 0;
1786        foreach my $line (@entries) {
1787                #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
1788                $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
1789                my $t_mode = $1;
1790                my $t_type = $2;
1791                my $t_hash = $3;
1792                my $t_name = validate_input($4);
1793                if ($alternate) {
1794                        print "<tr class=\"dark\">\n";
1795                } else {
1796                        print "<tr class=\"light\">\n";
1797                }
1798                $alternate ^= 1;
1799                print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n";
1800                if ($t_type eq "blob") {
1801                        print "<td class=\"list\">" .
1802                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name"), -class => "list"}, esc_html($t_name)) .
1803                              "</td>\n" .
1804                              "<td class=\"link\">" .
1805                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name")}, "blob") .
1806#                             " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$t_hash$base_key;f=$base$t_name")}, "blame") .
1807                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$t_hash;hb=$hash_base;f=$base$t_name")}, "history") .
1808                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$t_hash;f=$base$t_name")}, "raw") .
1809                              "</td>\n";
1810                } elsif ($t_type eq "tree") {
1811                        print "<td class=\"list\">" .
1812                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, esc_html($t_name)) .
1813                              "</td>\n" .
1814                              "<td class=\"link\">" .
1815                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, "tree") .
1816                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;hb=$hash_base;f=$base$t_name")}, "history") .
1817                              "</td>\n";
1818                }
1819                print "</tr>\n";
1820        }
1821        print "</table>\n" .
1822              "</div>";
1823        git_footer_html();
1824}
1825
1826sub git_log {
1827        my $head = git_read_head($project);
1828        if (!defined $hash) {
1829                $hash = $head;
1830        }
1831        if (!defined $page) {
1832                $page = 0;
1833        }
1834        my $refs = read_info_ref();
1835
1836        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
1837        open my $fd, "-|", $GIT, "rev-list", $limit, $hash
1838                or die_error(undef, "Open git-rev-list failed.");
1839        my @revlist = map { chomp; $_ } <$fd>;
1840        close $fd;
1841
1842        my $paging_nav = git_get_paging_nav('log', $hash, $head, $page, $#revlist);
1843
1844        git_header_html();
1845        git_page_nav('log','', $hash,undef,undef, $paging_nav);
1846
1847        if (!@revlist) {
1848                my %co = git_read_commit($hash);
1849
1850                git_header_div('summary', $project);
1851                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
1852        }
1853        for (my $i = ($page * 100); $i <= $#revlist; $i++) {
1854                my $commit = $revlist[$i];
1855                my $ref = git_get_referencing($refs, $commit);
1856                my %co = git_read_commit($commit);
1857                next if !%co;
1858                my %ad = date_str($co{'author_epoch'});
1859                git_header_div('commit',
1860                                                                         "<span class=\"age\">$co{'age_string'}</span>" .
1861                                                                         esc_html($co{'title'}) . $ref,
1862                                                                         $commit);
1863                print "<div class=\"title_text\">\n" .
1864                      "<div class=\"log_link\">\n" .
1865                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
1866                      " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
1867                      "<br/>\n" .
1868                      "</div>\n" .
1869                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
1870                      "</div>\n" .
1871                      "<div class=\"log_body\">\n";
1872                my $comment = $co{'comment'};
1873                my $empty = 0;
1874                foreach my $line (@$comment) {
1875                        if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1876                                next;
1877                        }
1878                        if ($line eq "") {
1879                                if ($empty) {
1880                                        next;
1881                                }
1882                                $empty = 1;
1883                        } else {
1884                                $empty = 0;
1885                        }
1886                        print format_log_line_html($line) . "<br/>\n";
1887                }
1888                if (!$empty) {
1889                        print "<br/>\n";
1890                }
1891                print "</div>\n";
1892        }
1893        git_footer_html();
1894}
1895
1896sub git_commit {
1897        my %co = git_read_commit($hash);
1898        if (!%co) {
1899                die_error(undef, "Unknown commit object.");
1900        }
1901        my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
1902        my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
1903
1904        my $parent = $co{'parent'};
1905        if (!defined $parent) {
1906                $parent = "--root";
1907        }
1908        open my $fd, "-|", $GIT, "diff-tree", '-r', '-M', $parent, $hash
1909                or die_error(undef, "Open git-diff-tree failed.");
1910        my @difftree = map { chomp; $_ } <$fd>;
1911        close $fd or die_error(undef, "Reading git-diff-tree failed.");
1912
1913        # non-textual hash id's can be cached
1914        my $expires;
1915        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
1916                $expires = "+1d";
1917        }
1918        my $refs = read_info_ref();
1919        my $ref = git_get_referencing($refs, $co{'id'});
1920        my $formats_nav = '';
1921        if (defined $file_name && defined $co{'parent'}) {
1922                my $parent = $co{'parent'};
1923                $formats_nav .= $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;hb=$parent;f=$file_name")}, "blame");
1924        }
1925        git_header_html(undef, $expires);
1926        git_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
1927                                                         $hash, $co{'tree'}, $hash,
1928                                                         $formats_nav);
1929
1930        if (defined $co{'parent'}) {
1931                git_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
1932        } else {
1933                git_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
1934        }
1935        print "<div class=\"title_text\">\n" .
1936              "<table cellspacing=\"0\">\n";
1937        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
1938              "<tr>" .
1939              "<td></td><td> $ad{'rfc2822'}";
1940        if ($ad{'hour_local'} < 6) {
1941                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1942        } else {
1943                printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1944        }
1945        print "</td>" .
1946              "</tr>\n";
1947        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
1948        print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
1949        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
1950        print "<tr>" .
1951              "<td>tree</td>" .
1952              "<td class=\"sha1\">" .
1953              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash"), class => "list"}, $co{'tree'}) .
1954              "</td>" .
1955              "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
1956              "</td>" .
1957              "</tr>\n";
1958        my $parents = $co{'parents'};
1959        foreach my $par (@$parents) {
1960                print "<tr>" .
1961                      "<td>parent</td>" .
1962                      "<td class=\"sha1\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$par"), class => "list"}, $par) . "</td>" .
1963                      "<td class=\"link\">" .
1964                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$par")}, "commit") .
1965                      " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash;hp=$par")}, "commitdiff") .
1966                      "</td>" .
1967                      "</tr>\n";
1968        }
1969        print "</table>".
1970              "</div>\n";
1971        print "<div class=\"page_body\">\n";
1972        my $comment = $co{'comment'};
1973        my $empty = 0;
1974        my $signed = 0;
1975        foreach my $line (@$comment) {
1976                # print only one empty line
1977                if ($line eq "") {
1978                        if ($empty || $signed) {
1979                                next;
1980                        }
1981                        $empty = 1;
1982                } else {
1983                        $empty = 0;
1984                }
1985                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1986                        $signed = 1;
1987                        print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
1988                } else {
1989                        $signed = 0;
1990                        print format_log_line_html($line) . "<br/>\n";
1991                }
1992        }
1993        print "</div>\n";
1994        print "<div class=\"list_head\">\n";
1995        if ($#difftree > 10) {
1996                print(($#difftree + 1) . " files changed:\n");
1997        }
1998        print "</div>\n";
1999        print "<table class=\"diff_tree\">\n";
2000        my $alternate = 0;
2001        foreach my $line (@difftree) {
2002                # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
2003                # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
2004                if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
2005                        next;
2006                }
2007                my $from_mode = $1;
2008                my $to_mode = $2;
2009                my $from_id = $3;
2010                my $to_id = $4;
2011                my $status = $5;
2012                my $similarity = $6;
2013                my $file = validate_input(unquote($7));
2014                if ($alternate) {
2015                        print "<tr class=\"dark\">\n";
2016                } else {
2017                        print "<tr class=\"light\">\n";
2018                }
2019                $alternate ^= 1;
2020                if ($status eq "A") {
2021                        my $mode_chng = "";
2022                        if (S_ISREG(oct $to_mode)) {
2023                                $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777);
2024                        }
2025                        print "<td>" .
2026                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file)) . "</td>\n" .
2027                              "<td><span class=\"file_status new\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" .
2028                              "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob") . "</td>\n";
2029                } elsif ($status eq "D") {
2030                        print "<td>" .
2031                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file)) . "</td>\n" .
2032                              "<td><span class=\"file_status deleted\">[deleted " . file_type($from_mode). "]</span></td>\n" .
2033                              "<td class=\"link\">" .
2034                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, "blob") .
2035                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;hb=$hash;f=$file")}, "history") .
2036                              "</td>\n"
2037                } elsif ($status eq "M" || $status eq "T") {
2038                        my $mode_chnge = "";
2039                        if ($from_mode != $to_mode) {
2040                                $mode_chnge = " <span class=\"file_status mode_chnge\">[changed";
2041                                if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
2042                                        $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
2043                                }
2044                                if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
2045                                        if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
2046                                                $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
2047                                        } elsif (S_ISREG($to_mode)) {
2048                                                $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
2049                                        }
2050                                }
2051                                $mode_chnge .= "]</span>\n";
2052                        }
2053                        print "<td>";
2054                        if ($to_id ne $from_id) {
2055                                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file));
2056                        } else {
2057                                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file));
2058                        }
2059                        print "</td>\n" .
2060                              "<td>$mode_chnge</td>\n" .
2061                              "<td class=\"link\">";
2062                        print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob");
2063                        if ($to_id ne $from_id) {
2064                                print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file")}, "diff");
2065                        }
2066                        print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;hb=$hash;f=$file")}, "history") . "\n";
2067                        print "</td>\n";
2068                } elsif ($status eq "R") {
2069                        my ($from_file, $to_file) = split "\t", $file;
2070                        my $mode_chng = "";
2071                        if ($from_mode != $to_mode) {
2072                                $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777);
2073                        }
2074                        print "<td>" .
2075                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file"), -class => "list"}, esc_html($to_file)) . "</td>\n" .
2076                              "<td><span class=\"file_status moved\">[moved from " .
2077                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$from_file"), -class => "list"}, esc_html($from_file)) .
2078                              " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" .
2079                              "<td class=\"link\">" .
2080                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file")}, "blob");
2081                        if ($to_id ne $from_id) {
2082                                print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file")}, "diff");
2083                        }
2084                        print "</td>\n";
2085                }
2086                print "</tr>\n";
2087        }
2088        print "</table>\n";
2089        git_footer_html();
2090}
2091
2092sub git_blobdiff {
2093        mkdir($git_temp, 0700);
2094        git_header_html();
2095        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
2096                my $formats_nav =
2097                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff_plain;h=$hash;hp=$hash_parent")}, "plain");
2098                git_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2099                git_header_div('commit', esc_html($co{'title'}), $hash_base);
2100        } else {
2101                print "<div class=\"page_nav\">\n" .
2102                      "<br/><br/></div>\n" .
2103                      "<div class=\"title\">$hash vs $hash_parent</div>\n";
2104        }
2105        git_print_page_path($file_name, "blob");
2106        print "<div class=\"page_body\">\n" .
2107              "<div class=\"diff_info\">blob:" .
2108              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name")}, $hash_parent) .
2109              " -> blob:" .
2110              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, $hash) .
2111              "</div>\n";
2112        git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
2113        print "</div>";
2114        git_footer_html();
2115}
2116
2117sub git_blobdiff_plain {
2118        mkdir($git_temp, 0700);
2119        print $cgi->header(-type => "text/plain", -charset => 'utf-8');
2120        git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain");
2121}
2122
2123sub git_commitdiff {
2124        mkdir($git_temp, 0700);
2125        my %co = git_read_commit($hash);
2126        if (!%co) {
2127                die_error(undef, "Unknown commit object.");
2128        }
2129        if (!defined $hash_parent) {
2130                $hash_parent = $co{'parent'};
2131        }
2132        open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
2133                or die_error(undef, "Open git-diff-tree failed.");
2134        my @difftree = map { chomp; $_ } <$fd>;
2135        close $fd or die_error(undef, "Reading diff-tree failed.");
2136
2137        # non-textual hash id's can be cached
2138        my $expires;
2139        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2140                $expires = "+1d";
2141        }
2142        my $refs = read_info_ref();
2143        my $ref = git_get_referencing($refs, $co{'id'});
2144        my $formats_nav =
2145                $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff_plain;h=$hash;hp=$hash_parent")}, "plain");
2146        git_header_html(undef, $expires);
2147        git_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
2148        git_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
2149        print "<div class=\"page_body\">\n";
2150        my $comment = $co{'comment'};
2151        my $empty = 0;
2152        my $signed = 0;
2153        my @log = @$comment;
2154        # remove first and empty lines after that
2155        shift @log;
2156        while (defined $log[0] && $log[0] eq "") {
2157                shift @log;
2158        }
2159        foreach my $line (@log) {
2160                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2161                        next;
2162                }
2163                if ($line eq "") {
2164                        if ($empty) {
2165                                next;
2166                        }
2167                        $empty = 1;
2168                } else {
2169                        $empty = 0;
2170                }
2171                print format_log_line_html($line) . "<br/>\n";
2172        }
2173        print "<br/>\n";
2174        foreach my $line (@difftree) {
2175                # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
2176                # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
2177                $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
2178                my $from_mode = $1;
2179                my $to_mode = $2;
2180                my $from_id = $3;
2181                my $to_id = $4;
2182                my $status = $5;
2183                my $file = validate_input(unquote($6));
2184                if ($status eq "A") {
2185                        print "<div class=\"diff_info\">" . file_type($to_mode) . ":" .
2186                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" .
2187                              "</div>\n";
2188                        git_diff_print(undef, "/dev/null", $to_id, "b/$file");
2189                } elsif ($status eq "D") {
2190                        print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
2191                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) . "(deleted)" .
2192                              "</div>\n";
2193                        git_diff_print($from_id, "a/$file", undef, "/dev/null");
2194                } elsif ($status eq "M") {
2195                        if ($from_id ne $to_id) {
2196                                print "<div class=\"diff_info\">" .
2197                                      file_type($from_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) .
2198                                      " -> " .
2199                                      file_type($to_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id);
2200                                print "</div>\n";
2201                                git_diff_print($from_id, "a/$file",  $to_id, "b/$file");
2202                        }
2203                }
2204        }
2205        print "<br/>\n" .
2206              "</div>";
2207        git_footer_html();
2208}
2209
2210sub git_commitdiff_plain {
2211        mkdir($git_temp, 0700);
2212        open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
2213                or die_error(undef, "Open git-diff-tree failed.");
2214        my @difftree = map { chomp; $_ } <$fd>;
2215        close $fd or die_error(undef, "Reading diff-tree failed.");
2216
2217        # try to figure out the next tag after this commit
2218        my $tagname;
2219        my $refs = read_info_ref("tags");
2220        open $fd, "-|", $GIT, "rev-list", "HEAD";
2221        my @commits = map { chomp; $_ } <$fd>;
2222        close $fd;
2223        foreach my $commit (@commits) {
2224                if (defined $refs->{$commit}) {
2225                        $tagname = $refs->{$commit}
2226                }
2227                if ($commit eq $hash) {
2228                        last;
2229                }
2230        }
2231
2232        print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
2233        my %co = git_read_commit($hash);
2234        my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
2235        my $comment = $co{'comment'};
2236        print "From: $co{'author'}\n" .
2237              "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n".
2238              "Subject: $co{'title'}\n";
2239        if (defined $tagname) {
2240                print "X-Git-Tag: $tagname\n";
2241        }
2242        print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" .
2243              "\n";
2244
2245        foreach my $line (@$comment) {;
2246                print "$line\n";
2247        }
2248        print "---\n\n";
2249
2250        foreach my $line (@difftree) {
2251                $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
2252                my $from_id = $3;
2253                my $to_id = $4;
2254                my $status = $5;
2255                my $file = $6;
2256                if ($status eq "A") {
2257                        git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain");
2258                } elsif ($status eq "D") {
2259                        git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain");
2260                } elsif ($status eq "M") {
2261                        git_diff_print($from_id, "a/$file",  $to_id, "b/$file", "plain");
2262                }
2263        }
2264}
2265
2266sub git_history {
2267        if (!defined $hash_base) {
2268                $hash_base = git_read_head($project);
2269        }
2270        my $ftype;
2271        my %co = git_read_commit($hash_base);
2272        if (!%co) {
2273                die_error(undef, "Unknown commit object.");
2274        }
2275        my $refs = read_info_ref();
2276        git_header_html();
2277        git_page_nav('','', $hash_base,$co{'tree'},$hash_base);
2278        git_header_div('commit', esc_html($co{'title'}), $hash_base);
2279        if (!defined $hash && defined $file_name) {
2280                $hash = git_get_hash_by_path($hash_base, $file_name);
2281        }
2282        if (defined $hash) {
2283                $ftype = git_get_type($hash);
2284        }
2285        git_print_page_path($file_name, $ftype);
2286
2287        open my $fd, "-|",
2288                $GIT, "rev-list", "--full-history", $hash_base, "--", $file_name;
2289        print "<table cellspacing=\"0\">\n";
2290        my $alternate = 0;
2291        while (my $line = <$fd>) {
2292                if ($line =~ m/^([0-9a-fA-F]{40})/){
2293                        my $commit = $1;
2294                        my %co = git_read_commit($commit);
2295                        if (!%co) {
2296                                next;
2297                        }
2298                        my $ref = git_get_referencing($refs, $commit);
2299                        if ($alternate) {
2300                                print "<tr class=\"dark\">\n";
2301                        } else {
2302                                print "<tr class=\"light\">\n";
2303                        }
2304                        $alternate ^= 1;
2305                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2306                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
2307                              "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list"}, "<b>" .
2308                              esc_html(chop_str($co{'title'}, 50)) . "$ref</b>") . "</td>\n" .
2309                              "<td class=\"link\">" .
2310                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
2311                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
2312                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=$commit;f=$file_name")}, "blob");
2313                        my $blob = git_get_hash_by_path($hash_base, $file_name);
2314                        my $blob_parent = git_get_hash_by_path($commit, $file_name);
2315                        if (defined $blob && defined $blob_parent && $blob ne $blob_parent) {
2316                                print " | " .
2317                                $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$blob;hp=$blob_parent;hb=$commit;f=$file_name")},
2318                                "diff to current");
2319                        }
2320                        print "</td>\n" .
2321                              "</tr>\n";
2322                }
2323        }
2324        print "</table>\n";
2325        close $fd;
2326        git_footer_html();
2327}
2328
2329sub git_search {
2330        if (!defined $searchtext) {
2331                die_error("", "Text field empty.");
2332        }
2333        if (!defined $hash) {
2334                $hash = git_read_head($project);
2335        }
2336        my %co = git_read_commit($hash);
2337        if (!%co) {
2338                die_error(undef, "Unknown commit object.");
2339        }
2340        # pickaxe may take all resources of your box and run for several minutes
2341        # with every query - so decide by yourself how public you make this feature :)
2342        my $commit_search = 1;
2343        my $author_search = 0;
2344        my $committer_search = 0;
2345        my $pickaxe_search = 0;
2346        if ($searchtext =~ s/^author\\://i) {
2347                $author_search = 1;
2348        } elsif ($searchtext =~ s/^committer\\://i) {
2349                $committer_search = 1;
2350        } elsif ($searchtext =~ s/^pickaxe\\://i) {
2351                $commit_search = 0;
2352                $pickaxe_search = 1;
2353        }
2354        git_header_html();
2355        git_page_nav('','', $hash,$co{'tree'},$hash);
2356        git_header_div('commit', esc_html($co{'title'}), $hash);
2357
2358        print "<table cellspacing=\"0\">\n";
2359        my $alternate = 0;
2360        if ($commit_search) {
2361                $/ = "\0";
2362                open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", $hash or next;
2363                while (my $commit_text = <$fd>) {
2364                        if (!grep m/$searchtext/i, $commit_text) {
2365                                next;
2366                        }
2367                        if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
2368                                next;
2369                        }
2370                        if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
2371                                next;
2372                        }
2373                        my @commit_lines = split "\n", $commit_text;
2374                        my %co = git_read_commit(undef, \@commit_lines);
2375                        if (!%co) {
2376                                next;
2377                        }
2378                        if ($alternate) {
2379                                print "<tr class=\"dark\">\n";
2380                        } else {
2381                                print "<tr class=\"light\">\n";
2382                        }
2383                        $alternate ^= 1;
2384                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2385                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
2386                              "<td>" .
2387                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
2388                        my $comment = $co{'comment'};
2389                        foreach my $line (@$comment) {
2390                                if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
2391                                        my $lead = esc_html($1) || "";
2392                                        $lead = chop_str($lead, 30, 10);
2393                                        my $match = esc_html($2) || "";
2394                                        my $trail = esc_html($3) || "";
2395                                        $trail = chop_str($trail, 30, 10);
2396                                        my $text = "$lead<span class=\"match\">$match</span>$trail";
2397                                        print chop_str($text, 80, 5) . "<br/>\n";
2398                                }
2399                        }
2400                        print "</td>\n" .
2401                              "<td class=\"link\">" .
2402                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
2403                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
2404                        print "</td>\n" .
2405                              "</tr>\n";
2406                }
2407                close $fd;
2408        }
2409
2410        if ($pickaxe_search) {
2411                $/ = "\n";
2412                open my $fd, "-|", "$GIT rev-list $hash | $GIT diff-tree -r --stdin -S\'$searchtext\'";
2413                undef %co;
2414                my @files;
2415                while (my $line = <$fd>) {
2416                        if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
2417                                my %set;
2418                                $set{'file'} = $6;
2419                                $set{'from_id'} = $3;
2420                                $set{'to_id'} = $4;
2421                                $set{'id'} = $set{'to_id'};
2422                                if ($set{'id'} =~ m/0{40}/) {
2423                                        $set{'id'} = $set{'from_id'};
2424                                }
2425                                if ($set{'id'} =~ m/0{40}/) {
2426                                        next;
2427                                }
2428                                push @files, \%set;
2429                        } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
2430                                if (%co) {
2431                                        if ($alternate) {
2432                                                print "<tr class=\"dark\">\n";
2433                                        } else {
2434                                                print "<tr class=\"light\">\n";
2435                                        }
2436                                        $alternate ^= 1;
2437                                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2438                                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
2439                                              "<td>" .
2440                                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" .
2441                                              esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
2442                                        while (my $setref = shift @files) {
2443                                                my %set = %$setref;
2444                                                print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$set{'id'};hb=$co{'id'};f=$set{'file'}"), class => "list"},
2445                                                      "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
2446                                                      "<br/>\n";
2447                                        }
2448                                        print "</td>\n" .
2449                                              "<td class=\"link\">" .
2450                                              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
2451                                              " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
2452                                        print "</td>\n" .
2453                                              "</tr>\n";
2454                                }
2455                                %co = git_read_commit($1);
2456                        }
2457                }
2458                close $fd;
2459        }
2460        print "</table>\n";
2461        git_footer_html();
2462}
2463
2464sub git_shortlog {
2465        my $head = git_read_head($project);
2466        if (!defined $hash) {
2467                $hash = $head;
2468        }
2469        if (!defined $page) {
2470                $page = 0;
2471        }
2472        my $refs = read_info_ref();
2473
2474        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
2475        open my $fd, "-|", $GIT, "rev-list", $limit, $hash
2476                or die_error(undef, "Open git-rev-list failed.");
2477        my @revlist = map { chomp; $_ } <$fd>;
2478        close $fd;
2479
2480        my $paging_nav = git_get_paging_nav('shortlog', $hash, $head, $page, $#revlist);
2481        my $next_link = '';
2482        if ($#revlist >= (100 * ($page+1)-1)) {
2483                $next_link =
2484                        $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page+1)),
2485                                 -title => "Alt-n"}, "next");
2486        }
2487
2488
2489        git_header_html();
2490        git_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
2491        git_header_div('summary', $project);
2492
2493        git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
2494
2495        git_footer_html();
2496}
2497
2498## ......................................................................
2499## feeds (RSS, OPML)
2500
2501sub git_rss {
2502        # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
2503        open my $fd, "-|", $GIT, "rev-list", "--max-count=150", git_read_head($project)
2504                or die_error(undef, "Open git-rev-list failed.");
2505        my @revlist = map { chomp; $_ } <$fd>;
2506        close $fd or die_error(undef, "Reading rev-list failed.");
2507        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
2508        print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
2509              "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n";
2510        print "<channel>\n";
2511        print "<title>$project</title>\n".
2512              "<link>" . esc_html("$my_url?p=$project;a=summary") . "</link>\n".
2513              "<description>$project log</description>\n".
2514              "<language>en</language>\n";
2515
2516        for (my $i = 0; $i <= $#revlist; $i++) {
2517                my $commit = $revlist[$i];
2518                my %co = git_read_commit($commit);
2519                # we read 150, we always show 30 and the ones more recent than 48 hours
2520                if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
2521                        last;
2522                }
2523                my %cd = date_str($co{'committer_epoch'});
2524                open $fd, "-|", $GIT, "diff-tree", '-r', $co{'parent'}, $co{'id'} or next;
2525                my @difftree = map { chomp; $_ } <$fd>;
2526                close $fd or next;
2527                print "<item>\n" .
2528                      "<title>" .
2529                      sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
2530                      "</title>\n" .
2531                      "<author>" . esc_html($co{'author'}) . "</author>\n" .
2532                      "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
2533                      "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
2534                      "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
2535                      "<description>" . esc_html($co{'title'}) . "</description>\n" .
2536                      "<content:encoded>" .
2537                      "<![CDATA[\n";
2538                my $comment = $co{'comment'};
2539                foreach my $line (@$comment) {
2540                        $line = decode("utf8", $line, Encode::FB_DEFAULT);
2541                        print "$line<br/>\n";
2542                }
2543                print "<br/>\n";
2544                foreach my $line (@difftree) {
2545                        if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
2546                                next;
2547                        }
2548                        my $file = validate_input(unquote($7));
2549                        $file = decode("utf8", $file, Encode::FB_DEFAULT);
2550                        print "$file<br/>\n";
2551                }
2552                print "]]>\n" .
2553                      "</content:encoded>\n" .
2554                      "</item>\n";
2555        }
2556        print "</channel></rss>";
2557}
2558
2559sub git_opml {
2560        my @list = git_read_projects();
2561
2562        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
2563        print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
2564              "<opml version=\"1.0\">\n".
2565              "<head>".
2566              "  <title>$site_name Git OPML Export</title>\n".
2567              "</head>\n".
2568              "<body>\n".
2569              "<outline text=\"git RSS feeds\">\n";
2570
2571        foreach my $pr (@list) {
2572                my %proj = %$pr;
2573                my $head = git_read_head($proj{'path'});
2574                if (!defined $head) {
2575                        next;
2576                }
2577                $ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}";
2578                my %co = git_read_commit($head);
2579                if (!%co) {
2580                        next;
2581                }
2582
2583                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
2584                my $rss  = "$my_url?p=$proj{'path'};a=rss";
2585                my $html = "$my_url?p=$proj{'path'};a=summary";
2586                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
2587        }
2588        print "</outline>\n".
2589              "</body>\n".
2590              "</opml>\n";
2591}