gitweb / gitweb.perlon commit Merge branch 'jc/archive' (4d69065)
   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();
  18use File::Basename qw(basename);
  19binmode STDOUT, ':utf8';
  20
  21our $cgi = new CGI;
  22our $version = "++GIT_VERSION++";
  23our $my_url = $cgi->url();
  24our $my_uri = $cgi->url(-absolute => 1);
  25
  26# core git executable to use
  27# this can just be "git" if your webserver has a sensible PATH
  28our $GIT = "++GIT_BINDIR++/git";
  29
  30# absolute fs-path which will be prepended to the project path
  31#our $projectroot = "/pub/scm";
  32our $projectroot = "++GITWEB_PROJECTROOT++";
  33
  34# target of the home link on top of all pages
  35our $home_link = $my_uri || "/";
  36
  37# string of the home link on top of all pages
  38our $home_link_str = "++GITWEB_HOME_LINK_STR++";
  39
  40# name of your site or organization to appear in page titles
  41# replace this with something more descriptive for clearer bookmarks
  42our $site_name = "++GITWEB_SITENAME++" || $ENV{'SERVER_NAME'} || "Untitled";
  43
  44# html text to include at home page
  45our $home_text = "++GITWEB_HOMETEXT++";
  46
  47# URI of default stylesheet
  48our $stylesheet = "++GITWEB_CSS++";
  49# URI of GIT logo
  50our $logo = "++GITWEB_LOGO++";
  51# URI of GIT favicon, assumed to be image/png type
  52our $favicon = "++GITWEB_FAVICON++";
  53
  54# source of projects list
  55our $projects_list = "++GITWEB_LIST++";
  56
  57# show repository only if this file exists
  58# (only effective if this variable evaluates to true)
  59our $export_ok = "++GITWEB_EXPORT_OK++";
  60
  61# only allow viewing of repositories also shown on the overview page
  62our $strict_export = "++GITWEB_STRICT_EXPORT++";
  63
  64# list of git base URLs used for URL to where fetch project from,
  65# i.e. full URL is "$git_base_url/$project"
  66our @git_base_url_list = ("++GITWEB_BASE_URL++");
  67
  68# default blob_plain mimetype and default charset for text/plain blob
  69our $default_blob_plain_mimetype = 'text/plain';
  70our $default_text_plain_charset  = undef;
  71
  72# file to use for guessing MIME types before trying /etc/mime.types
  73# (relative to the current git repository)
  74our $mimetypes_file = undef;
  75
  76# You define site-wide feature defaults here; override them with
  77# $GITWEB_CONFIG as necessary.
  78our %feature = (
  79        # feature => {
  80        #       'sub' => feature-sub (subroutine),
  81        #       'override' => allow-override (boolean),
  82        #       'default' => [ default options...] (array reference)}
  83        #
  84        # if feature is overridable (it means that allow-override has true value,
  85        # then feature-sub will be called with default options as parameters;
  86        # return value of feature-sub indicates if to enable specified feature
  87        #
  88        # use gitweb_check_feature(<feature>) to check if <feature> is enabled
  89
  90        'blame' => {
  91                'sub' => \&feature_blame,
  92                'override' => 0,
  93                'default' => [0]},
  94
  95        'snapshot' => {
  96                'sub' => \&feature_snapshot,
  97                'override' => 0,
  98                #         => [content-encoding, suffix, program]
  99                'default' => ['x-gzip', 'gz', 'gzip']},
 100
 101        'pickaxe' => {
 102                'sub' => \&feature_pickaxe,
 103                'override' => 0,
 104                'default' => [1]},
 105);
 106
 107sub gitweb_check_feature {
 108        my ($name) = @_;
 109        return undef unless exists $feature{$name};
 110        my ($sub, $override, @defaults) = (
 111                $feature{$name}{'sub'},
 112                $feature{$name}{'override'},
 113                @{$feature{$name}{'default'}});
 114        if (!$override) { return @defaults; }
 115        return $sub->(@defaults);
 116}
 117
 118# To enable system wide have in $GITWEB_CONFIG
 119# $feature{'blame'}{'default'} = [1];
 120# To have project specific config enable override in $GITWEB_CONFIG
 121# $feature{'blame'}{'override'} = 1;
 122# and in project config gitweb.blame = 0|1;
 123
 124sub feature_blame {
 125        my ($val) = git_get_project_config('blame', '--bool');
 126
 127        if ($val eq 'true') {
 128                return 1;
 129        } elsif ($val eq 'false') {
 130                return 0;
 131        }
 132
 133        return $_[0];
 134}
 135
 136# To disable system wide have in $GITWEB_CONFIG
 137# $feature{'snapshot'}{'default'} = [undef];
 138# To have project specific config enable override in $GITWEB_CONFIG
 139# $feature{'blame'}{'override'} = 1;
 140# and in project config  gitweb.snapshot = none|gzip|bzip2
 141
 142sub feature_snapshot {
 143        my ($ctype, $suffix, $command) = @_;
 144
 145        my ($val) = git_get_project_config('snapshot');
 146
 147        if ($val eq 'gzip') {
 148                return ('x-gzip', 'gz', 'gzip');
 149        } elsif ($val eq 'bzip2') {
 150                return ('x-bzip2', 'bz2', 'bzip2');
 151        } elsif ($val eq 'none') {
 152                return ();
 153        }
 154
 155        return ($ctype, $suffix, $command);
 156}
 157
 158# To enable system wide have in $GITWEB_CONFIG
 159# $feature{'pickaxe'}{'default'} = [1];
 160# To have project specific config enable override in $GITWEB_CONFIG
 161# $feature{'pickaxe'}{'override'} = 1;
 162# and in project config gitweb.pickaxe = 0|1;
 163
 164sub feature_pickaxe {
 165        my ($val) = git_get_project_config('pickaxe', '--bool');
 166
 167        if ($val eq 'true') {
 168                return (1);
 169        } elsif ($val eq 'false') {
 170                return (0);
 171        }
 172
 173        return ($_[0]);
 174}
 175
 176# rename detection options for git-diff and git-diff-tree
 177# - default is '-M', with the cost proportional to
 178#   (number of removed files) * (number of new files).
 179# - more costly is '-C' (or '-C', '-M'), with the cost proportional to
 180#   (number of changed files + number of removed files) * (number of new files)
 181# - even more costly is '-C', '--find-copies-harder' with cost
 182#   (number of files in the original tree) * (number of new files)
 183# - one might want to include '-B' option, e.g. '-B', '-M'
 184our @diff_opts = ('-M'); # taken from git_commit
 185
 186our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
 187do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
 188
 189# version of the core git binary
 190our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
 191
 192# path to the current git repository
 193our $git_dir;
 194
 195$projects_list ||= $projectroot;
 196
 197# ======================================================================
 198# input validation and dispatch
 199our $action = $cgi->param('a');
 200if (defined $action) {
 201        if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
 202                die_error(undef, "Invalid action parameter");
 203        }
 204}
 205
 206our $project = $cgi->param('p');
 207if (defined $project) {
 208        if (!validate_input($project) ||
 209            !(-d "$projectroot/$project") ||
 210            !(-e "$projectroot/$project/HEAD") ||
 211            ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
 212            ($strict_export && !project_in_list($project))) {
 213                undef $project;
 214                die_error(undef, "No such project");
 215        }
 216}
 217
 218our $file_name = $cgi->param('f');
 219if (defined $file_name) {
 220        if (!validate_input($file_name)) {
 221                die_error(undef, "Invalid file parameter");
 222        }
 223}
 224
 225our $file_parent = $cgi->param('fp');
 226if (defined $file_parent) {
 227        if (!validate_input($file_parent)) {
 228                die_error(undef, "Invalid file parent parameter");
 229        }
 230}
 231
 232our $hash = $cgi->param('h');
 233if (defined $hash) {
 234        if (!validate_input($hash)) {
 235                die_error(undef, "Invalid hash parameter");
 236        }
 237}
 238
 239our $hash_parent = $cgi->param('hp');
 240if (defined $hash_parent) {
 241        if (!validate_input($hash_parent)) {
 242                die_error(undef, "Invalid hash parent parameter");
 243        }
 244}
 245
 246our $hash_base = $cgi->param('hb');
 247if (defined $hash_base) {
 248        if (!validate_input($hash_base)) {
 249                die_error(undef, "Invalid hash base parameter");
 250        }
 251}
 252
 253our $hash_parent_base = $cgi->param('hpb');
 254if (defined $hash_parent_base) {
 255        if (!validate_input($hash_parent_base)) {
 256                die_error(undef, "Invalid hash parent base parameter");
 257        }
 258}
 259
 260our $page = $cgi->param('pg');
 261if (defined $page) {
 262        if ($page =~ m/[^0-9]$/) {
 263                die_error(undef, "Invalid page parameter");
 264        }
 265}
 266
 267our $searchtext = $cgi->param('s');
 268if (defined $searchtext) {
 269        if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
 270                die_error(undef, "Invalid search parameter");
 271        }
 272        $searchtext = quotemeta $searchtext;
 273}
 274
 275# now read PATH_INFO and use it as alternative to parameters
 276our $path_info = $ENV{"PATH_INFO"};
 277$path_info =~ s|^/||;
 278$path_info =~ s|/$||;
 279if (validate_input($path_info) && !defined $project) {
 280        $project = $path_info;
 281        while ($project && !-e "$projectroot/$project/HEAD") {
 282                $project =~ s,/*[^/]*$,,;
 283        }
 284        if (defined $project) {
 285                $project = undef unless $project;
 286        }
 287        if ($path_info =~ m,^$project/([^/]+)/(.+)$,) {
 288                # we got "project.git/branch/filename"
 289                $action    ||= "blob_plain";
 290                $hash_base ||= $1;
 291                $file_name ||= $2;
 292        } elsif ($path_info =~ m,^$project/([^/]+)$,) {
 293                # we got "project.git/branch"
 294                $action ||= "shortlog";
 295                $hash   ||= $1;
 296        }
 297}
 298
 299$git_dir = "$projectroot/$project";
 300
 301# dispatch
 302my %actions = (
 303        "blame" => \&git_blame2,
 304        "blobdiff" => \&git_blobdiff,
 305        "blobdiff_plain" => \&git_blobdiff_plain,
 306        "blob" => \&git_blob,
 307        "blob_plain" => \&git_blob_plain,
 308        "commitdiff" => \&git_commitdiff,
 309        "commitdiff_plain" => \&git_commitdiff_plain,
 310        "commit" => \&git_commit,
 311        "heads" => \&git_heads,
 312        "history" => \&git_history,
 313        "log" => \&git_log,
 314        "rss" => \&git_rss,
 315        "search" => \&git_search,
 316        "shortlog" => \&git_shortlog,
 317        "summary" => \&git_summary,
 318        "tag" => \&git_tag,
 319        "tags" => \&git_tags,
 320        "tree" => \&git_tree,
 321        "snapshot" => \&git_snapshot,
 322        # those below don't need $project
 323        "opml" => \&git_opml,
 324        "project_list" => \&git_project_list,
 325        "project_index" => \&git_project_index,
 326);
 327
 328if (defined $project) {
 329        $action ||= 'summary';
 330} else {
 331        $action ||= 'project_list';
 332}
 333if (!defined($actions{$action})) {
 334        die_error(undef, "Unknown action");
 335}
 336$actions{$action}->();
 337exit;
 338
 339## ======================================================================
 340## action links
 341
 342sub href(%) {
 343        my %params = @_;
 344
 345        my @mapping = (
 346                project => "p",
 347                action => "a",
 348                file_name => "f",
 349                file_parent => "fp",
 350                hash => "h",
 351                hash_parent => "hp",
 352                hash_base => "hb",
 353                hash_parent_base => "hpb",
 354                page => "pg",
 355                order => "o",
 356                searchtext => "s",
 357        );
 358        my %mapping = @mapping;
 359
 360        $params{'project'} = $project unless exists $params{'project'};
 361
 362        my @result = ();
 363        for (my $i = 0; $i < @mapping; $i += 2) {
 364                my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
 365                if (defined $params{$name}) {
 366                        push @result, $symbol . "=" . esc_param($params{$name});
 367                }
 368        }
 369        return "$my_uri?" . join(';', @result);
 370}
 371
 372
 373## ======================================================================
 374## validation, quoting/unquoting and escaping
 375
 376sub validate_input {
 377        my $input = shift;
 378
 379        if ($input =~ m/^[0-9a-fA-F]{40}$/) {
 380                return $input;
 381        }
 382        if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
 383                return undef;
 384        }
 385        if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) {
 386                return undef;
 387        }
 388        return $input;
 389}
 390
 391# quote unsafe chars, but keep the slash, even when it's not
 392# correct, but quoted slashes look too horrible in bookmarks
 393sub esc_param {
 394        my $str = shift;
 395        $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
 396        $str =~ s/\+/%2B/g;
 397        $str =~ s/ /\+/g;
 398        return $str;
 399}
 400
 401# replace invalid utf8 character with SUBSTITUTION sequence
 402sub esc_html {
 403        my $str = shift;
 404        $str = decode("utf8", $str, Encode::FB_DEFAULT);
 405        $str = escapeHTML($str);
 406        $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
 407        return $str;
 408}
 409
 410# git may return quoted and escaped filenames
 411sub unquote {
 412        my $str = shift;
 413        if ($str =~ m/^"(.*)"$/) {
 414                $str = $1;
 415                $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
 416        }
 417        return $str;
 418}
 419
 420# escape tabs (convert tabs to spaces)
 421sub untabify {
 422        my $line = shift;
 423
 424        while ((my $pos = index($line, "\t")) != -1) {
 425                if (my $count = (8 - ($pos % 8))) {
 426                        my $spaces = ' ' x $count;
 427                        $line =~ s/\t/$spaces/;
 428                }
 429        }
 430
 431        return $line;
 432}
 433
 434sub project_in_list {
 435        my $project = shift;
 436        my @list = git_get_projects_list();
 437        return @list && scalar(grep { $_->{'path'} eq $project } @list);
 438}
 439
 440## ----------------------------------------------------------------------
 441## HTML aware string manipulation
 442
 443sub chop_str {
 444        my $str = shift;
 445        my $len = shift;
 446        my $add_len = shift || 10;
 447
 448        # allow only $len chars, but don't cut a word if it would fit in $add_len
 449        # if it doesn't fit, cut it if it's still longer than the dots we would add
 450        $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
 451        my $body = $1;
 452        my $tail = $2;
 453        if (length($tail) > 4) {
 454                $tail = " ...";
 455                $body =~ s/&[^;]*$//; # remove chopped character entities
 456        }
 457        return "$body$tail";
 458}
 459
 460## ----------------------------------------------------------------------
 461## functions returning short strings
 462
 463# CSS class for given age value (in seconds)
 464sub age_class {
 465        my $age = shift;
 466
 467        if ($age < 60*60*2) {
 468                return "age0";
 469        } elsif ($age < 60*60*24*2) {
 470                return "age1";
 471        } else {
 472                return "age2";
 473        }
 474}
 475
 476# convert age in seconds to "nn units ago" string
 477sub age_string {
 478        my $age = shift;
 479        my $age_str;
 480
 481        if ($age > 60*60*24*365*2) {
 482                $age_str = (int $age/60/60/24/365);
 483                $age_str .= " years ago";
 484        } elsif ($age > 60*60*24*(365/12)*2) {
 485                $age_str = int $age/60/60/24/(365/12);
 486                $age_str .= " months ago";
 487        } elsif ($age > 60*60*24*7*2) {
 488                $age_str = int $age/60/60/24/7;
 489                $age_str .= " weeks ago";
 490        } elsif ($age > 60*60*24*2) {
 491                $age_str = int $age/60/60/24;
 492                $age_str .= " days ago";
 493        } elsif ($age > 60*60*2) {
 494                $age_str = int $age/60/60;
 495                $age_str .= " hours ago";
 496        } elsif ($age > 60*2) {
 497                $age_str = int $age/60;
 498                $age_str .= " min ago";
 499        } elsif ($age > 2) {
 500                $age_str = int $age;
 501                $age_str .= " sec ago";
 502        } else {
 503                $age_str .= " right now";
 504        }
 505        return $age_str;
 506}
 507
 508# convert file mode in octal to symbolic file mode string
 509sub mode_str {
 510        my $mode = oct shift;
 511
 512        if (S_ISDIR($mode & S_IFMT)) {
 513                return 'drwxr-xr-x';
 514        } elsif (S_ISLNK($mode)) {
 515                return 'lrwxrwxrwx';
 516        } elsif (S_ISREG($mode)) {
 517                # git cares only about the executable bit
 518                if ($mode & S_IXUSR) {
 519                        return '-rwxr-xr-x';
 520                } else {
 521                        return '-rw-r--r--';
 522                };
 523        } else {
 524                return '----------';
 525        }
 526}
 527
 528# convert file mode in octal to file type string
 529sub file_type {
 530        my $mode = shift;
 531
 532        if ($mode !~ m/^[0-7]+$/) {
 533                return $mode;
 534        } else {
 535                $mode = oct $mode;
 536        }
 537
 538        if (S_ISDIR($mode & S_IFMT)) {
 539                return "directory";
 540        } elsif (S_ISLNK($mode)) {
 541                return "symlink";
 542        } elsif (S_ISREG($mode)) {
 543                return "file";
 544        } else {
 545                return "unknown";
 546        }
 547}
 548
 549## ----------------------------------------------------------------------
 550## functions returning short HTML fragments, or transforming HTML fragments
 551## which don't beling to other sections
 552
 553# format line of commit message or tag comment
 554sub format_log_line_html {
 555        my $line = shift;
 556
 557        $line = esc_html($line);
 558        $line =~ s/ /&nbsp;/g;
 559        if ($line =~ m/([0-9a-fA-F]{40})/) {
 560                my $hash_text = $1;
 561                if (git_get_type($hash_text) eq "commit") {
 562                        my $link =
 563                                $cgi->a({-href => href(action=>"commit", hash=>$hash_text),
 564                                        -class => "text"}, $hash_text);
 565                        $line =~ s/$hash_text/$link/;
 566                }
 567        }
 568        return $line;
 569}
 570
 571# format marker of refs pointing to given object
 572sub format_ref_marker {
 573        my ($refs, $id) = @_;
 574        my $markers = '';
 575
 576        if (defined $refs->{$id}) {
 577                foreach my $ref (@{$refs->{$id}}) {
 578                        my ($type, $name) = qw();
 579                        # e.g. tags/v2.6.11 or heads/next
 580                        if ($ref =~ m!^(.*?)s?/(.*)$!) {
 581                                $type = $1;
 582                                $name = $2;
 583                        } else {
 584                                $type = "ref";
 585                                $name = $ref;
 586                        }
 587
 588                        $markers .= " <span class=\"$type\">" . esc_html($name) . "</span>";
 589                }
 590        }
 591
 592        if ($markers) {
 593                return ' <span class="refs">'. $markers . '</span>';
 594        } else {
 595                return "";
 596        }
 597}
 598
 599# format, perhaps shortened and with markers, title line
 600sub format_subject_html {
 601        my ($long, $short, $href, $extra) = @_;
 602        $extra = '' unless defined($extra);
 603
 604        if (length($short) < length($long)) {
 605                return $cgi->a({-href => $href, -class => "list subject",
 606                                -title => $long},
 607                       esc_html($short) . $extra);
 608        } else {
 609                return $cgi->a({-href => $href, -class => "list subject"},
 610                       esc_html($long)  . $extra);
 611        }
 612}
 613
 614sub format_diff_line {
 615        my $line = shift;
 616        my $char = substr($line, 0, 1);
 617        my $diff_class = "";
 618
 619        chomp $line;
 620
 621        if ($char eq '+') {
 622                $diff_class = " add";
 623        } elsif ($char eq "-") {
 624                $diff_class = " rem";
 625        } elsif ($char eq "@") {
 626                $diff_class = " chunk_header";
 627        } elsif ($char eq "\\") {
 628                $diff_class = " incomplete";
 629        }
 630        $line = untabify($line);
 631        return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
 632}
 633
 634## ----------------------------------------------------------------------
 635## git utility subroutines, invoking git commands
 636
 637# returns path to the core git executable and the --git-dir parameter as list
 638sub git_cmd {
 639        return $GIT, '--git-dir='.$git_dir;
 640}
 641
 642# returns path to the core git executable and the --git-dir parameter as string
 643sub git_cmd_str {
 644        return join(' ', git_cmd());
 645}
 646
 647# get HEAD ref of given project as hash
 648sub git_get_head_hash {
 649        my $project = shift;
 650        my $o_git_dir = $git_dir;
 651        my $retval = undef;
 652        $git_dir = "$projectroot/$project";
 653        if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
 654                my $head = <$fd>;
 655                close $fd;
 656                if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
 657                        $retval = $1;
 658                }
 659        }
 660        if (defined $o_git_dir) {
 661                $git_dir = $o_git_dir;
 662        }
 663        return $retval;
 664}
 665
 666# get type of given object
 667sub git_get_type {
 668        my $hash = shift;
 669
 670        open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
 671        my $type = <$fd>;
 672        close $fd or return;
 673        chomp $type;
 674        return $type;
 675}
 676
 677sub git_get_project_config {
 678        my ($key, $type) = @_;
 679
 680        return unless ($key);
 681        $key =~ s/^gitweb\.//;
 682        return if ($key =~ m/\W/);
 683
 684        my @x = (git_cmd(), 'repo-config');
 685        if (defined $type) { push @x, $type; }
 686        push @x, "--get";
 687        push @x, "gitweb.$key";
 688        my $val = qx(@x);
 689        chomp $val;
 690        return ($val);
 691}
 692
 693# get hash of given path at given ref
 694sub git_get_hash_by_path {
 695        my $base = shift;
 696        my $path = shift || return undef;
 697
 698        my $tree = $base;
 699
 700        open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
 701                or die_error(undef, "Open git-ls-tree failed");
 702        my $line = <$fd>;
 703        close $fd or return undef;
 704
 705        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 706        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
 707        return $3;
 708}
 709
 710## ......................................................................
 711## git utility functions, directly accessing git repository
 712
 713sub git_get_project_description {
 714        my $path = shift;
 715
 716        open my $fd, "$projectroot/$path/description" or return undef;
 717        my $descr = <$fd>;
 718        close $fd;
 719        chomp $descr;
 720        return $descr;
 721}
 722
 723sub git_get_project_url_list {
 724        my $path = shift;
 725
 726        open my $fd, "$projectroot/$path/cloneurl" or return undef;
 727        my @git_project_url_list = map { chomp; $_ } <$fd>;
 728        close $fd;
 729
 730        return wantarray ? @git_project_url_list : \@git_project_url_list;
 731}
 732
 733sub git_get_projects_list {
 734        my @list;
 735
 736        if (-d $projects_list) {
 737                # search in directory
 738                my $dir = $projects_list;
 739                my $pfxlen = length("$dir");
 740
 741                File::Find::find({
 742                        follow_fast => 1, # follow symbolic links
 743                        dangling_symlinks => 0, # ignore dangling symlinks, silently
 744                        wanted => sub {
 745                                # skip project-list toplevel, if we get it.
 746                                return if (m!^[/.]$!);
 747                                # only directories can be git repositories
 748                                return unless (-d $_);
 749
 750                                my $subdir = substr($File::Find::name, $pfxlen + 1);
 751                                # we check related file in $projectroot
 752                                if (-e "$projectroot/$subdir/HEAD" && (!$export_ok ||
 753                                    -e "$projectroot/$subdir/$export_ok")) {
 754                                        push @list, { path => $subdir };
 755                                        $File::Find::prune = 1;
 756                                }
 757                        },
 758                }, "$dir");
 759
 760        } elsif (-f $projects_list) {
 761                # read from file(url-encoded):
 762                # 'git%2Fgit.git Linus+Torvalds'
 763                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
 764                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
 765                open my ($fd), $projects_list or return undef;
 766                while (my $line = <$fd>) {
 767                        chomp $line;
 768                        my ($path, $owner) = split ' ', $line;
 769                        $path = unescape($path);
 770                        $owner = unescape($owner);
 771                        if (!defined $path) {
 772                                next;
 773                        }
 774                        if (-e "$projectroot/$path/HEAD" && (!$export_ok ||
 775                            -e "$projectroot/$path/$export_ok")) {
 776                                my $pr = {
 777                                        path => $path,
 778                                        owner => decode("utf8", $owner, Encode::FB_DEFAULT),
 779                                };
 780                                push @list, $pr
 781                        }
 782                }
 783                close $fd;
 784        }
 785        @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
 786        return @list;
 787}
 788
 789sub git_get_project_owner {
 790        my $project = shift;
 791        my $owner;
 792
 793        return undef unless $project;
 794
 795        # read from file (url-encoded):
 796        # 'git%2Fgit.git Linus+Torvalds'
 797        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
 798        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
 799        if (-f $projects_list) {
 800                open (my $fd , $projects_list);
 801                while (my $line = <$fd>) {
 802                        chomp $line;
 803                        my ($pr, $ow) = split ' ', $line;
 804                        $pr = unescape($pr);
 805                        $ow = unescape($ow);
 806                        if ($pr eq $project) {
 807                                $owner = decode("utf8", $ow, Encode::FB_DEFAULT);
 808                                last;
 809                        }
 810                }
 811                close $fd;
 812        }
 813        if (!defined $owner) {
 814                $owner = get_file_owner("$projectroot/$project");
 815        }
 816
 817        return $owner;
 818}
 819
 820sub git_get_references {
 821        my $type = shift || "";
 822        my %refs;
 823        my $fd;
 824        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c      refs/tags/v2.6.11
 825        # c39ae07f393806ccf406ef966e9a15afc43cc36a      refs/tags/v2.6.11^{}
 826        if (-f "$projectroot/$project/info/refs") {
 827                open $fd, "$projectroot/$project/info/refs"
 828                        or return;
 829        } else {
 830                open $fd, "-|", git_cmd(), "ls-remote", "."
 831                        or return;
 832        }
 833
 834        while (my $line = <$fd>) {
 835                chomp $line;
 836                if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
 837                        if (defined $refs{$1}) {
 838                                push @{$refs{$1}}, $2;
 839                        } else {
 840                                $refs{$1} = [ $2 ];
 841                        }
 842                }
 843        }
 844        close $fd or return;
 845        return \%refs;
 846}
 847
 848sub git_get_rev_name_tags {
 849        my $hash = shift || return undef;
 850
 851        open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
 852                or return;
 853        my $name_rev = <$fd>;
 854        close $fd;
 855
 856        if ($name_rev =~ m|^$hash tags/(.*)$|) {
 857                return $1;
 858        } else {
 859                # catches also '$hash undefined' output
 860                return undef;
 861        }
 862}
 863
 864## ----------------------------------------------------------------------
 865## parse to hash functions
 866
 867sub parse_date {
 868        my $epoch = shift;
 869        my $tz = shift || "-0000";
 870
 871        my %date;
 872        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
 873        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
 874        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
 875        $date{'hour'} = $hour;
 876        $date{'minute'} = $min;
 877        $date{'mday'} = $mday;
 878        $date{'day'} = $days[$wday];
 879        $date{'month'} = $months[$mon];
 880        $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
 881                           $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
 882        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
 883                             $mday, $months[$mon], $hour ,$min;
 884
 885        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
 886        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
 887        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
 888        $date{'hour_local'} = $hour;
 889        $date{'minute_local'} = $min;
 890        $date{'tz_local'} = $tz;
 891        return %date;
 892}
 893
 894sub parse_tag {
 895        my $tag_id = shift;
 896        my %tag;
 897        my @comment;
 898
 899        open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
 900        $tag{'id'} = $tag_id;
 901        while (my $line = <$fd>) {
 902                chomp $line;
 903                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
 904                        $tag{'object'} = $1;
 905                } elsif ($line =~ m/^type (.+)$/) {
 906                        $tag{'type'} = $1;
 907                } elsif ($line =~ m/^tag (.+)$/) {
 908                        $tag{'name'} = $1;
 909                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
 910                        $tag{'author'} = $1;
 911                        $tag{'epoch'} = $2;
 912                        $tag{'tz'} = $3;
 913                } elsif ($line =~ m/--BEGIN/) {
 914                        push @comment, $line;
 915                        last;
 916                } elsif ($line eq "") {
 917                        last;
 918                }
 919        }
 920        push @comment, <$fd>;
 921        $tag{'comment'} = \@comment;
 922        close $fd or return;
 923        if (!defined $tag{'name'}) {
 924                return
 925        };
 926        return %tag
 927}
 928
 929sub parse_commit {
 930        my $commit_id = shift;
 931        my $commit_text = shift;
 932
 933        my @commit_lines;
 934        my %co;
 935
 936        if (defined $commit_text) {
 937                @commit_lines = @$commit_text;
 938        } else {
 939                $/ = "\0";
 940                open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id
 941                        or return;
 942                @commit_lines = split '\n', <$fd>;
 943                close $fd or return;
 944                $/ = "\n";
 945                pop @commit_lines;
 946        }
 947        my $header = shift @commit_lines;
 948        if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
 949                return;
 950        }
 951        ($co{'id'}, my @parents) = split ' ', $header;
 952        $co{'parents'} = \@parents;
 953        $co{'parent'} = $parents[0];
 954        while (my $line = shift @commit_lines) {
 955                last if $line eq "\n";
 956                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
 957                        $co{'tree'} = $1;
 958                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 959                        $co{'author'} = $1;
 960                        $co{'author_epoch'} = $2;
 961                        $co{'author_tz'} = $3;
 962                        if ($co{'author'} =~ m/^([^<]+) </) {
 963                                $co{'author_name'} = $1;
 964                        } else {
 965                                $co{'author_name'} = $co{'author'};
 966                        }
 967                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
 968                        $co{'committer'} = $1;
 969                        $co{'committer_epoch'} = $2;
 970                        $co{'committer_tz'} = $3;
 971                        $co{'committer_name'} = $co{'committer'};
 972                        $co{'committer_name'} =~ s/ <.*//;
 973                }
 974        }
 975        if (!defined $co{'tree'}) {
 976                return;
 977        };
 978
 979        foreach my $title (@commit_lines) {
 980                $title =~ s/^    //;
 981                if ($title ne "") {
 982                        $co{'title'} = chop_str($title, 80, 5);
 983                        # remove leading stuff of merges to make the interesting part visible
 984                        if (length($title) > 50) {
 985                                $title =~ s/^Automatic //;
 986                                $title =~ s/^merge (of|with) /Merge ... /i;
 987                                if (length($title) > 50) {
 988                                        $title =~ s/(http|rsync):\/\///;
 989                                }
 990                                if (length($title) > 50) {
 991                                        $title =~ s/(master|www|rsync)\.//;
 992                                }
 993                                if (length($title) > 50) {
 994                                        $title =~ s/kernel.org:?//;
 995                                }
 996                                if (length($title) > 50) {
 997                                        $title =~ s/\/pub\/scm//;
 998                                }
 999                        }
1000                        $co{'title_short'} = chop_str($title, 50, 5);
1001                        last;
1002                }
1003        }
1004        # remove added spaces
1005        foreach my $line (@commit_lines) {
1006                $line =~ s/^    //;
1007        }
1008        $co{'comment'} = \@commit_lines;
1009
1010        my $age = time - $co{'committer_epoch'};
1011        $co{'age'} = $age;
1012        $co{'age_string'} = age_string($age);
1013        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
1014        if ($age > 60*60*24*7*2) {
1015                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
1016                $co{'age_string_age'} = $co{'age_string'};
1017        } else {
1018                $co{'age_string_date'} = $co{'age_string'};
1019                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
1020        }
1021        return %co;
1022}
1023
1024# parse ref from ref_file, given by ref_id, with given type
1025sub parse_ref {
1026        my $ref_file = shift;
1027        my $ref_id = shift;
1028        my $type = shift || git_get_type($ref_id);
1029        my %ref_item;
1030
1031        $ref_item{'type'} = $type;
1032        $ref_item{'id'} = $ref_id;
1033        $ref_item{'epoch'} = 0;
1034        $ref_item{'age'} = "unknown";
1035        if ($type eq "tag") {
1036                my %tag = parse_tag($ref_id);
1037                $ref_item{'comment'} = $tag{'comment'};
1038                if ($tag{'type'} eq "commit") {
1039                        my %co = parse_commit($tag{'object'});
1040                        $ref_item{'epoch'} = $co{'committer_epoch'};
1041                        $ref_item{'age'} = $co{'age_string'};
1042                } elsif (defined($tag{'epoch'})) {
1043                        my $age = time - $tag{'epoch'};
1044                        $ref_item{'epoch'} = $tag{'epoch'};
1045                        $ref_item{'age'} = age_string($age);
1046                }
1047                $ref_item{'reftype'} = $tag{'type'};
1048                $ref_item{'name'} = $tag{'name'};
1049                $ref_item{'refid'} = $tag{'object'};
1050        } elsif ($type eq "commit"){
1051                my %co = parse_commit($ref_id);
1052                $ref_item{'reftype'} = "commit";
1053                $ref_item{'name'} = $ref_file;
1054                $ref_item{'title'} = $co{'title'};
1055                $ref_item{'refid'} = $ref_id;
1056                $ref_item{'epoch'} = $co{'committer_epoch'};
1057                $ref_item{'age'} = $co{'age_string'};
1058        } else {
1059                $ref_item{'reftype'} = $type;
1060                $ref_item{'name'} = $ref_file;
1061                $ref_item{'refid'} = $ref_id;
1062        }
1063
1064        return %ref_item;
1065}
1066
1067# parse line of git-diff-tree "raw" output
1068sub parse_difftree_raw_line {
1069        my $line = shift;
1070        my %res;
1071
1072        # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
1073        # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
1074        if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
1075                $res{'from_mode'} = $1;
1076                $res{'to_mode'} = $2;
1077                $res{'from_id'} = $3;
1078                $res{'to_id'} = $4;
1079                $res{'status'} = $5;
1080                $res{'similarity'} = $6;
1081                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
1082                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
1083                } else {
1084                        $res{'file'} = unquote($7);
1085                }
1086        }
1087        # 'c512b523472485aef4fff9e57b229d9d243c967f'
1088        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
1089                $res{'commit'} = $1;
1090        }
1091
1092        return wantarray ? %res : \%res;
1093}
1094
1095# parse line of git-ls-tree output
1096sub parse_ls_tree_line ($;%) {
1097        my $line = shift;
1098        my %opts = @_;
1099        my %res;
1100
1101        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1102        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
1103
1104        $res{'mode'} = $1;
1105        $res{'type'} = $2;
1106        $res{'hash'} = $3;
1107        if ($opts{'-z'}) {
1108                $res{'name'} = $4;
1109        } else {
1110                $res{'name'} = unquote($4);
1111        }
1112
1113        return wantarray ? %res : \%res;
1114}
1115
1116## ......................................................................
1117## parse to array of hashes functions
1118
1119sub git_get_refs_list {
1120        my $ref_dir = shift;
1121        my @reflist;
1122
1123        my @refs;
1124        open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
1125                or return;
1126        while (my $line = <$fd>) {
1127                chomp $line;
1128                if ($line =~ m/^([0-9a-fA-F]{40})\t$ref_dir\/?([^\^]+)$/) {
1129                        push @refs, { hash => $1, name => $2 };
1130                } elsif ($line =~ m/^[0-9a-fA-F]{40}\t$ref_dir\/?(.*)\^\{\}$/ &&
1131                         $1 eq $refs[-1]{'name'}) {
1132                        # most likely a tag is followed by its peeled
1133                        # (deref) one, and when that happens we know the
1134                        # previous one was of type 'tag'.
1135                        $refs[-1]{'type'} = "tag";
1136                }
1137        }
1138        close $fd;
1139
1140        foreach my $ref (@refs) {
1141                my $ref_file = $ref->{'name'};
1142                my $ref_id   = $ref->{'hash'};
1143
1144                my $type = $ref->{'type'} || git_get_type($ref_id) || next;
1145                my %ref_item = parse_ref($ref_file, $ref_id, $type);
1146
1147                push @reflist, \%ref_item;
1148        }
1149        # sort refs by age
1150        @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
1151        return \@reflist;
1152}
1153
1154## ----------------------------------------------------------------------
1155## filesystem-related functions
1156
1157sub get_file_owner {
1158        my $path = shift;
1159
1160        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
1161        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
1162        if (!defined $gcos) {
1163                return undef;
1164        }
1165        my $owner = $gcos;
1166        $owner =~ s/[,;].*$//;
1167        return decode("utf8", $owner, Encode::FB_DEFAULT);
1168}
1169
1170## ......................................................................
1171## mimetype related functions
1172
1173sub mimetype_guess_file {
1174        my $filename = shift;
1175        my $mimemap = shift;
1176        -r $mimemap or return undef;
1177
1178        my %mimemap;
1179        open(MIME, $mimemap) or return undef;
1180        while (<MIME>) {
1181                next if m/^#/; # skip comments
1182                my ($mime, $exts) = split(/\t+/);
1183                if (defined $exts) {
1184                        my @exts = split(/\s+/, $exts);
1185                        foreach my $ext (@exts) {
1186                                $mimemap{$ext} = $mime;
1187                        }
1188                }
1189        }
1190        close(MIME);
1191
1192        $filename =~ /\.(.*?)$/;
1193        return $mimemap{$1};
1194}
1195
1196sub mimetype_guess {
1197        my $filename = shift;
1198        my $mime;
1199        $filename =~ /\./ or return undef;
1200
1201        if ($mimetypes_file) {
1202                my $file = $mimetypes_file;
1203                if ($file !~ m!^/!) { # if it is relative path
1204                        # it is relative to project
1205                        $file = "$projectroot/$project/$file";
1206                }
1207                $mime = mimetype_guess_file($filename, $file);
1208        }
1209        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
1210        return $mime;
1211}
1212
1213sub blob_mimetype {
1214        my $fd = shift;
1215        my $filename = shift;
1216
1217        if ($filename) {
1218                my $mime = mimetype_guess($filename);
1219                $mime and return $mime;
1220        }
1221
1222        # just in case
1223        return $default_blob_plain_mimetype unless $fd;
1224
1225        if (-T $fd) {
1226                return 'text/plain' .
1227                       ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
1228        } elsif (! $filename) {
1229                return 'application/octet-stream';
1230        } elsif ($filename =~ m/\.png$/i) {
1231                return 'image/png';
1232        } elsif ($filename =~ m/\.gif$/i) {
1233                return 'image/gif';
1234        } elsif ($filename =~ m/\.jpe?g$/i) {
1235                return 'image/jpeg';
1236        } else {
1237                return 'application/octet-stream';
1238        }
1239}
1240
1241## ======================================================================
1242## functions printing HTML: header, footer, error page
1243
1244sub git_header_html {
1245        my $status = shift || "200 OK";
1246        my $expires = shift;
1247
1248        my $title = "$site_name git";
1249        if (defined $project) {
1250                $title .= " - $project";
1251                if (defined $action) {
1252                        $title .= "/$action";
1253                        if (defined $file_name) {
1254                                $title .= " - $file_name";
1255                                if ($action eq "tree" && $file_name !~ m|/$|) {
1256                                        $title .= "/";
1257                                }
1258                        }
1259                }
1260        }
1261        my $content_type;
1262        # require explicit support from the UA if we are to send the page as
1263        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
1264        # we have to do this because MSIE sometimes globs '*/*', pretending to
1265        # support xhtml+xml but choking when it gets what it asked for.
1266        if (defined $cgi->http('HTTP_ACCEPT') &&
1267            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
1268            $cgi->Accept('application/xhtml+xml') != 0) {
1269                $content_type = 'application/xhtml+xml';
1270        } else {
1271                $content_type = 'text/html';
1272        }
1273        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
1274                           -status=> $status, -expires => $expires);
1275        print <<EOF;
1276<?xml version="1.0" encoding="utf-8"?>
1277<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1278<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
1279<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
1280<!-- git core binaries version $git_version -->
1281<head>
1282<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
1283<meta name="generator" content="gitweb/$version git/$git_version"/>
1284<meta name="robots" content="index, nofollow"/>
1285<title>$title</title>
1286<link rel="stylesheet" type="text/css" href="$stylesheet"/>
1287EOF
1288        if (defined $project) {
1289                printf('<link rel="alternate" title="%s log" '.
1290                       'href="%s" type="application/rss+xml"/>'."\n",
1291                       esc_param($project), href(action=>"rss"));
1292        } else {
1293                printf('<link rel="alternate" title="%s projects list" '.
1294                       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
1295                       $site_name, href(project=>undef, action=>"project_index"));
1296                printf('<link rel="alternate" title="%s projects logs" '.
1297                       'href="%s" type="text/x-opml"/>'."\n",
1298                       $site_name, href(project=>undef, action=>"opml"));
1299        }
1300        if (defined $favicon) {
1301                print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
1302        }
1303
1304        print "</head>\n" .
1305              "<body>\n" .
1306              "<div class=\"page_header\">\n" .
1307              "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
1308              "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
1309              "</a>\n";
1310        print $cgi->a({-href => esc_param($home_link)}, $home_link_str) . " / ";
1311        if (defined $project) {
1312                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
1313                if (defined $action) {
1314                        print " / $action";
1315                }
1316                print "\n";
1317                if (!defined $searchtext) {
1318                        $searchtext = "";
1319                }
1320                my $search_hash;
1321                if (defined $hash_base) {
1322                        $search_hash = $hash_base;
1323                } elsif (defined $hash) {
1324                        $search_hash = $hash;
1325                } else {
1326                        $search_hash = "HEAD";
1327                }
1328                $cgi->param("a", "search");
1329                $cgi->param("h", $search_hash);
1330                print $cgi->startform(-method => "get", -action => $my_uri) .
1331                      "<div class=\"search\">\n" .
1332                      $cgi->hidden(-name => "p") . "\n" .
1333                      $cgi->hidden(-name => "a") . "\n" .
1334                      $cgi->hidden(-name => "h") . "\n" .
1335                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
1336                      "</div>" .
1337                      $cgi->end_form() . "\n";
1338        }
1339        print "</div>\n";
1340}
1341
1342sub git_footer_html {
1343        print "<div class=\"page_footer\">\n";
1344        if (defined $project) {
1345                my $descr = git_get_project_description($project);
1346                if (defined $descr) {
1347                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
1348                }
1349                print $cgi->a({-href => href(action=>"rss"),
1350                              -class => "rss_logo"}, "RSS") . "\n";
1351        } else {
1352                print $cgi->a({-href => href(project=>undef, action=>"opml"),
1353                              -class => "rss_logo"}, "OPML") . " ";
1354                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
1355                              -class => "rss_logo"}, "TXT") . "\n";
1356        }
1357        print "</div>\n" .
1358              "</body>\n" .
1359              "</html>";
1360}
1361
1362sub die_error {
1363        my $status = shift || "403 Forbidden";
1364        my $error = shift || "Malformed query, file missing or permission denied";
1365
1366        git_header_html($status);
1367        print <<EOF;
1368<div class="page_body">
1369<br /><br />
1370$status - $error
1371<br />
1372</div>
1373EOF
1374        git_footer_html();
1375        exit;
1376}
1377
1378## ----------------------------------------------------------------------
1379## functions printing or outputting HTML: navigation
1380
1381sub git_print_page_nav {
1382        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
1383        $extra = '' if !defined $extra; # pager or formats
1384
1385        my @navs = qw(summary shortlog log commit commitdiff tree);
1386        if ($suppress) {
1387                @navs = grep { $_ ne $suppress } @navs;
1388        }
1389
1390        my %arg = map { $_ => {action=>$_} } @navs;
1391        if (defined $head) {
1392                for (qw(commit commitdiff)) {
1393                        $arg{$_}{hash} = $head;
1394                }
1395                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
1396                        for (qw(shortlog log)) {
1397                                $arg{$_}{hash} = $head;
1398                        }
1399                }
1400        }
1401        $arg{tree}{hash} = $treehead if defined $treehead;
1402        $arg{tree}{hash_base} = $treebase if defined $treebase;
1403
1404        print "<div class=\"page_nav\">\n" .
1405                (join " | ",
1406                 map { $_ eq $current ?
1407                       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
1408                 } @navs);
1409        print "<br/>\n$extra<br/>\n" .
1410              "</div>\n";
1411}
1412
1413sub format_paging_nav {
1414        my ($action, $hash, $head, $page, $nrevs) = @_;
1415        my $paging_nav;
1416
1417
1418        if ($hash ne $head || $page) {
1419                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
1420        } else {
1421                $paging_nav .= "HEAD";
1422        }
1423
1424        if ($page > 0) {
1425                $paging_nav .= " &sdot; " .
1426                        $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
1427                                 -accesskey => "p", -title => "Alt-p"}, "prev");
1428        } else {
1429                $paging_nav .= " &sdot; prev";
1430        }
1431
1432        if ($nrevs >= (100 * ($page+1)-1)) {
1433                $paging_nav .= " &sdot; " .
1434                        $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
1435                                 -accesskey => "n", -title => "Alt-n"}, "next");
1436        } else {
1437                $paging_nav .= " &sdot; next";
1438        }
1439
1440        return $paging_nav;
1441}
1442
1443## ......................................................................
1444## functions printing or outputting HTML: div
1445
1446sub git_print_header_div {
1447        my ($action, $title, $hash, $hash_base) = @_;
1448        my %args = ();
1449
1450        $args{action} = $action;
1451        $args{hash} = $hash if $hash;
1452        $args{hash_base} = $hash_base if $hash_base;
1453
1454        print "<div class=\"header\">\n" .
1455              $cgi->a({-href => href(%args), -class => "title"},
1456              $title ? $title : $action) .
1457              "\n</div>\n";
1458}
1459
1460#sub git_print_authorship (\%) {
1461sub git_print_authorship {
1462        my $co = shift;
1463
1464        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
1465        print "<div class=\"author_date\">" .
1466              esc_html($co->{'author_name'}) .
1467              " [$ad{'rfc2822'}";
1468        if ($ad{'hour_local'} < 6) {
1469                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
1470                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1471        } else {
1472                printf(" (%02d:%02d %s)",
1473                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1474        }
1475        print "]</div>\n";
1476}
1477
1478sub git_print_page_path {
1479        my $name = shift;
1480        my $type = shift;
1481        my $hb = shift;
1482
1483        if (!defined $name) {
1484                print "<div class=\"page_path\">/</div>\n";
1485        } else {
1486                my @dirname = split '/', $name;
1487                my $basename = pop @dirname;
1488                my $fullname = '';
1489
1490                print "<div class=\"page_path\">";
1491                foreach my $dir (@dirname) {
1492                        $fullname .= $dir . '/';
1493                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
1494                                                     hash_base=>$hb),
1495                                      -title => $fullname}, esc_html($dir));
1496                        print "/";
1497                }
1498                if (defined $type && $type eq 'blob') {
1499                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
1500                                                     hash_base=>$hb),
1501                                      -title => $name}, esc_html($basename));
1502                } elsif (defined $type && $type eq 'tree') {
1503                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
1504                                                     hash_base=>$hb),
1505                                      -title => $name}, esc_html($basename));
1506                        print "/";
1507                } else {
1508                        print esc_html($basename);
1509                }
1510                print "<br/></div>\n";
1511        }
1512}
1513
1514# sub git_print_log (\@;%) {
1515sub git_print_log ($;%) {
1516        my $log = shift;
1517        my %opts = @_;
1518
1519        if ($opts{'-remove_title'}) {
1520                # remove title, i.e. first line of log
1521                shift @$log;
1522        }
1523        # remove leading empty lines
1524        while (defined $log->[0] && $log->[0] eq "") {
1525                shift @$log;
1526        }
1527
1528        # print log
1529        my $signoff = 0;
1530        my $empty = 0;
1531        foreach my $line (@$log) {
1532                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1533                        $signoff = 1;
1534                        $empty = 0;
1535                        if (! $opts{'-remove_signoff'}) {
1536                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
1537                                next;
1538                        } else {
1539                                # remove signoff lines
1540                                next;
1541                        }
1542                } else {
1543                        $signoff = 0;
1544                }
1545
1546                # print only one empty line
1547                # do not print empty line after signoff
1548                if ($line eq "") {
1549                        next if ($empty || $signoff);
1550                        $empty = 1;
1551                } else {
1552                        $empty = 0;
1553                }
1554
1555                print format_log_line_html($line) . "<br/>\n";
1556        }
1557
1558        if ($opts{'-final_empty_line'}) {
1559                # end with single empty line
1560                print "<br/>\n" unless $empty;
1561        }
1562}
1563
1564sub git_print_simplified_log {
1565        my $log = shift;
1566        my $remove_title = shift;
1567
1568        git_print_log($log,
1569                -final_empty_line=> 1,
1570                -remove_title => $remove_title);
1571}
1572
1573# print tree entry (row of git_tree), but without encompassing <tr> element
1574sub git_print_tree_entry {
1575        my ($t, $basedir, $hash_base, $have_blame) = @_;
1576
1577        my %base_key = ();
1578        $base_key{hash_base} = $hash_base if defined $hash_base;
1579
1580        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
1581        if ($t->{'type'} eq "blob") {
1582                print "<td class=\"list\">" .
1583                      $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
1584                                             file_name=>"$basedir$t->{'name'}", %base_key),
1585                              -class => "list"}, esc_html($t->{'name'})) .
1586                      "</td>\n" .
1587                      "<td class=\"link\">" .
1588                      $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
1589                                             file_name=>"$basedir$t->{'name'}", %base_key)},
1590                              "blob");
1591                if ($have_blame) {
1592                        print " | " .
1593                                $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
1594                                                       file_name=>"$basedir$t->{'name'}", %base_key)},
1595                                        "blame");
1596                }
1597                if (defined $hash_base) {
1598                        print " | " .
1599                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
1600                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
1601                                      "history");
1602                }
1603                print " | " .
1604                      $cgi->a({-href => href(action=>"blob_plain",
1605                                             hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
1606                              "raw") .
1607                      "</td>\n";
1608
1609        } elsif ($t->{'type'} eq "tree") {
1610                print "<td class=\"list\">" .
1611                      $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
1612                                             file_name=>"$basedir$t->{'name'}", %base_key)},
1613                              esc_html($t->{'name'})) .
1614                      "</td>\n" .
1615                      "<td class=\"link\">" .
1616                      $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
1617                                             file_name=>"$basedir$t->{'name'}", %base_key)},
1618                              "tree");
1619                if (defined $hash_base) {
1620                        print " | " .
1621                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
1622                                                     file_name=>"$basedir$t->{'name'}")},
1623                                      "history");
1624                }
1625                print "</td>\n";
1626        }
1627}
1628
1629## ......................................................................
1630## functions printing large fragments of HTML
1631
1632sub git_difftree_body {
1633        my ($difftree, $hash, $parent) = @_;
1634
1635        print "<div class=\"list_head\">\n";
1636        if ($#{$difftree} > 10) {
1637                print(($#{$difftree} + 1) . " files changed:\n");
1638        }
1639        print "</div>\n";
1640
1641        print "<table class=\"diff_tree\">\n";
1642        my $alternate = 0;
1643        my $patchno = 0;
1644        foreach my $line (@{$difftree}) {
1645                my %diff = parse_difftree_raw_line($line);
1646
1647                if ($alternate) {
1648                        print "<tr class=\"dark\">\n";
1649                } else {
1650                        print "<tr class=\"light\">\n";
1651                }
1652                $alternate ^= 1;
1653
1654                my ($to_mode_oct, $to_mode_str, $to_file_type);
1655                my ($from_mode_oct, $from_mode_str, $from_file_type);
1656                if ($diff{'to_mode'} ne ('0' x 6)) {
1657                        $to_mode_oct = oct $diff{'to_mode'};
1658                        if (S_ISREG($to_mode_oct)) { # only for regular file
1659                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
1660                        }
1661                        $to_file_type = file_type($diff{'to_mode'});
1662                }
1663                if ($diff{'from_mode'} ne ('0' x 6)) {
1664                        $from_mode_oct = oct $diff{'from_mode'};
1665                        if (S_ISREG($to_mode_oct)) { # only for regular file
1666                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
1667                        }
1668                        $from_file_type = file_type($diff{'from_mode'});
1669                }
1670
1671                if ($diff{'status'} eq "A") { # created
1672                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
1673                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
1674                        $mode_chng   .= "]</span>";
1675                        print "<td>" .
1676                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1677                                                     hash_base=>$hash, file_name=>$diff{'file'}),
1678                                      -class => "list"}, esc_html($diff{'file'})) .
1679                              "</td>\n" .
1680                              "<td>$mode_chng</td>\n" .
1681                              "<td class=\"link\">" .
1682                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1683                                                     hash_base=>$hash, file_name=>$diff{'file'})},
1684                                      "blob");
1685                        if ($action eq 'commitdiff') {
1686                                # link to patch
1687                                $patchno++;
1688                                print " | " .
1689                                      $cgi->a({-href => "#patch$patchno"}, "patch");
1690                        }
1691                        print "</td>\n";
1692
1693                } elsif ($diff{'status'} eq "D") { # deleted
1694                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
1695                        print "<td>" .
1696                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
1697                                                     hash_base=>$parent, file_name=>$diff{'file'}),
1698                                       -class => "list"}, esc_html($diff{'file'})) .
1699                              "</td>\n" .
1700                              "<td>$mode_chng</td>\n" .
1701                              "<td class=\"link\">" .
1702                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
1703                                                     hash_base=>$parent, file_name=>$diff{'file'})},
1704                                      "blob") .
1705                              " | ";
1706                        if ($action eq 'commitdiff') {
1707                                # link to patch
1708                                $patchno++;
1709                                print " | " .
1710                                      $cgi->a({-href => "#patch$patchno"}, "patch");
1711                        }
1712                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
1713                                                     file_name=>$diff{'file'})},
1714                                      "history") .
1715                              "</td>\n";
1716
1717                } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
1718                        my $mode_chnge = "";
1719                        if ($diff{'from_mode'} != $diff{'to_mode'}) {
1720                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
1721                                if ($from_file_type != $to_file_type) {
1722                                        $mode_chnge .= " from $from_file_type to $to_file_type";
1723                                }
1724                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
1725                                        if ($from_mode_str && $to_mode_str) {
1726                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
1727                                        } elsif ($to_mode_str) {
1728                                                $mode_chnge .= " mode: $to_mode_str";
1729                                        }
1730                                }
1731                                $mode_chnge .= "]</span>\n";
1732                        }
1733                        print "<td>";
1734                        if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
1735                                print $cgi->a({-href => href(action=>"blobdiff",
1736                                                             hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
1737                                                             hash_base=>$hash, hash_parent_base=>$parent,
1738                                                             file_name=>$diff{'file'}),
1739                                              -class => "list"}, esc_html($diff{'file'}));
1740                        } else { # only mode changed
1741                                print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1742                                                             hash_base=>$hash, file_name=>$diff{'file'}),
1743                                              -class => "list"}, esc_html($diff{'file'}));
1744                        }
1745                        print "</td>\n" .
1746                              "<td>$mode_chnge</td>\n" .
1747                              "<td class=\"link\">" .
1748                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1749                                                     hash_base=>$hash, file_name=>$diff{'file'})},
1750                                      "blob");
1751                        if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
1752                                if ($action eq 'commitdiff') {
1753                                        # link to patch
1754                                        $patchno++;
1755                                        print " | " .
1756                                                $cgi->a({-href => "#patch$patchno"}, "patch");
1757                                } else {
1758                                        print " | " .
1759                                                $cgi->a({-href => href(action=>"blobdiff",
1760                                                                       hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
1761                                                                       hash_base=>$hash, hash_parent_base=>$parent,
1762                                                                       file_name=>$diff{'file'})},
1763                                                        "diff");
1764                                }
1765                        }
1766                        print " | " .
1767                                $cgi->a({-href => href(action=>"history",
1768                                                       hash_base=>$hash, file_name=>$diff{'file'})},
1769                                        "history");
1770                        print "</td>\n";
1771
1772                } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
1773                        my %status_name = ('R' => 'moved', 'C' => 'copied');
1774                        my $nstatus = $status_name{$diff{'status'}};
1775                        my $mode_chng = "";
1776                        if ($diff{'from_mode'} != $diff{'to_mode'}) {
1777                                # mode also for directories, so we cannot use $to_mode_str
1778                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
1779                        }
1780                        print "<td>" .
1781                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1782                                                     hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
1783                                      -class => "list"}, esc_html($diff{'to_file'})) . "</td>\n" .
1784                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
1785                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
1786                                                     hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
1787                                      -class => "list"}, esc_html($diff{'from_file'})) .
1788                              " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
1789                              "<td class=\"link\">" .
1790                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1791                                                     hash=>$diff{'to_id'}, file_name=>$diff{'to_file'})},
1792                                      "blob");
1793                        if ($diff{'to_id'} ne $diff{'from_id'}) {
1794                                if ($action eq 'commitdiff') {
1795                                        # link to patch
1796                                        $patchno++;
1797                                        print " | " .
1798                                                $cgi->a({-href => "#patch$patchno"}, "patch");
1799                                } else {
1800                                        print " | " .
1801                                                $cgi->a({-href => href(action=>"blobdiff",
1802                                                                       hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
1803                                                                       hash_base=>$hash, hash_parent_base=>$parent,
1804                                                                       file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
1805                                                        "diff");
1806                                }
1807                        }
1808                        print "</td>\n";
1809
1810                } # we should not encounter Unmerged (U) or Unknown (X) status
1811                print "</tr>\n";
1812        }
1813        print "</table>\n";
1814}
1815
1816sub git_patchset_body {
1817        my ($fd, $difftree, $hash, $hash_parent) = @_;
1818
1819        my $patch_idx = 0;
1820        my $in_header = 0;
1821        my $patch_found = 0;
1822        my $diffinfo;
1823
1824        print "<div class=\"patchset\">\n";
1825
1826        LINE:
1827        while (my $patch_line = <$fd>) {
1828                chomp $patch_line;
1829
1830                if ($patch_line =~ m/^diff /) { # "git diff" header
1831                        # beginning of patch (in patchset)
1832                        if ($patch_found) {
1833                                # close previous patch
1834                                print "</div>\n"; # class="patch"
1835                        } else {
1836                                # first patch in patchset
1837                                $patch_found = 1;
1838                        }
1839                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1840
1841                        if (ref($difftree->[$patch_idx]) eq "HASH") {
1842                                $diffinfo = $difftree->[$patch_idx];
1843                        } else {
1844                                $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
1845                        }
1846                        $patch_idx++;
1847
1848                        # for now, no extended header, hence we skip empty patches
1849                        # companion to  next LINE if $in_header;
1850                        if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
1851                                $in_header = 1;
1852                                next LINE;
1853                        }
1854
1855                        if ($diffinfo->{'status'} eq "A") { # added
1856                                print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
1857                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1858                                                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
1859                                              $diffinfo->{'to_id'}) . "(new)" .
1860                                      "</div>\n"; # class="diff_info"
1861
1862                        } elsif ($diffinfo->{'status'} eq "D") { # deleted
1863                                print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
1864                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1865                                                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
1866                                              $diffinfo->{'from_id'}) . "(deleted)" .
1867                                      "</div>\n"; # class="diff_info"
1868
1869                        } elsif ($diffinfo->{'status'} eq "R" || # renamed
1870                                 $diffinfo->{'status'} eq "C" || # copied
1871                                 $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
1872                                print "<div class=\"diff_info\">" .
1873                                      file_type($diffinfo->{'from_mode'}) . ":" .
1874                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1875                                                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})},
1876                                              $diffinfo->{'from_id'}) .
1877                                      " -> " .
1878                                      file_type($diffinfo->{'to_mode'}) . ":" .
1879                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1880                                                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})},
1881                                              $diffinfo->{'to_id'});
1882                                print "</div>\n"; # class="diff_info"
1883
1884                        } else { # modified, mode changed, ...
1885                                print "<div class=\"diff_info\">" .
1886                                      file_type($diffinfo->{'from_mode'}) . ":" .
1887                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1888                                                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
1889                                              $diffinfo->{'from_id'}) .
1890                                      " -> " .
1891                                      file_type($diffinfo->{'to_mode'}) . ":" .
1892                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1893                                                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
1894                                              $diffinfo->{'to_id'});
1895                                print "</div>\n"; # class="diff_info"
1896                        }
1897
1898                        #print "<div class=\"diff extended_header\">\n";
1899                        $in_header = 1;
1900                        next LINE;
1901                } # start of patch in patchset
1902
1903
1904                if ($in_header && $patch_line =~ m/^---/) {
1905                        #print "</div>\n"; # class="diff extended_header"
1906                        $in_header = 0;
1907
1908                        my $file = $diffinfo->{'from_file'};
1909                        $file  ||= $diffinfo->{'file'};
1910                        $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1911                                                       hash=>$diffinfo->{'from_id'}, file_name=>$file),
1912                                        -class => "list"}, esc_html($file));
1913                        $patch_line =~ s|a/.*$|a/$file|g;
1914                        print "<div class=\"diff from_file\">$patch_line</div>\n";
1915
1916                        $patch_line = <$fd>;
1917                        chomp $patch_line;
1918
1919                        #$patch_line =~ m/^+++/;
1920                        $file    = $diffinfo->{'to_file'};
1921                        $file  ||= $diffinfo->{'file'};
1922                        $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1923                                                       hash=>$diffinfo->{'to_id'}, file_name=>$file),
1924                                        -class => "list"}, esc_html($file));
1925                        $patch_line =~ s|b/.*|b/$file|g;
1926                        print "<div class=\"diff to_file\">$patch_line</div>\n";
1927
1928                        next LINE;
1929                }
1930                next LINE if $in_header;
1931
1932                print format_diff_line($patch_line);
1933        }
1934        print "</div>\n" if $patch_found; # class="patch"
1935
1936        print "</div>\n"; # class="patchset"
1937}
1938
1939# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1940
1941sub git_shortlog_body {
1942        # uses global variable $project
1943        my ($revlist, $from, $to, $refs, $extra) = @_;
1944
1945        my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
1946        my $have_snapshot = (defined $ctype && defined $suffix);
1947
1948        $from = 0 unless defined $from;
1949        $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
1950
1951        print "<table class=\"shortlog\" cellspacing=\"0\">\n";
1952        my $alternate = 0;
1953        for (my $i = $from; $i <= $to; $i++) {
1954                my $commit = $revlist->[$i];
1955                #my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
1956                my $ref = format_ref_marker($refs, $commit);
1957                my %co = parse_commit($commit);
1958                if ($alternate) {
1959                        print "<tr class=\"dark\">\n";
1960                } else {
1961                        print "<tr class=\"light\">\n";
1962                }
1963                $alternate ^= 1;
1964                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
1965                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1966                      "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
1967                      "<td>";
1968                print format_subject_html($co{'title'}, $co{'title_short'},
1969                                          href(action=>"commit", hash=>$commit), $ref);
1970                print "</td>\n" .
1971                      "<td class=\"link\">" .
1972                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
1973                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
1974                if ($have_snapshot) {
1975                        print " | " .  $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
1976                }
1977                print "</td>\n" .
1978                      "</tr>\n";
1979        }
1980        if (defined $extra) {
1981                print "<tr>\n" .
1982                      "<td colspan=\"4\">$extra</td>\n" .
1983                      "</tr>\n";
1984        }
1985        print "</table>\n";
1986}
1987
1988sub git_history_body {
1989        # Warning: assumes constant type (blob or tree) during history
1990        my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
1991
1992        $from = 0 unless defined $from;
1993        $to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
1994
1995        print "<table class=\"history\" cellspacing=\"0\">\n";
1996        my $alternate = 0;
1997        for (my $i = $from; $i <= $to; $i++) {
1998                if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
1999                        next;
2000                }
2001
2002                my $commit = $1;
2003                my %co = parse_commit($commit);
2004                if (!%co) {
2005                        next;
2006                }
2007
2008                my $ref = format_ref_marker($refs, $commit);
2009
2010                if ($alternate) {
2011                        print "<tr class=\"dark\">\n";
2012                } else {
2013                        print "<tr class=\"light\">\n";
2014                }
2015                $alternate ^= 1;
2016                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2017                      # shortlog uses      chop_str($co{'author_name'}, 10)
2018                      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
2019                      "<td>";
2020                # originally git_history used chop_str($co{'title'}, 50)
2021                print format_subject_html($co{'title'}, $co{'title_short'},
2022                                          href(action=>"commit", hash=>$commit), $ref);
2023                print "</td>\n" .
2024                      "<td class=\"link\">" .
2025                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
2026                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
2027                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype);
2028
2029                if ($ftype eq 'blob') {
2030                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
2031                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
2032                        if (defined $blob_current && defined $blob_parent &&
2033                                        $blob_current ne $blob_parent) {
2034                                print " | " .
2035                                        $cgi->a({-href => href(action=>"blobdiff",
2036                                                               hash=>$blob_current, hash_parent=>$blob_parent,
2037                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
2038                                                               file_name=>$file_name)},
2039                                                "diff to current");
2040                        }
2041                }
2042                print "</td>\n" .
2043                      "</tr>\n";
2044        }
2045        if (defined $extra) {
2046                print "<tr>\n" .
2047                      "<td colspan=\"4\">$extra</td>\n" .
2048                      "</tr>\n";
2049        }
2050        print "</table>\n";
2051}
2052
2053sub git_tags_body {
2054        # uses global variable $project
2055        my ($taglist, $from, $to, $extra) = @_;
2056        $from = 0 unless defined $from;
2057        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
2058
2059        print "<table class=\"tags\" cellspacing=\"0\">\n";
2060        my $alternate = 0;
2061        for (my $i = $from; $i <= $to; $i++) {
2062                my $entry = $taglist->[$i];
2063                my %tag = %$entry;
2064                my $comment_lines = $tag{'comment'};
2065                my $comment = shift @$comment_lines;
2066                my $comment_short;
2067                if (defined $comment) {
2068                        $comment_short = chop_str($comment, 30, 5);
2069                }
2070                if ($alternate) {
2071                        print "<tr class=\"dark\">\n";
2072                } else {
2073                        print "<tr class=\"light\">\n";
2074                }
2075                $alternate ^= 1;
2076                print "<td><i>$tag{'age'}</i></td>\n" .
2077                      "<td>" .
2078                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
2079                               -class => "list name"}, esc_html($tag{'name'})) .
2080                      "</td>\n" .
2081                      "<td>";
2082                if (defined $comment) {
2083                        print format_subject_html($comment, $comment_short,
2084                                                  href(action=>"tag", hash=>$tag{'id'}));
2085                }
2086                print "</td>\n" .
2087                      "<td class=\"selflink\">";
2088                if ($tag{'type'} eq "tag") {
2089                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
2090                } else {
2091                        print "&nbsp;";
2092                }
2093                print "</td>\n" .
2094                      "<td class=\"link\">" . " | " .
2095                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
2096                if ($tag{'reftype'} eq "commit") {
2097                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
2098                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
2099                } elsif ($tag{'reftype'} eq "blob") {
2100                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
2101                }
2102                print "</td>\n" .
2103                      "</tr>";
2104        }
2105        if (defined $extra) {
2106                print "<tr>\n" .
2107                      "<td colspan=\"5\">$extra</td>\n" .
2108                      "</tr>\n";
2109        }
2110        print "</table>\n";
2111}
2112
2113sub git_heads_body {
2114        # uses global variable $project
2115        my ($taglist, $head, $from, $to, $extra) = @_;
2116        $from = 0 unless defined $from;
2117        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
2118
2119        print "<table class=\"heads\" cellspacing=\"0\">\n";
2120        my $alternate = 0;
2121        for (my $i = $from; $i <= $to; $i++) {
2122                my $entry = $taglist->[$i];
2123                my %tag = %$entry;
2124                my $curr = $tag{'id'} eq $head;
2125                if ($alternate) {
2126                        print "<tr class=\"dark\">\n";
2127                } else {
2128                        print "<tr class=\"light\">\n";
2129                }
2130                $alternate ^= 1;
2131                print "<td><i>$tag{'age'}</i></td>\n" .
2132                      ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
2133                      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
2134                               -class => "list name"},esc_html($tag{'name'})) .
2135                      "</td>\n" .
2136                      "<td class=\"link\">" .
2137                      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
2138                      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") .
2139                      "</td>\n" .
2140                      "</tr>";
2141        }
2142        if (defined $extra) {
2143                print "<tr>\n" .
2144                      "<td colspan=\"3\">$extra</td>\n" .
2145                      "</tr>\n";
2146        }
2147        print "</table>\n";
2148}
2149
2150## ======================================================================
2151## ======================================================================
2152## actions
2153
2154sub git_project_list {
2155        my $order = $cgi->param('o');
2156        if (defined $order && $order !~ m/project|descr|owner|age/) {
2157                die_error(undef, "Unknown order parameter");
2158        }
2159
2160        my @list = git_get_projects_list();
2161        my @projects;
2162        if (!@list) {
2163                die_error(undef, "No projects found");
2164        }
2165        foreach my $pr (@list) {
2166                my $head = git_get_head_hash($pr->{'path'});
2167                if (!defined $head) {
2168                        next;
2169                }
2170                $git_dir = "$projectroot/$pr->{'path'}";
2171                my %co = parse_commit($head);
2172                if (!%co) {
2173                        next;
2174                }
2175                $pr->{'commit'} = \%co;
2176                if (!defined $pr->{'descr'}) {
2177                        my $descr = git_get_project_description($pr->{'path'}) || "";
2178                        $pr->{'descr'} = chop_str($descr, 25, 5);
2179                }
2180                if (!defined $pr->{'owner'}) {
2181                        $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
2182                }
2183                push @projects, $pr;
2184        }
2185
2186        git_header_html();
2187        if (-f $home_text) {
2188                print "<div class=\"index_include\">\n";
2189                open (my $fd, $home_text);
2190                print <$fd>;
2191                close $fd;
2192                print "</div>\n";
2193        }
2194        print "<table class=\"project_list\">\n" .
2195              "<tr>\n";
2196        $order ||= "project";
2197        if ($order eq "project") {
2198                @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
2199                print "<th>Project</th>\n";
2200        } else {
2201                print "<th>" .
2202                      $cgi->a({-href => href(project=>undef, order=>'project'),
2203                               -class => "header"}, "Project") .
2204                      "</th>\n";
2205        }
2206        if ($order eq "descr") {
2207                @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
2208                print "<th>Description</th>\n";
2209        } else {
2210                print "<th>" .
2211                      $cgi->a({-href => href(project=>undef, order=>'descr'),
2212                               -class => "header"}, "Description") .
2213                      "</th>\n";
2214        }
2215        if ($order eq "owner") {
2216                @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
2217                print "<th>Owner</th>\n";
2218        } else {
2219                print "<th>" .
2220                      $cgi->a({-href => href(project=>undef, order=>'owner'),
2221                               -class => "header"}, "Owner") .
2222                      "</th>\n";
2223        }
2224        if ($order eq "age") {
2225                @projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
2226                print "<th>Last Change</th>\n";
2227        } else {
2228                print "<th>" .
2229                      $cgi->a({-href => href(project=>undef, order=>'age'),
2230                               -class => "header"}, "Last Change") .
2231                      "</th>\n";
2232        }
2233        print "<th></th>\n" .
2234              "</tr>\n";
2235        my $alternate = 0;
2236        foreach my $pr (@projects) {
2237                if ($alternate) {
2238                        print "<tr class=\"dark\">\n";
2239                } else {
2240                        print "<tr class=\"light\">\n";
2241                }
2242                $alternate ^= 1;
2243                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
2244                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
2245                      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
2246                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
2247                print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
2248                      $pr->{'commit'}{'age_string'} . "</td>\n" .
2249                      "<td class=\"link\">" .
2250                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
2251                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
2252                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") .
2253                      "</td>\n" .
2254                      "</tr>\n";
2255        }
2256        print "</table>\n";
2257        git_footer_html();
2258}
2259
2260sub git_project_index {
2261        my @projects = git_get_projects_list();
2262
2263        print $cgi->header(
2264                -type => 'text/plain',
2265                -charset => 'utf-8',
2266                -content_disposition => qq(inline; filename="index.aux"));
2267
2268        foreach my $pr (@projects) {
2269                if (!exists $pr->{'owner'}) {
2270                        $pr->{'owner'} = get_file_owner("$projectroot/$project");
2271                }
2272
2273                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
2274                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
2275                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
2276                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
2277                $path  =~ s/ /\+/g;
2278                $owner =~ s/ /\+/g;
2279
2280                print "$path $owner\n";
2281        }
2282}
2283
2284sub git_summary {
2285        my $descr = git_get_project_description($project) || "none";
2286        my $head = git_get_head_hash($project);
2287        my %co = parse_commit($head);
2288        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
2289
2290        my $owner = git_get_project_owner($project);
2291
2292        my $refs = git_get_references();
2293        git_header_html();
2294        git_print_page_nav('summary','', $head);
2295
2296        print "<div class=\"title\">&nbsp;</div>\n";
2297        print "<table cellspacing=\"0\">\n" .
2298              "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
2299              "<tr><td>owner</td><td>$owner</td></tr>\n" .
2300              "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
2301        # use per project git URL list in $projectroot/$project/cloneurl
2302        # or make project git URL from git base URL and project name
2303        my $url_tag = "URL";
2304        my @url_list = git_get_project_url_list($project);
2305        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
2306        foreach my $git_url (@url_list) {
2307                next unless $git_url;
2308                print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
2309                $url_tag = "";
2310        }
2311        print "</table>\n";
2312
2313        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
2314                git_get_head_hash($project)
2315                or die_error(undef, "Open git-rev-list failed");
2316        my @revlist = map { chomp; $_ } <$fd>;
2317        close $fd;
2318        git_print_header_div('shortlog');
2319        git_shortlog_body(\@revlist, 0, 15, $refs,
2320                          $cgi->a({-href => href(action=>"shortlog")}, "..."));
2321
2322        my $taglist = git_get_refs_list("refs/tags");
2323        if (defined @$taglist) {
2324                git_print_header_div('tags');
2325                git_tags_body($taglist, 0, 15,
2326                              $cgi->a({-href => href(action=>"tags")}, "..."));
2327        }
2328
2329        my $headlist = git_get_refs_list("refs/heads");
2330        if (defined @$headlist) {
2331                git_print_header_div('heads');
2332                git_heads_body($headlist, $head, 0, 15,
2333                               $cgi->a({-href => href(action=>"heads")}, "..."));
2334        }
2335
2336        git_footer_html();
2337}
2338
2339sub git_tag {
2340        my $head = git_get_head_hash($project);
2341        git_header_html();
2342        git_print_page_nav('','', $head,undef,$head);
2343        my %tag = parse_tag($hash);
2344        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
2345        print "<div class=\"title_text\">\n" .
2346              "<table cellspacing=\"0\">\n" .
2347              "<tr>\n" .
2348              "<td>object</td>\n" .
2349              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
2350                               $tag{'object'}) . "</td>\n" .
2351              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
2352                                              $tag{'type'}) . "</td>\n" .
2353              "</tr>\n";
2354        if (defined($tag{'author'})) {
2355                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
2356                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
2357                print "<tr><td></td><td>" . $ad{'rfc2822'} .
2358                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
2359                        "</td></tr>\n";
2360        }
2361        print "</table>\n\n" .
2362              "</div>\n";
2363        print "<div class=\"page_body\">";
2364        my $comment = $tag{'comment'};
2365        foreach my $line (@$comment) {
2366                print esc_html($line) . "<br/>\n";
2367        }
2368        print "</div>\n";
2369        git_footer_html();
2370}
2371
2372sub git_blame2 {
2373        my $fd;
2374        my $ftype;
2375
2376        my ($have_blame) = gitweb_check_feature('blame');
2377        if (!$have_blame) {
2378                die_error('403 Permission denied', "Permission denied");
2379        }
2380        die_error('404 Not Found', "File name not defined") if (!$file_name);
2381        $hash_base ||= git_get_head_hash($project);
2382        die_error(undef, "Couldn't find base commit") unless ($hash_base);
2383        my %co = parse_commit($hash_base)
2384                or die_error(undef, "Reading commit failed");
2385        if (!defined $hash) {
2386                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2387                        or die_error(undef, "Error looking up file");
2388        }
2389        $ftype = git_get_type($hash);
2390        if ($ftype !~ "blob") {
2391                die_error("400 Bad Request", "Object is not a blob");
2392        }
2393        open ($fd, "-|", git_cmd(), "blame", '-l', $file_name, $hash_base)
2394                or die_error(undef, "Open git-blame failed");
2395        git_header_html();
2396        my $formats_nav =
2397                $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
2398                        "blob") .
2399                " | " .
2400                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
2401                        "head");
2402        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2403        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2404        git_print_page_path($file_name, $ftype, $hash_base);
2405        my @rev_color = (qw(light2 dark2));
2406        my $num_colors = scalar(@rev_color);
2407        my $current_color = 0;
2408        my $last_rev;
2409        print <<HTML;
2410<div class="page_body">
2411<table class="blame">
2412<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
2413HTML
2414        while (<$fd>) {
2415                /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
2416                my $full_rev = $1;
2417                my $rev = substr($full_rev, 0, 8);
2418                my $lineno = $2;
2419                my $data = $3;
2420
2421                if (!defined $last_rev) {
2422                        $last_rev = $full_rev;
2423                } elsif ($last_rev ne $full_rev) {
2424                        $last_rev = $full_rev;
2425                        $current_color = ++$current_color % $num_colors;
2426                }
2427                print "<tr class=\"$rev_color[$current_color]\">\n";
2428                print "<td class=\"sha1\">" .
2429                        $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
2430                                esc_html($rev)) . "</td>\n";
2431                print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
2432                      esc_html($lineno) . "</a></td>\n";
2433                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
2434                print "</tr>\n";
2435        }
2436        print "</table>\n";
2437        print "</div>";
2438        close $fd
2439                or print "Reading blob failed\n";
2440        git_footer_html();
2441}
2442
2443sub git_blame {
2444        my $fd;
2445
2446        my ($have_blame) = gitweb_check_feature('blame');
2447        if (!$have_blame) {
2448                die_error('403 Permission denied', "Permission denied");
2449        }
2450        die_error('404 Not Found', "File name not defined") if (!$file_name);
2451        $hash_base ||= git_get_head_hash($project);
2452        die_error(undef, "Couldn't find base commit") unless ($hash_base);
2453        my %co = parse_commit($hash_base)
2454                or die_error(undef, "Reading commit failed");
2455        if (!defined $hash) {
2456                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2457                        or die_error(undef, "Error lookup file");
2458        }
2459        open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
2460                or die_error(undef, "Open git-annotate failed");
2461        git_header_html();
2462        my $formats_nav =
2463                $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
2464                        "blob") .
2465                " | " .
2466                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
2467                        "head");
2468        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2469        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2470        git_print_page_path($file_name, 'blob', $hash_base);
2471        print "<div class=\"page_body\">\n";
2472        print <<HTML;
2473<table class="blame">
2474  <tr>
2475    <th>Commit</th>
2476    <th>Age</th>
2477    <th>Author</th>
2478    <th>Line</th>
2479    <th>Data</th>
2480  </tr>
2481HTML
2482        my @line_class = (qw(light dark));
2483        my $line_class_len = scalar (@line_class);
2484        my $line_class_num = $#line_class;
2485        while (my $line = <$fd>) {
2486                my $long_rev;
2487                my $short_rev;
2488                my $author;
2489                my $time;
2490                my $lineno;
2491                my $data;
2492                my $age;
2493                my $age_str;
2494                my $age_class;
2495
2496                chomp $line;
2497                $line_class_num = ($line_class_num + 1) % $line_class_len;
2498
2499                if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
2500                        $long_rev = $1;
2501                        $author   = $2;
2502                        $time     = $3;
2503                        $lineno   = $4;
2504                        $data     = $5;
2505                } else {
2506                        print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
2507                        next;
2508                }
2509                $short_rev  = substr ($long_rev, 0, 8);
2510                $age        = time () - $time;
2511                $age_str    = age_string ($age);
2512                $age_str    =~ s/ /&nbsp;/g;
2513                $age_class  = age_class($age);
2514                $author     = esc_html ($author);
2515                $author     =~ s/ /&nbsp;/g;
2516
2517                $data = untabify($data);
2518                $data = esc_html ($data);
2519
2520                print <<HTML;
2521  <tr class="$line_class[$line_class_num]">
2522    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
2523    <td class="$age_class">$age_str</td>
2524    <td>$author</td>
2525    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
2526    <td class="pre">$data</td>
2527  </tr>
2528HTML
2529        } # while (my $line = <$fd>)
2530        print "</table>\n\n";
2531        close $fd
2532                or print "Reading blob failed.\n";
2533        print "</div>";
2534        git_footer_html();
2535}
2536
2537sub git_tags {
2538        my $head = git_get_head_hash($project);
2539        git_header_html();
2540        git_print_page_nav('','', $head,undef,$head);
2541        git_print_header_div('summary', $project);
2542
2543        my $taglist = git_get_refs_list("refs/tags");
2544        if (defined @$taglist) {
2545                git_tags_body($taglist);
2546        }
2547        git_footer_html();
2548}
2549
2550sub git_heads {
2551        my $head = git_get_head_hash($project);
2552        git_header_html();
2553        git_print_page_nav('','', $head,undef,$head);
2554        git_print_header_div('summary', $project);
2555
2556        my $taglist = git_get_refs_list("refs/heads");
2557        if (defined @$taglist) {
2558                git_heads_body($taglist, $head);
2559        }
2560        git_footer_html();
2561}
2562
2563sub git_blob_plain {
2564        my $expires;
2565
2566        if (!defined $hash) {
2567                if (defined $file_name) {
2568                        my $base = $hash_base || git_get_head_hash($project);
2569                        $hash = git_get_hash_by_path($base, $file_name, "blob")
2570                                or die_error(undef, "Error lookup file");
2571                } else {
2572                        die_error(undef, "No file name defined");
2573                }
2574        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2575                # blobs defined by non-textual hash id's can be cached
2576                $expires = "+1d";
2577        }
2578
2579        my $type = shift;
2580        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2581                or die_error(undef, "Couldn't cat $file_name, $hash");
2582
2583        $type ||= blob_mimetype($fd, $file_name);
2584
2585        # save as filename, even when no $file_name is given
2586        my $save_as = "$hash";
2587        if (defined $file_name) {
2588                $save_as = $file_name;
2589        } elsif ($type =~ m/^text\//) {
2590                $save_as .= '.txt';
2591        }
2592
2593        print $cgi->header(
2594                -type => "$type",
2595                -expires=>$expires,
2596                -content_disposition => "inline; filename=\"$save_as\"");
2597        undef $/;
2598        binmode STDOUT, ':raw';
2599        print <$fd>;
2600        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2601        $/ = "\n";
2602        close $fd;
2603}
2604
2605sub git_blob {
2606        my $expires;
2607
2608        if (!defined $hash) {
2609                if (defined $file_name) {
2610                        my $base = $hash_base || git_get_head_hash($project);
2611                        $hash = git_get_hash_by_path($base, $file_name, "blob")
2612                                or die_error(undef, "Error lookup file");
2613                } else {
2614                        die_error(undef, "No file name defined");
2615                }
2616        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2617                # blobs defined by non-textual hash id's can be cached
2618                $expires = "+1d";
2619        }
2620
2621        my ($have_blame) = gitweb_check_feature('blame');
2622        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2623                or die_error(undef, "Couldn't cat $file_name, $hash");
2624        my $mimetype = blob_mimetype($fd, $file_name);
2625        if ($mimetype !~ m/^text\//) {
2626                close $fd;
2627                return git_blob_plain($mimetype);
2628        }
2629        git_header_html(undef, $expires);
2630        my $formats_nav = '';
2631        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2632                if (defined $file_name) {
2633                        if ($have_blame) {
2634                                $formats_nav .=
2635                                        $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
2636                                                               hash=>$hash, file_name=>$file_name)},
2637                                                "blame") .
2638                                        " | ";
2639                        }
2640                        $formats_nav .=
2641                                $cgi->a({-href => href(action=>"blob_plain",
2642                                                       hash=>$hash, file_name=>$file_name)},
2643                                        "plain") .
2644                                " | " .
2645                                $cgi->a({-href => href(action=>"blob",
2646                                                       hash_base=>"HEAD", file_name=>$file_name)},
2647                                        "head");
2648                } else {
2649                        $formats_nav .=
2650                                $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain");
2651                }
2652                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2653                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2654        } else {
2655                print "<div class=\"page_nav\">\n" .
2656                      "<br/><br/></div>\n" .
2657                      "<div class=\"title\">$hash</div>\n";
2658        }
2659        git_print_page_path($file_name, "blob", $hash_base);
2660        print "<div class=\"page_body\">\n";
2661        my $nr;
2662        while (my $line = <$fd>) {
2663                chomp $line;
2664                $nr++;
2665                $line = untabify($line);
2666                printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
2667                       $nr, $nr, $nr, esc_html($line);
2668        }
2669        close $fd
2670                or print "Reading blob failed.\n";
2671        print "</div>";
2672        git_footer_html();
2673}
2674
2675sub git_tree {
2676        if (!defined $hash) {
2677                $hash = git_get_head_hash($project);
2678                if (defined $file_name) {
2679                        my $base = $hash_base || $hash;
2680                        $hash = git_get_hash_by_path($base, $file_name, "tree");
2681                }
2682                if (!defined $hash_base) {
2683                        $hash_base = $hash;
2684                }
2685        }
2686        $/ = "\0";
2687        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
2688                or die_error(undef, "Open git-ls-tree failed");
2689        my @entries = map { chomp; $_ } <$fd>;
2690        close $fd or die_error(undef, "Reading tree failed");
2691        $/ = "\n";
2692
2693        my $refs = git_get_references();
2694        my $ref = format_ref_marker($refs, $hash_base);
2695        git_header_html();
2696        my $base = "";
2697        my ($have_blame) = gitweb_check_feature('blame');
2698        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2699                git_print_page_nav('tree','', $hash_base);
2700                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
2701        } else {
2702                undef $hash_base;
2703                print "<div class=\"page_nav\">\n";
2704                print "<br/><br/></div>\n";
2705                print "<div class=\"title\">$hash</div>\n";
2706        }
2707        if (defined $file_name) {
2708                $base = esc_html("$file_name/");
2709        }
2710        git_print_page_path($file_name, 'tree', $hash_base);
2711        print "<div class=\"page_body\">\n";
2712        print "<table cellspacing=\"0\">\n";
2713        my $alternate = 0;
2714        foreach my $line (@entries) {
2715                my %t = parse_ls_tree_line($line, -z => 1);
2716
2717                if ($alternate) {
2718                        print "<tr class=\"dark\">\n";
2719                } else {
2720                        print "<tr class=\"light\">\n";
2721                }
2722                $alternate ^= 1;
2723
2724                git_print_tree_entry(\%t, $base, $hash_base, $have_blame);
2725
2726                print "</tr>\n";
2727        }
2728        print "</table>\n" .
2729              "</div>";
2730        git_footer_html();
2731}
2732
2733sub git_snapshot {
2734
2735        my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
2736        my $have_snapshot = (defined $ctype && defined $suffix);
2737        if (!$have_snapshot) {
2738                die_error('403 Permission denied', "Permission denied");
2739        }
2740
2741        if (!defined $hash) {
2742                $hash = git_get_head_hash($project);
2743        }
2744
2745        my $filename = basename($project) . "-$hash.tar.$suffix";
2746
2747        print $cgi->header(-type => 'application/x-tar',
2748                           -content_encoding => $ctype,
2749                           -content_disposition => "inline; filename=\"$filename\"",
2750                           -status => '200 OK');
2751
2752        my $git_command = git_cmd_str();
2753        open my $fd, "-|", "$git_command tar-tree $hash \'$project\' | $command" or
2754                die_error(undef, "Execute git-tar-tree failed.");
2755        binmode STDOUT, ':raw';
2756        print <$fd>;
2757        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2758        close $fd;
2759
2760}
2761
2762sub git_log {
2763        my $head = git_get_head_hash($project);
2764        if (!defined $hash) {
2765                $hash = $head;
2766        }
2767        if (!defined $page) {
2768                $page = 0;
2769        }
2770        my $refs = git_get_references();
2771
2772        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
2773        open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
2774                or die_error(undef, "Open git-rev-list failed");
2775        my @revlist = map { chomp; $_ } <$fd>;
2776        close $fd;
2777
2778        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
2779
2780        git_header_html();
2781        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
2782
2783        if (!@revlist) {
2784                my %co = parse_commit($hash);
2785
2786                git_print_header_div('summary', $project);
2787                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
2788        }
2789        for (my $i = ($page * 100); $i <= $#revlist; $i++) {
2790                my $commit = $revlist[$i];
2791                my $ref = format_ref_marker($refs, $commit);
2792                my %co = parse_commit($commit);
2793                next if !%co;
2794                my %ad = parse_date($co{'author_epoch'});
2795                git_print_header_div('commit',
2796                               "<span class=\"age\">$co{'age_string'}</span>" .
2797                               esc_html($co{'title'}) . $ref,
2798                               $commit);
2799                print "<div class=\"title_text\">\n" .
2800                      "<div class=\"log_link\">\n" .
2801                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
2802                      " | " .
2803                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
2804                      "<br/>\n" .
2805                      "</div>\n" .
2806                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
2807                      "</div>\n";
2808
2809                print "<div class=\"log_body\">\n";
2810                git_print_simplified_log($co{'comment'});
2811                print "</div>\n";
2812        }
2813        git_footer_html();
2814}
2815
2816sub git_commit {
2817        my %co = parse_commit($hash);
2818        if (!%co) {
2819                die_error(undef, "Unknown commit object");
2820        }
2821        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
2822        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
2823
2824        my $parent = $co{'parent'};
2825        if (!defined $parent) {
2826                $parent = "--root";
2827        }
2828        open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
2829                or die_error(undef, "Open git-diff-tree failed");
2830        my @difftree = map { chomp; $_ } <$fd>;
2831        close $fd or die_error(undef, "Reading git-diff-tree failed");
2832
2833        # non-textual hash id's can be cached
2834        my $expires;
2835        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2836                $expires = "+1d";
2837        }
2838        my $refs = git_get_references();
2839        my $ref = format_ref_marker($refs, $co{'id'});
2840
2841        my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
2842        my $have_snapshot = (defined $ctype && defined $suffix);
2843
2844        my $formats_nav = '';
2845        if (defined $file_name && defined $co{'parent'}) {
2846                my $parent = $co{'parent'};
2847                $formats_nav .=
2848                        $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
2849                                "blame");
2850        }
2851        git_header_html(undef, $expires);
2852        git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
2853                           $hash, $co{'tree'}, $hash,
2854                           $formats_nav);
2855
2856        if (defined $co{'parent'}) {
2857                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
2858        } else {
2859                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
2860        }
2861        print "<div class=\"title_text\">\n" .
2862              "<table cellspacing=\"0\">\n";
2863        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
2864              "<tr>" .
2865              "<td></td><td> $ad{'rfc2822'}";
2866        if ($ad{'hour_local'} < 6) {
2867                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2868                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2869        } else {
2870                printf(" (%02d:%02d %s)",
2871                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2872        }
2873        print "</td>" .
2874              "</tr>\n";
2875        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
2876        print "<tr><td></td><td> $cd{'rfc2822'}" .
2877              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
2878              "</td></tr>\n";
2879        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
2880        print "<tr>" .
2881              "<td>tree</td>" .
2882              "<td class=\"sha1\">" .
2883              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
2884                       class => "list"}, $co{'tree'}) .
2885              "</td>" .
2886              "<td class=\"link\">" .
2887              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
2888                      "tree");
2889        if ($have_snapshot) {
2890                print " | " .
2891                      $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
2892        }
2893        print "</td>" .
2894              "</tr>\n";
2895        my $parents = $co{'parents'};
2896        foreach my $par (@$parents) {
2897                print "<tr>" .
2898                      "<td>parent</td>" .
2899                      "<td class=\"sha1\">" .
2900                      $cgi->a({-href => href(action=>"commit", hash=>$par),
2901                               class => "list"}, $par) .
2902                      "</td>" .
2903                      "<td class=\"link\">" .
2904                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
2905                      " | " .
2906                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
2907                      "</td>" .
2908                      "</tr>\n";
2909        }
2910        print "</table>".
2911              "</div>\n";
2912
2913        print "<div class=\"page_body\">\n";
2914        git_print_log($co{'comment'});
2915        print "</div>\n";
2916
2917        git_difftree_body(\@difftree, $hash, $parent);
2918
2919        git_footer_html();
2920}
2921
2922sub git_blobdiff {
2923        my $format = shift || 'html';
2924
2925        my $fd;
2926        my @difftree;
2927        my %diffinfo;
2928        my $expires;
2929
2930        # preparing $fd and %diffinfo for git_patchset_body
2931        # new style URI
2932        if (defined $hash_base && defined $hash_parent_base) {
2933                if (defined $file_name) {
2934                        # read raw output
2935                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base,
2936                                "--", $file_name
2937                                or die_error(undef, "Open git-diff-tree failed");
2938                        @difftree = map { chomp; $_ } <$fd>;
2939                        close $fd
2940                                or die_error(undef, "Reading git-diff-tree failed");
2941                        @difftree
2942                                or die_error('404 Not Found', "Blob diff not found");
2943
2944                } elsif (defined $hash &&
2945                         $hash =~ /[0-9a-fA-F]{40}/) {
2946                        # try to find filename from $hash
2947
2948                        # read filtered raw output
2949                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base
2950                                or die_error(undef, "Open git-diff-tree failed");
2951                        @difftree =
2952                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
2953                                # $hash == to_id
2954                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
2955                                map { chomp; $_ } <$fd>;
2956                        close $fd
2957                                or die_error(undef, "Reading git-diff-tree failed");
2958                        @difftree
2959                                or die_error('404 Not Found', "Blob diff not found");
2960
2961                } else {
2962                        die_error('404 Not Found', "Missing one of the blob diff parameters");
2963                }
2964
2965                if (@difftree > 1) {
2966                        die_error('404 Not Found', "Ambiguous blob diff specification");
2967                }
2968
2969                %diffinfo = parse_difftree_raw_line($difftree[0]);
2970                $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
2971                $file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};
2972
2973                $hash_parent ||= $diffinfo{'from_id'};
2974                $hash        ||= $diffinfo{'to_id'};
2975
2976                # non-textual hash id's can be cached
2977                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
2978                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
2979                        $expires = '+1d';
2980                }
2981
2982                # open patch output
2983                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2984                        '-p', $hash_parent_base, $hash_base,
2985                        "--", $file_name
2986                        or die_error(undef, "Open git-diff-tree failed");
2987        }
2988
2989        # old/legacy style URI
2990        if (!%diffinfo && # if new style URI failed
2991            defined $hash && defined $hash_parent) {
2992                # fake git-diff-tree raw output
2993                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
2994                $diffinfo{'from_id'} = $hash_parent;
2995                $diffinfo{'to_id'}   = $hash;
2996                if (defined $file_name) {
2997                        if (defined $file_parent) {
2998                                $diffinfo{'status'} = '2';
2999                                $diffinfo{'from_file'} = $file_parent;
3000                                $diffinfo{'to_file'}   = $file_name;
3001                        } else { # assume not renamed
3002                                $diffinfo{'status'} = '1';
3003                                $diffinfo{'from_file'} = $file_name;
3004                                $diffinfo{'to_file'}   = $file_name;
3005                        }
3006                } else { # no filename given
3007                        $diffinfo{'status'} = '2';
3008                        $diffinfo{'from_file'} = $hash_parent;
3009                        $diffinfo{'to_file'}   = $hash;
3010                }
3011
3012                # non-textual hash id's can be cached
3013                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
3014                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
3015                        $expires = '+1d';
3016                }
3017
3018                # open patch output
3019                open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash
3020                        or die_error(undef, "Open git-diff failed");
3021        } else  {
3022                die_error('404 Not Found', "Missing one of the blob diff parameters")
3023                        unless %diffinfo;
3024        }
3025
3026        # header
3027        if ($format eq 'html') {
3028                my $formats_nav =
3029                        $cgi->a({-href => href(action=>"blobdiff_plain",
3030                                               hash=>$hash, hash_parent=>$hash_parent,
3031                                               hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
3032                                               file_name=>$file_name, file_parent=>$file_parent)},
3033                                "plain");
3034                git_header_html(undef, $expires);
3035                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
3036                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
3037                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
3038                } else {
3039                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
3040                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
3041                }
3042                if (defined $file_name) {
3043                        git_print_page_path($file_name, "blob", $hash_base);
3044                } else {
3045                        print "<div class=\"page_path\"></div>\n";
3046                }
3047
3048        } elsif ($format eq 'plain') {
3049                print $cgi->header(
3050                        -type => 'text/plain',
3051                        -charset => 'utf-8',
3052                        -expires => $expires,
3053                        -content_disposition => qq(inline; filename="${file_name}.patch"));
3054
3055                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
3056
3057        } else {
3058                die_error(undef, "Unknown blobdiff format");
3059        }
3060
3061        # patch
3062        if ($format eq 'html') {
3063                print "<div class=\"page_body\">\n";
3064
3065                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
3066                close $fd;
3067
3068                print "</div>\n"; # class="page_body"
3069                git_footer_html();
3070
3071        } else {
3072                while (my $line = <$fd>) {
3073                        $line =~ s!a/($hash|$hash_parent)!a/$diffinfo{'from_file'}!g;
3074                        $line =~ s!b/($hash|$hash_parent)!b/$diffinfo{'to_file'}!g;
3075
3076                        print $line;
3077
3078                        last if $line =~ m!^\+\+\+!;
3079                }
3080                local $/ = undef;
3081                print <$fd>;
3082                close $fd;
3083        }
3084}
3085
3086sub git_blobdiff_plain {
3087        git_blobdiff('plain');
3088}
3089
3090sub git_commitdiff {
3091        my $format = shift || 'html';
3092        my %co = parse_commit($hash);
3093        if (!%co) {
3094                die_error(undef, "Unknown commit object");
3095        }
3096        if (!defined $hash_parent) {
3097                $hash_parent = $co{'parent'} || '--root';
3098        }
3099
3100        # read commitdiff
3101        my $fd;
3102        my @difftree;
3103        if ($format eq 'html') {
3104                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3105                        "--patch-with-raw", "--full-index", $hash_parent, $hash
3106                        or die_error(undef, "Open git-diff-tree failed");
3107
3108                while (chomp(my $line = <$fd>)) {
3109                        # empty line ends raw part of diff-tree output
3110                        last unless $line;
3111                        push @difftree, $line;
3112                }
3113
3114        } elsif ($format eq 'plain') {
3115                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3116                        '-p', $hash_parent, $hash
3117                        or die_error(undef, "Open git-diff-tree failed");
3118
3119        } else {
3120                die_error(undef, "Unknown commitdiff format");
3121        }
3122
3123        # non-textual hash id's can be cached
3124        my $expires;
3125        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
3126                $expires = "+1d";
3127        }
3128
3129        # write commit message
3130        if ($format eq 'html') {
3131                my $refs = git_get_references();
3132                my $ref = format_ref_marker($refs, $co{'id'});
3133                my $formats_nav =
3134                        $cgi->a({-href => href(action=>"commitdiff_plain",
3135                                               hash=>$hash, hash_parent=>$hash_parent)},
3136                                "plain");
3137
3138                git_header_html(undef, $expires);
3139                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
3140                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
3141                git_print_authorship(\%co);
3142                print "<div class=\"page_body\">\n";
3143                print "<div class=\"log\">\n";
3144                git_print_simplified_log($co{'comment'}, 1); # skip title
3145                print "</div>\n"; # class="log"
3146
3147        } elsif ($format eq 'plain') {
3148                my $refs = git_get_references("tags");
3149                my $tagname = git_get_rev_name_tags($hash);
3150                my $filename = basename($project) . "-$hash.patch";
3151
3152                print $cgi->header(
3153                        -type => 'text/plain',
3154                        -charset => 'utf-8',
3155                        -expires => $expires,
3156                        -content_disposition => qq(inline; filename="$filename"));
3157                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
3158                print <<TEXT;
3159From: $co{'author'}
3160Date: $ad{'rfc2822'} ($ad{'tz_local'})
3161Subject: $co{'title'}
3162TEXT
3163                print "X-Git-Tag: $tagname\n" if $tagname;
3164                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
3165
3166                foreach my $line (@{$co{'comment'}}) {
3167                        print "$line\n";
3168                }
3169                print "---\n\n";
3170        }
3171
3172        # write patch
3173        if ($format eq 'html') {
3174                git_difftree_body(\@difftree, $hash, $hash_parent);
3175                print "<br/>\n";
3176
3177                git_patchset_body($fd, \@difftree, $hash, $hash_parent);
3178                close $fd;
3179                print "</div>\n"; # class="page_body"
3180                git_footer_html();
3181
3182        } elsif ($format eq 'plain') {
3183                local $/ = undef;
3184                print <$fd>;
3185                close $fd
3186                        or print "Reading git-diff-tree failed\n";
3187        }
3188}
3189
3190sub git_commitdiff_plain {
3191        git_commitdiff('plain');
3192}
3193
3194sub git_history {
3195        if (!defined $hash_base) {
3196                $hash_base = git_get_head_hash($project);
3197        }
3198        if (!defined $page) {
3199                $page = 0;
3200        }
3201        my $ftype;
3202        my %co = parse_commit($hash_base);
3203        if (!%co) {
3204                die_error(undef, "Unknown commit object");
3205        }
3206
3207        my $refs = git_get_references();
3208        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
3209
3210        if (!defined $hash && defined $file_name) {
3211                $hash = git_get_hash_by_path($hash_base, $file_name);
3212        }
3213        if (defined $hash) {
3214                $ftype = git_get_type($hash);
3215        }
3216
3217        open my $fd, "-|",
3218                git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
3219                        or die_error(undef, "Open git-rev-list-failed");
3220        my @revlist = map { chomp; $_ } <$fd>;
3221        close $fd
3222                or die_error(undef, "Reading git-rev-list failed");
3223
3224        my $paging_nav = '';
3225        if ($page > 0) {
3226                $paging_nav .=
3227                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
3228                                               file_name=>$file_name)},
3229                                "first");
3230                $paging_nav .= " &sdot; " .
3231                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
3232                                               file_name=>$file_name, page=>$page-1),
3233                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3234        } else {
3235                $paging_nav .= "first";
3236                $paging_nav .= " &sdot; prev";
3237        }
3238        if ($#revlist >= (100 * ($page+1)-1)) {
3239                $paging_nav .= " &sdot; " .
3240                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
3241                                               file_name=>$file_name, page=>$page+1),
3242                                 -accesskey => "n", -title => "Alt-n"}, "next");
3243        } else {
3244                $paging_nav .= " &sdot; next";
3245        }
3246        my $next_link = '';
3247        if ($#revlist >= (100 * ($page+1)-1)) {
3248                $next_link =
3249                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
3250                                               file_name=>$file_name, page=>$page+1),
3251                                 -title => "Alt-n"}, "next");
3252        }
3253
3254        git_header_html();
3255        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
3256        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
3257        git_print_page_path($file_name, $ftype, $hash_base);
3258
3259        git_history_body(\@revlist, ($page * 100), $#revlist,
3260                         $refs, $hash_base, $ftype, $next_link);
3261
3262        git_footer_html();
3263}
3264
3265sub git_search {
3266        if (!defined $searchtext) {
3267                die_error(undef, "Text field empty");
3268        }
3269        if (!defined $hash) {
3270                $hash = git_get_head_hash($project);
3271        }
3272        my %co = parse_commit($hash);
3273        if (!%co) {
3274                die_error(undef, "Unknown commit object");
3275        }
3276
3277        my $commit_search = 1;
3278        my $author_search = 0;
3279        my $committer_search = 0;
3280        my $pickaxe_search = 0;
3281        if ($searchtext =~ s/^author\\://i) {
3282                $author_search = 1;
3283        } elsif ($searchtext =~ s/^committer\\://i) {
3284                $committer_search = 1;
3285        } elsif ($searchtext =~ s/^pickaxe\\://i) {
3286                $commit_search = 0;
3287                $pickaxe_search = 1;
3288
3289                # pickaxe may take all resources of your box and run for several minutes
3290                # with every query - so decide by yourself how public you make this feature
3291                my ($have_pickaxe) = gitweb_check_feature('pickaxe');
3292                if (!$have_pickaxe) {
3293                        die_error('403 Permission denied', "Permission denied");
3294                }
3295        }
3296        git_header_html();
3297        git_print_page_nav('','', $hash,$co{'tree'},$hash);
3298        git_print_header_div('commit', esc_html($co{'title'}), $hash);
3299
3300        print "<table cellspacing=\"0\">\n";
3301        my $alternate = 0;
3302        if ($commit_search) {
3303                $/ = "\0";
3304                open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next;
3305                while (my $commit_text = <$fd>) {
3306                        if (!grep m/$searchtext/i, $commit_text) {
3307                                next;
3308                        }
3309                        if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
3310                                next;
3311                        }
3312                        if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
3313                                next;
3314                        }
3315                        my @commit_lines = split "\n", $commit_text;
3316                        my %co = parse_commit(undef, \@commit_lines);
3317                        if (!%co) {
3318                                next;
3319                        }
3320                        if ($alternate) {
3321                                print "<tr class=\"dark\">\n";
3322                        } else {
3323                                print "<tr class=\"light\">\n";
3324                        }
3325                        $alternate ^= 1;
3326                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3327                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
3328                              "<td>" .
3329                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
3330                                       esc_html(chop_str($co{'title'}, 50)) . "<br/>");
3331                        my $comment = $co{'comment'};
3332                        foreach my $line (@$comment) {
3333                                if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
3334                                        my $lead = esc_html($1) || "";
3335                                        $lead = chop_str($lead, 30, 10);
3336                                        my $match = esc_html($2) || "";
3337                                        my $trail = esc_html($3) || "";
3338                                        $trail = chop_str($trail, 30, 10);
3339                                        my $text = "$lead<span class=\"match\">$match</span>$trail";
3340                                        print chop_str($text, 80, 5) . "<br/>\n";
3341                                }
3342                        }
3343                        print "</td>\n" .
3344                              "<td class=\"link\">" .
3345                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3346                              " | " .
3347                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3348                        print "</td>\n" .
3349                              "</tr>\n";
3350                }
3351                close $fd;
3352        }
3353
3354        if ($pickaxe_search) {
3355                $/ = "\n";
3356                my $git_command = git_cmd_str();
3357                open my $fd, "-|", "$git_command rev-list $hash | " .
3358                        "$git_command diff-tree -r --stdin -S\'$searchtext\'";
3359                undef %co;
3360                my @files;
3361                while (my $line = <$fd>) {
3362                        if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
3363                                my %set;
3364                                $set{'file'} = $6;
3365                                $set{'from_id'} = $3;
3366                                $set{'to_id'} = $4;
3367                                $set{'id'} = $set{'to_id'};
3368                                if ($set{'id'} =~ m/0{40}/) {
3369                                        $set{'id'} = $set{'from_id'};
3370                                }
3371                                if ($set{'id'} =~ m/0{40}/) {
3372                                        next;
3373                                }
3374                                push @files, \%set;
3375                        } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
3376                                if (%co) {
3377                                        if ($alternate) {
3378                                                print "<tr class=\"dark\">\n";
3379                                        } else {
3380                                                print "<tr class=\"light\">\n";
3381                                        }
3382                                        $alternate ^= 1;
3383                                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3384                                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
3385                                              "<td>" .
3386                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3387                                                      -class => "list subject"},
3388                                                      esc_html(chop_str($co{'title'}, 50)) . "<br/>");
3389                                        while (my $setref = shift @files) {
3390                                                my %set = %$setref;
3391                                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
3392                                                                             hash=>$set{'id'}, file_name=>$set{'file'}),
3393                                                              -class => "list"},
3394                                                              "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
3395                                                      "<br/>\n";
3396                                        }
3397                                        print "</td>\n" .
3398                                              "<td class=\"link\">" .
3399                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3400                                              " | " .
3401                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3402                                        print "</td>\n" .
3403                                              "</tr>\n";
3404                                }
3405                                %co = parse_commit($1);
3406                        }
3407                }
3408                close $fd;
3409        }
3410        print "</table>\n";
3411        git_footer_html();
3412}
3413
3414sub git_shortlog {
3415        my $head = git_get_head_hash($project);
3416        if (!defined $hash) {
3417                $hash = $head;
3418        }
3419        if (!defined $page) {
3420                $page = 0;
3421        }
3422        my $refs = git_get_references();
3423
3424        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
3425        open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
3426                or die_error(undef, "Open git-rev-list failed");
3427        my @revlist = map { chomp; $_ } <$fd>;
3428        close $fd;
3429
3430        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
3431        my $next_link = '';
3432        if ($#revlist >= (100 * ($page+1)-1)) {
3433                $next_link =
3434                        $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
3435                                 -title => "Alt-n"}, "next");
3436        }
3437
3438
3439        git_header_html();
3440        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
3441        git_print_header_div('summary', $project);
3442
3443        git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
3444
3445        git_footer_html();
3446}
3447
3448## ......................................................................
3449## feeds (RSS, OPML)
3450
3451sub git_rss {
3452        # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3453        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project)
3454                or die_error(undef, "Open git-rev-list failed");
3455        my @revlist = map { chomp; $_ } <$fd>;
3456        close $fd or die_error(undef, "Reading git-rev-list failed");
3457        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
3458        print <<XML;
3459<?xml version="1.0" encoding="utf-8"?>
3460<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
3461<channel>
3462<title>$project $my_uri $my_url</title>
3463<link>${\esc_html("$my_url?p=$project;a=summary")}</link>
3464<description>$project log</description>
3465<language>en</language>
3466XML
3467
3468        for (my $i = 0; $i <= $#revlist; $i++) {
3469                my $commit = $revlist[$i];
3470                my %co = parse_commit($commit);
3471                # we read 150, we always show 30 and the ones more recent than 48 hours
3472                if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
3473                        last;
3474                }
3475                my %cd = parse_date($co{'committer_epoch'});
3476                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3477                        $co{'parent'}, $co{'id'}
3478                        or next;
3479                my @difftree = map { chomp; $_ } <$fd>;
3480                close $fd
3481                        or next;
3482                print "<item>\n" .
3483                      "<title>" .
3484                      sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
3485                      "</title>\n" .
3486                      "<author>" . esc_html($co{'author'}) . "</author>\n" .
3487                      "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
3488                      "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
3489                      "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
3490                      "<description>" . esc_html($co{'title'}) . "</description>\n" .
3491                      "<content:encoded>" .
3492                      "<![CDATA[\n";
3493                my $comment = $co{'comment'};
3494                foreach my $line (@$comment) {
3495                        $line = decode("utf8", $line, Encode::FB_DEFAULT);
3496                        print "$line<br/>\n";
3497                }
3498                print "<br/>\n";
3499                foreach my $line (@difftree) {
3500                        if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
3501                                next;
3502                        }
3503                        my $file = validate_input(unquote($7));
3504                        $file = decode("utf8", $file, Encode::FB_DEFAULT);
3505                        print "$file<br/>\n";
3506                }
3507                print "]]>\n" .
3508                      "</content:encoded>\n" .
3509                      "</item>\n";
3510        }
3511        print "</channel></rss>";
3512}
3513
3514sub git_opml {
3515        my @list = git_get_projects_list();
3516
3517        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
3518        print <<XML;
3519<?xml version="1.0" encoding="utf-8"?>
3520<opml version="1.0">
3521<head>
3522  <title>$site_name Git OPML Export</title>
3523</head>
3524<body>
3525<outline text="git RSS feeds">
3526XML
3527
3528        foreach my $pr (@list) {
3529                my %proj = %$pr;
3530                my $head = git_get_head_hash($proj{'path'});
3531                if (!defined $head) {
3532                        next;
3533                }
3534                $git_dir = "$projectroot/$proj{'path'}";
3535                my %co = parse_commit($head);
3536                if (!%co) {
3537                        next;
3538                }
3539
3540                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
3541                my $rss  = "$my_url?p=$proj{'path'};a=rss";
3542                my $html = "$my_url?p=$proj{'path'};a=summary";
3543                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
3544        }
3545        print <<XML;
3546</outline>
3547</body>
3548</opml>
3549XML
3550}