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