gitweb / gitweb.perlon commit Merge branch 'maint-1.5.4' into maint (3e08f5d)
   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
  21BEGIN {
  22        CGI->compile() if $ENV{'MOD_PERL'};
  23}
  24
  25our $cgi = new CGI;
  26our $version = "++GIT_VERSION++";
  27our $my_url = $cgi->url();
  28our $my_uri = $cgi->url(-absolute => 1);
  29
  30# core git executable to use
  31# this can just be "git" if your webserver has a sensible PATH
  32our $GIT = "++GIT_BINDIR++/git";
  33
  34# absolute fs-path which will be prepended to the project path
  35#our $projectroot = "/pub/scm";
  36our $projectroot = "++GITWEB_PROJECTROOT++";
  37
  38# fs traversing limit for getting project list
  39# the number is relative to the projectroot
  40our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
  41
  42# target of the home link on top of all pages
  43our $home_link = $my_uri || "/";
  44
  45# string of the home link on top of all pages
  46our $home_link_str = "++GITWEB_HOME_LINK_STR++";
  47
  48# name of your site or organization to appear in page titles
  49# replace this with something more descriptive for clearer bookmarks
  50our $site_name = "++GITWEB_SITENAME++"
  51                 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
  52
  53# filename of html text to include at top of each page
  54our $site_header = "++GITWEB_SITE_HEADER++";
  55# html text to include at home page
  56our $home_text = "++GITWEB_HOMETEXT++";
  57# filename of html text to include at bottom of each page
  58our $site_footer = "++GITWEB_SITE_FOOTER++";
  59
  60# URI of stylesheets
  61our @stylesheets = ("++GITWEB_CSS++");
  62# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
  63our $stylesheet = undef;
  64# URI of GIT logo (72x27 size)
  65our $logo = "++GITWEB_LOGO++";
  66# URI of GIT favicon, assumed to be image/png type
  67our $favicon = "++GITWEB_FAVICON++";
  68
  69# URI and label (title) of GIT logo link
  70#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
  71#our $logo_label = "git documentation";
  72our $logo_url = "http://git.or.cz/";
  73our $logo_label = "git homepage";
  74
  75# source of projects list
  76our $projects_list = "++GITWEB_LIST++";
  77
  78# the width (in characters) of the projects list "Description" column
  79our $projects_list_description_width = 25;
  80
  81# default order of projects list
  82# valid values are none, project, descr, owner, and age
  83our $default_projects_order = "project";
  84
  85# show repository only if this file exists
  86# (only effective if this variable evaluates to true)
  87our $export_ok = "++GITWEB_EXPORT_OK++";
  88
  89# only allow viewing of repositories also shown on the overview page
  90our $strict_export = "++GITWEB_STRICT_EXPORT++";
  91
  92# list of git base URLs used for URL to where fetch project from,
  93# i.e. full URL is "$git_base_url/$project"
  94our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
  95
  96# default blob_plain mimetype and default charset for text/plain blob
  97our $default_blob_plain_mimetype = 'text/plain';
  98our $default_text_plain_charset  = undef;
  99
 100# file to use for guessing MIME types before trying /etc/mime.types
 101# (relative to the current git repository)
 102our $mimetypes_file = undef;
 103
 104# assume this charset if line contains non-UTF-8 characters;
 105# it should be valid encoding (see Encoding::Supported(3pm) for list),
 106# for which encoding all byte sequences are valid, for example
 107# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
 108# could be even 'utf-8' for the old behavior)
 109our $fallback_encoding = 'latin1';
 110
 111# rename detection options for git-diff and git-diff-tree
 112# - default is '-M', with the cost proportional to
 113#   (number of removed files) * (number of new files).
 114# - more costly is '-C' (which implies '-M'), with the cost proportional to
 115#   (number of changed files + number of removed files) * (number of new files)
 116# - even more costly is '-C', '--find-copies-harder' with cost
 117#   (number of files in the original tree) * (number of new files)
 118# - one might want to include '-B' option, e.g. '-B', '-M'
 119our @diff_opts = ('-M'); # taken from git_commit
 120
 121# information about snapshot formats that gitweb is capable of serving
 122our %known_snapshot_formats = (
 123        # name => {
 124        #       'display' => display name,
 125        #       'type' => mime type,
 126        #       'suffix' => filename suffix,
 127        #       'format' => --format for git-archive,
 128        #       'compressor' => [compressor command and arguments]
 129        #                       (array reference, optional)}
 130        #
 131        'tgz' => {
 132                'display' => 'tar.gz',
 133                'type' => 'application/x-gzip',
 134                'suffix' => '.tar.gz',
 135                'format' => 'tar',
 136                'compressor' => ['gzip']},
 137
 138        'tbz2' => {
 139                'display' => 'tar.bz2',
 140                'type' => 'application/x-bzip2',
 141                'suffix' => '.tar.bz2',
 142                'format' => 'tar',
 143                'compressor' => ['bzip2']},
 144
 145        'zip' => {
 146                'display' => 'zip',
 147                'type' => 'application/x-zip',
 148                'suffix' => '.zip',
 149                'format' => 'zip'},
 150);
 151
 152# Aliases so we understand old gitweb.snapshot values in repository
 153# configuration.
 154our %known_snapshot_format_aliases = (
 155        'gzip'  => 'tgz',
 156        'bzip2' => 'tbz2',
 157
 158        # backward compatibility: legacy gitweb config support
 159        'x-gzip' => undef, 'gz' => undef,
 160        'x-bzip2' => undef, 'bz2' => undef,
 161        'x-zip' => undef, '' => undef,
 162);
 163
 164# You define site-wide feature defaults here; override them with
 165# $GITWEB_CONFIG as necessary.
 166our %feature = (
 167        # feature => {
 168        #       'sub' => feature-sub (subroutine),
 169        #       'override' => allow-override (boolean),
 170        #       'default' => [ default options...] (array reference)}
 171        #
 172        # if feature is overridable (it means that allow-override has true value),
 173        # then feature-sub will be called with default options as parameters;
 174        # return value of feature-sub indicates if to enable specified feature
 175        #
 176        # if there is no 'sub' key (no feature-sub), then feature cannot be
 177        # overriden
 178        #
 179        # use gitweb_check_feature(<feature>) to check if <feature> is enabled
 180
 181        # Enable the 'blame' blob view, showing the last commit that modified
 182        # each line in the file. This can be very CPU-intensive.
 183
 184        # To enable system wide have in $GITWEB_CONFIG
 185        # $feature{'blame'}{'default'} = [1];
 186        # To have project specific config enable override in $GITWEB_CONFIG
 187        # $feature{'blame'}{'override'} = 1;
 188        # and in project config gitweb.blame = 0|1;
 189        'blame' => {
 190                'sub' => \&feature_blame,
 191                'override' => 0,
 192                'default' => [0]},
 193
 194        # Enable the 'snapshot' link, providing a compressed archive of any
 195        # tree. This can potentially generate high traffic if you have large
 196        # project.
 197
 198        # Value is a list of formats defined in %known_snapshot_formats that
 199        # you wish to offer.
 200        # To disable system wide have in $GITWEB_CONFIG
 201        # $feature{'snapshot'}{'default'} = [];
 202        # To have project specific config enable override in $GITWEB_CONFIG
 203        # $feature{'snapshot'}{'override'} = 1;
 204        # and in project config, a comma-separated list of formats or "none"
 205        # to disable.  Example: gitweb.snapshot = tbz2,zip;
 206        'snapshot' => {
 207                'sub' => \&feature_snapshot,
 208                'override' => 0,
 209                'default' => ['tgz']},
 210
 211        # Enable text search, which will list the commits which match author,
 212        # committer or commit text to a given string.  Enabled by default.
 213        # Project specific override is not supported.
 214        'search' => {
 215                'override' => 0,
 216                'default' => [1]},
 217
 218        # Enable grep search, which will list the files in currently selected
 219        # tree containing the given string. Enabled by default. This can be
 220        # potentially CPU-intensive, of course.
 221
 222        # To enable system wide have in $GITWEB_CONFIG
 223        # $feature{'grep'}{'default'} = [1];
 224        # To have project specific config enable override in $GITWEB_CONFIG
 225        # $feature{'grep'}{'override'} = 1;
 226        # and in project config gitweb.grep = 0|1;
 227        'grep' => {
 228                'override' => 0,
 229                'default' => [1]},
 230
 231        # Enable the pickaxe search, which will list the commits that modified
 232        # a given string in a file. This can be practical and quite faster
 233        # alternative to 'blame', but still potentially CPU-intensive.
 234
 235        # To enable system wide have in $GITWEB_CONFIG
 236        # $feature{'pickaxe'}{'default'} = [1];
 237        # To have project specific config enable override in $GITWEB_CONFIG
 238        # $feature{'pickaxe'}{'override'} = 1;
 239        # and in project config gitweb.pickaxe = 0|1;
 240        'pickaxe' => {
 241                'sub' => \&feature_pickaxe,
 242                'override' => 0,
 243                'default' => [1]},
 244
 245        # Make gitweb use an alternative format of the URLs which can be
 246        # more readable and natural-looking: project name is embedded
 247        # directly in the path and the query string contains other
 248        # auxiliary information. All gitweb installations recognize
 249        # URL in either format; this configures in which formats gitweb
 250        # generates links.
 251
 252        # To enable system wide have in $GITWEB_CONFIG
 253        # $feature{'pathinfo'}{'default'} = [1];
 254        # Project specific override is not supported.
 255
 256        # Note that you will need to change the default location of CSS,
 257        # favicon, logo and possibly other files to an absolute URL. Also,
 258        # if gitweb.cgi serves as your indexfile, you will need to force
 259        # $my_uri to contain the script name in your $GITWEB_CONFIG.
 260        'pathinfo' => {
 261                'override' => 0,
 262                'default' => [0]},
 263
 264        # Make gitweb consider projects in project root subdirectories
 265        # to be forks of existing projects. Given project $projname.git,
 266        # projects matching $projname/*.git will not be shown in the main
 267        # projects list, instead a '+' mark will be added to $projname
 268        # there and a 'forks' view will be enabled for the project, listing
 269        # all the forks. If project list is taken from a file, forks have
 270        # to be listed after the main project.
 271
 272        # To enable system wide have in $GITWEB_CONFIG
 273        # $feature{'forks'}{'default'} = [1];
 274        # Project specific override is not supported.
 275        'forks' => {
 276                'override' => 0,
 277                'default' => [0]},
 278);
 279
 280sub gitweb_check_feature {
 281        my ($name) = @_;
 282        return unless exists $feature{$name};
 283        my ($sub, $override, @defaults) = (
 284                $feature{$name}{'sub'},
 285                $feature{$name}{'override'},
 286                @{$feature{$name}{'default'}});
 287        if (!$override) { return @defaults; }
 288        if (!defined $sub) {
 289                warn "feature $name is not overrideable";
 290                return @defaults;
 291        }
 292        return $sub->(@defaults);
 293}
 294
 295sub feature_blame {
 296        my ($val) = git_get_project_config('blame', '--bool');
 297
 298        if ($val eq 'true') {
 299                return 1;
 300        } elsif ($val eq 'false') {
 301                return 0;
 302        }
 303
 304        return $_[0];
 305}
 306
 307sub feature_snapshot {
 308        my (@fmts) = @_;
 309
 310        my ($val) = git_get_project_config('snapshot');
 311
 312        if ($val) {
 313                @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
 314        }
 315
 316        return @fmts;
 317}
 318
 319sub feature_grep {
 320        my ($val) = git_get_project_config('grep', '--bool');
 321
 322        if ($val eq 'true') {
 323                return (1);
 324        } elsif ($val eq 'false') {
 325                return (0);
 326        }
 327
 328        return ($_[0]);
 329}
 330
 331sub feature_pickaxe {
 332        my ($val) = git_get_project_config('pickaxe', '--bool');
 333
 334        if ($val eq 'true') {
 335                return (1);
 336        } elsif ($val eq 'false') {
 337                return (0);
 338        }
 339
 340        return ($_[0]);
 341}
 342
 343# checking HEAD file with -e is fragile if the repository was
 344# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
 345# and then pruned.
 346sub check_head_link {
 347        my ($dir) = @_;
 348        my $headfile = "$dir/HEAD";
 349        return ((-e $headfile) ||
 350                (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
 351}
 352
 353sub check_export_ok {
 354        my ($dir) = @_;
 355        return (check_head_link($dir) &&
 356                (!$export_ok || -e "$dir/$export_ok"));
 357}
 358
 359# process alternate names for backward compatibility
 360# filter out unsupported (unknown) snapshot formats
 361sub filter_snapshot_fmts {
 362        my @fmts = @_;
 363
 364        @fmts = map {
 365                exists $known_snapshot_format_aliases{$_} ?
 366                       $known_snapshot_format_aliases{$_} : $_} @fmts;
 367        @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
 368
 369}
 370
 371our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
 372do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
 373
 374# version of the core git binary
 375our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
 376
 377$projects_list ||= $projectroot;
 378
 379# ======================================================================
 380# input validation and dispatch
 381our $action = $cgi->param('a');
 382if (defined $action) {
 383        if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
 384                die_error(undef, "Invalid action parameter");
 385        }
 386}
 387
 388# parameters which are pathnames
 389our $project = $cgi->param('p');
 390if (defined $project) {
 391        if (!validate_pathname($project) ||
 392            !(-d "$projectroot/$project") ||
 393            !check_head_link("$projectroot/$project") ||
 394            ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
 395            ($strict_export && !project_in_list($project))) {
 396                undef $project;
 397                die_error(undef, "No such project");
 398        }
 399}
 400
 401our $file_name = $cgi->param('f');
 402if (defined $file_name) {
 403        if (!validate_pathname($file_name)) {
 404                die_error(undef, "Invalid file parameter");
 405        }
 406}
 407
 408our $file_parent = $cgi->param('fp');
 409if (defined $file_parent) {
 410        if (!validate_pathname($file_parent)) {
 411                die_error(undef, "Invalid file parent parameter");
 412        }
 413}
 414
 415# parameters which are refnames
 416our $hash = $cgi->param('h');
 417if (defined $hash) {
 418        if (!validate_refname($hash)) {
 419                die_error(undef, "Invalid hash parameter");
 420        }
 421}
 422
 423our $hash_parent = $cgi->param('hp');
 424if (defined $hash_parent) {
 425        if (!validate_refname($hash_parent)) {
 426                die_error(undef, "Invalid hash parent parameter");
 427        }
 428}
 429
 430our $hash_base = $cgi->param('hb');
 431if (defined $hash_base) {
 432        if (!validate_refname($hash_base)) {
 433                die_error(undef, "Invalid hash base parameter");
 434        }
 435}
 436
 437my %allowed_options = (
 438        "--no-merges" => [ qw(rss atom log shortlog history) ],
 439);
 440
 441our @extra_options = $cgi->param('opt');
 442if (defined @extra_options) {
 443        foreach my $opt (@extra_options) {
 444                if (not exists $allowed_options{$opt}) {
 445                        die_error(undef, "Invalid option parameter");
 446                }
 447                if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
 448                        die_error(undef, "Invalid option parameter for this action");
 449                }
 450        }
 451}
 452
 453our $hash_parent_base = $cgi->param('hpb');
 454if (defined $hash_parent_base) {
 455        if (!validate_refname($hash_parent_base)) {
 456                die_error(undef, "Invalid hash parent base parameter");
 457        }
 458}
 459
 460# other parameters
 461our $page = $cgi->param('pg');
 462if (defined $page) {
 463        if ($page =~ m/[^0-9]/) {
 464                die_error(undef, "Invalid page parameter");
 465        }
 466}
 467
 468our $searchtype = $cgi->param('st');
 469if (defined $searchtype) {
 470        if ($searchtype =~ m/[^a-z]/) {
 471                die_error(undef, "Invalid searchtype parameter");
 472        }
 473}
 474
 475our $search_use_regexp = $cgi->param('sr');
 476
 477our $searchtext = $cgi->param('s');
 478our $search_regexp;
 479if (defined $searchtext) {
 480        if (length($searchtext) < 2) {
 481                die_error(undef, "At least two characters are required for search parameter");
 482        }
 483        $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
 484}
 485
 486# now read PATH_INFO and use it as alternative to parameters
 487sub evaluate_path_info {
 488        return if defined $project;
 489        my $path_info = $ENV{"PATH_INFO"};
 490        return if !$path_info;
 491        $path_info =~ s,^/+,,;
 492        return if !$path_info;
 493        # find which part of PATH_INFO is project
 494        $project = $path_info;
 495        $project =~ s,/+$,,;
 496        while ($project && !check_head_link("$projectroot/$project")) {
 497                $project =~ s,/*[^/]*$,,;
 498        }
 499        # validate project
 500        $project = validate_pathname($project);
 501        if (!$project ||
 502            ($export_ok && !-e "$projectroot/$project/$export_ok") ||
 503            ($strict_export && !project_in_list($project))) {
 504                undef $project;
 505                return;
 506        }
 507        # do not change any parameters if an action is given using the query string
 508        return if $action;
 509        $path_info =~ s,^\Q$project\E/*,,;
 510        my ($refname, $pathname) = split(/:/, $path_info, 2);
 511        if (defined $pathname) {
 512                # we got "project.git/branch:filename" or "project.git/branch:dir/"
 513                # we could use git_get_type(branch:pathname), but it needs $git_dir
 514                $pathname =~ s,^/+,,;
 515                if (!$pathname || substr($pathname, -1) eq "/") {
 516                        $action  ||= "tree";
 517                        $pathname =~ s,/$,,;
 518                } else {
 519                        $action  ||= "blob_plain";
 520                }
 521                $hash_base ||= validate_refname($refname);
 522                $file_name ||= validate_pathname($pathname);
 523        } elsif (defined $refname) {
 524                # we got "project.git/branch"
 525                $action ||= "shortlog";
 526                $hash   ||= validate_refname($refname);
 527        }
 528}
 529evaluate_path_info();
 530
 531# path to the current git repository
 532our $git_dir;
 533$git_dir = "$projectroot/$project" if $project;
 534
 535# dispatch
 536my %actions = (
 537        "blame" => \&git_blame2,
 538        "blobdiff" => \&git_blobdiff,
 539        "blobdiff_plain" => \&git_blobdiff_plain,
 540        "blob" => \&git_blob,
 541        "blob_plain" => \&git_blob_plain,
 542        "commitdiff" => \&git_commitdiff,
 543        "commitdiff_plain" => \&git_commitdiff_plain,
 544        "commit" => \&git_commit,
 545        "forks" => \&git_forks,
 546        "heads" => \&git_heads,
 547        "history" => \&git_history,
 548        "log" => \&git_log,
 549        "rss" => \&git_rss,
 550        "atom" => \&git_atom,
 551        "search" => \&git_search,
 552        "search_help" => \&git_search_help,
 553        "shortlog" => \&git_shortlog,
 554        "summary" => \&git_summary,
 555        "tag" => \&git_tag,
 556        "tags" => \&git_tags,
 557        "tree" => \&git_tree,
 558        "snapshot" => \&git_snapshot,
 559        "object" => \&git_object,
 560        # those below don't need $project
 561        "opml" => \&git_opml,
 562        "project_list" => \&git_project_list,
 563        "project_index" => \&git_project_index,
 564);
 565
 566if (!defined $action) {
 567        if (defined $hash) {
 568                $action = git_get_type($hash);
 569        } elsif (defined $hash_base && defined $file_name) {
 570                $action = git_get_type("$hash_base:$file_name");
 571        } elsif (defined $project) {
 572                $action = 'summary';
 573        } else {
 574                $action = 'project_list';
 575        }
 576}
 577if (!defined($actions{$action})) {
 578        die_error(undef, "Unknown action");
 579}
 580if ($action !~ m/^(opml|project_list|project_index)$/ &&
 581    !$project) {
 582        die_error(undef, "Project needed");
 583}
 584$actions{$action}->();
 585exit;
 586
 587## ======================================================================
 588## action links
 589
 590sub href(%) {
 591        my %params = @_;
 592        # default is to use -absolute url() i.e. $my_uri
 593        my $href = $params{-full} ? $my_url : $my_uri;
 594
 595        # XXX: Warning: If you touch this, check the search form for updating,
 596        # too.
 597
 598        my @mapping = (
 599                project => "p",
 600                action => "a",
 601                file_name => "f",
 602                file_parent => "fp",
 603                hash => "h",
 604                hash_parent => "hp",
 605                hash_base => "hb",
 606                hash_parent_base => "hpb",
 607                page => "pg",
 608                order => "o",
 609                searchtext => "s",
 610                searchtype => "st",
 611                snapshot_format => "sf",
 612                extra_options => "opt",
 613                search_use_regexp => "sr",
 614        );
 615        my %mapping = @mapping;
 616
 617        $params{'project'} = $project unless exists $params{'project'};
 618
 619        if ($params{-replay}) {
 620                while (my ($name, $symbol) = each %mapping) {
 621                        if (!exists $params{$name}) {
 622                                # to allow for multivalued params we use arrayref form
 623                                $params{$name} = [ $cgi->param($symbol) ];
 624                        }
 625                }
 626        }
 627
 628        my ($use_pathinfo) = gitweb_check_feature('pathinfo');
 629        if ($use_pathinfo) {
 630                # use PATH_INFO for project name
 631                $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
 632                delete $params{'project'};
 633
 634                # Summary just uses the project path URL
 635                if (defined $params{'action'} && $params{'action'} eq 'summary') {
 636                        delete $params{'action'};
 637                }
 638        }
 639
 640        # now encode the parameters explicitly
 641        my @result = ();
 642        for (my $i = 0; $i < @mapping; $i += 2) {
 643                my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
 644                if (defined $params{$name}) {
 645                        if (ref($params{$name}) eq "ARRAY") {
 646                                foreach my $par (@{$params{$name}}) {
 647                                        push @result, $symbol . "=" . esc_param($par);
 648                                }
 649                        } else {
 650                                push @result, $symbol . "=" . esc_param($params{$name});
 651                        }
 652                }
 653        }
 654        $href .= "?" . join(';', @result) if scalar @result;
 655
 656        return $href;
 657}
 658
 659
 660## ======================================================================
 661## validation, quoting/unquoting and escaping
 662
 663sub validate_pathname {
 664        my $input = shift || return undef;
 665
 666        # no '.' or '..' as elements of path, i.e. no '.' nor '..'
 667        # at the beginning, at the end, and between slashes.
 668        # also this catches doubled slashes
 669        if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
 670                return undef;
 671        }
 672        # no null characters
 673        if ($input =~ m!\0!) {
 674                return undef;
 675        }
 676        return $input;
 677}
 678
 679sub validate_refname {
 680        my $input = shift || return undef;
 681
 682        # textual hashes are O.K.
 683        if ($input =~ m/^[0-9a-fA-F]{40}$/) {
 684                return $input;
 685        }
 686        # it must be correct pathname
 687        $input = validate_pathname($input)
 688                or return undef;
 689        # restrictions on ref name according to git-check-ref-format
 690        if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
 691                return undef;
 692        }
 693        return $input;
 694}
 695
 696# decode sequences of octets in utf8 into Perl's internal form,
 697# which is utf-8 with utf8 flag set if needed.  gitweb writes out
 698# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
 699sub to_utf8 {
 700        my $str = shift;
 701        if (utf8::valid($str)) {
 702                utf8::decode($str);
 703                return $str;
 704        } else {
 705                return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
 706        }
 707}
 708
 709# quote unsafe chars, but keep the slash, even when it's not
 710# correct, but quoted slashes look too horrible in bookmarks
 711sub esc_param {
 712        my $str = shift;
 713        $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
 714        $str =~ s/\+/%2B/g;
 715        $str =~ s/ /\+/g;
 716        return $str;
 717}
 718
 719# quote unsafe chars in whole URL, so some charactrs cannot be quoted
 720sub esc_url {
 721        my $str = shift;
 722        $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
 723        $str =~ s/\+/%2B/g;
 724        $str =~ s/ /\+/g;
 725        return $str;
 726}
 727
 728# replace invalid utf8 character with SUBSTITUTION sequence
 729sub esc_html ($;%) {
 730        my $str = shift;
 731        my %opts = @_;
 732
 733        $str = to_utf8($str);
 734        $str = $cgi->escapeHTML($str);
 735        if ($opts{'-nbsp'}) {
 736                $str =~ s/ /&nbsp;/g;
 737        }
 738        $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
 739        return $str;
 740}
 741
 742# quote control characters and escape filename to HTML
 743sub esc_path {
 744        my $str = shift;
 745        my %opts = @_;
 746
 747        $str = to_utf8($str);
 748        $str = $cgi->escapeHTML($str);
 749        if ($opts{'-nbsp'}) {
 750                $str =~ s/ /&nbsp;/g;
 751        }
 752        $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
 753        return $str;
 754}
 755
 756# Make control characters "printable", using character escape codes (CEC)
 757sub quot_cec {
 758        my $cntrl = shift;
 759        my %opts = @_;
 760        my %es = ( # character escape codes, aka escape sequences
 761                "\t" => '\t',   # tab            (HT)
 762                "\n" => '\n',   # line feed      (LF)
 763                "\r" => '\r',   # carrige return (CR)
 764                "\f" => '\f',   # form feed      (FF)
 765                "\b" => '\b',   # backspace      (BS)
 766                "\a" => '\a',   # alarm (bell)   (BEL)
 767                "\e" => '\e',   # escape         (ESC)
 768                "\013" => '\v', # vertical tab   (VT)
 769                "\000" => '\0', # nul character  (NUL)
 770        );
 771        my $chr = ( (exists $es{$cntrl})
 772                    ? $es{$cntrl}
 773                    : sprintf('\%03o', ord($cntrl)) );
 774        if ($opts{-nohtml}) {
 775                return $chr;
 776        } else {
 777                return "<span class=\"cntrl\">$chr</span>";
 778        }
 779}
 780
 781# Alternatively use unicode control pictures codepoints,
 782# Unicode "printable representation" (PR)
 783sub quot_upr {
 784        my $cntrl = shift;
 785        my %opts = @_;
 786
 787        my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
 788        if ($opts{-nohtml}) {
 789                return $chr;
 790        } else {
 791                return "<span class=\"cntrl\">$chr</span>";
 792        }
 793}
 794
 795# git may return quoted and escaped filenames
 796sub unquote {
 797        my $str = shift;
 798
 799        sub unq {
 800                my $seq = shift;
 801                my %es = ( # character escape codes, aka escape sequences
 802                        't' => "\t",   # tab            (HT, TAB)
 803                        'n' => "\n",   # newline        (NL)
 804                        'r' => "\r",   # return         (CR)
 805                        'f' => "\f",   # form feed      (FF)
 806                        'b' => "\b",   # backspace      (BS)
 807                        'a' => "\a",   # alarm (bell)   (BEL)
 808                        'e' => "\e",   # escape         (ESC)
 809                        'v' => "\013", # vertical tab   (VT)
 810                );
 811
 812                if ($seq =~ m/^[0-7]{1,3}$/) {
 813                        # octal char sequence
 814                        return chr(oct($seq));
 815                } elsif (exists $es{$seq}) {
 816                        # C escape sequence, aka character escape code
 817                        return $es{$seq};
 818                }
 819                # quoted ordinary character
 820                return $seq;
 821        }
 822
 823        if ($str =~ m/^"(.*)"$/) {
 824                # needs unquoting
 825                $str = $1;
 826                $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
 827        }
 828        return $str;
 829}
 830
 831# escape tabs (convert tabs to spaces)
 832sub untabify {
 833        my $line = shift;
 834
 835        while ((my $pos = index($line, "\t")) != -1) {
 836                if (my $count = (8 - ($pos % 8))) {
 837                        my $spaces = ' ' x $count;
 838                        $line =~ s/\t/$spaces/;
 839                }
 840        }
 841
 842        return $line;
 843}
 844
 845sub project_in_list {
 846        my $project = shift;
 847        my @list = git_get_projects_list();
 848        return @list && scalar(grep { $_->{'path'} eq $project } @list);
 849}
 850
 851## ----------------------------------------------------------------------
 852## HTML aware string manipulation
 853
 854# Try to chop given string on a word boundary between position
 855# $len and $len+$add_len. If there is no word boundary there,
 856# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
 857# (marking chopped part) would be longer than given string.
 858sub chop_str {
 859        my $str = shift;
 860        my $len = shift;
 861        my $add_len = shift || 10;
 862        my $where = shift || 'right'; # 'left' | 'center' | 'right'
 863
 864        # allow only $len chars, but don't cut a word if it would fit in $add_len
 865        # if it doesn't fit, cut it if it's still longer than the dots we would add
 866        # remove chopped character entities entirely
 867
 868        # when chopping in the middle, distribute $len into left and right part
 869        # return early if chopping wouldn't make string shorter
 870        if ($where eq 'center') {
 871                return $str if ($len + 5 >= length($str)); # filler is length 5
 872                $len = int($len/2);
 873        } else {
 874                return $str if ($len + 4 >= length($str)); # filler is length 4
 875        }
 876
 877        # regexps: ending and beginning with word part up to $add_len
 878        my $endre = qr/.{$len}\w{0,$add_len}/;
 879        my $begre = qr/\w{0,$add_len}.{$len}/;
 880
 881        if ($where eq 'left') {
 882                $str =~ m/^(.*?)($begre)$/;
 883                my ($lead, $body) = ($1, $2);
 884                if (length($lead) > 4) {
 885                        $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
 886                        $lead = " ...";
 887                }
 888                return "$lead$body";
 889
 890        } elsif ($where eq 'center') {
 891                $str =~ m/^($endre)(.*)$/;
 892                my ($left, $str)  = ($1, $2);
 893                $str =~ m/^(.*?)($begre)$/;
 894                my ($mid, $right) = ($1, $2);
 895                if (length($mid) > 5) {
 896                        $left  =~ s/&[^;]*$//;
 897                        $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
 898                        $mid = " ... ";
 899                }
 900                return "$left$mid$right";
 901
 902        } else {
 903                $str =~ m/^($endre)(.*)$/;
 904                my $body = $1;
 905                my $tail = $2;
 906                if (length($tail) > 4) {
 907                        $body =~ s/&[^;]*$//;
 908                        $tail = "... ";
 909                }
 910                return "$body$tail";
 911        }
 912}
 913
 914# takes the same arguments as chop_str, but also wraps a <span> around the
 915# result with a title attribute if it does get chopped. Additionally, the
 916# string is HTML-escaped.
 917sub chop_and_escape_str {
 918        my ($str) = @_;
 919
 920        my $chopped = chop_str(@_);
 921        if ($chopped eq $str) {
 922                return esc_html($chopped);
 923        } else {
 924                $str =~ s/([[:cntrl:]])/?/g;
 925                return $cgi->span({-title=>$str}, esc_html($chopped));
 926        }
 927}
 928
 929## ----------------------------------------------------------------------
 930## functions returning short strings
 931
 932# CSS class for given age value (in seconds)
 933sub age_class {
 934        my $age = shift;
 935
 936        if (!defined $age) {
 937                return "noage";
 938        } elsif ($age < 60*60*2) {
 939                return "age0";
 940        } elsif ($age < 60*60*24*2) {
 941                return "age1";
 942        } else {
 943                return "age2";
 944        }
 945}
 946
 947# convert age in seconds to "nn units ago" string
 948sub age_string {
 949        my $age = shift;
 950        my $age_str;
 951
 952        if ($age > 60*60*24*365*2) {
 953                $age_str = (int $age/60/60/24/365);
 954                $age_str .= " years ago";
 955        } elsif ($age > 60*60*24*(365/12)*2) {
 956                $age_str = int $age/60/60/24/(365/12);
 957                $age_str .= " months ago";
 958        } elsif ($age > 60*60*24*7*2) {
 959                $age_str = int $age/60/60/24/7;
 960                $age_str .= " weeks ago";
 961        } elsif ($age > 60*60*24*2) {
 962                $age_str = int $age/60/60/24;
 963                $age_str .= " days ago";
 964        } elsif ($age > 60*60*2) {
 965                $age_str = int $age/60/60;
 966                $age_str .= " hours ago";
 967        } elsif ($age > 60*2) {
 968                $age_str = int $age/60;
 969                $age_str .= " min ago";
 970        } elsif ($age > 2) {
 971                $age_str = int $age;
 972                $age_str .= " sec ago";
 973        } else {
 974                $age_str .= " right now";
 975        }
 976        return $age_str;
 977}
 978
 979use constant {
 980        S_IFINVALID => 0030000,
 981        S_IFGITLINK => 0160000,
 982};
 983
 984# submodule/subproject, a commit object reference
 985sub S_ISGITLINK($) {
 986        my $mode = shift;
 987
 988        return (($mode & S_IFMT) == S_IFGITLINK)
 989}
 990
 991# convert file mode in octal to symbolic file mode string
 992sub mode_str {
 993        my $mode = oct shift;
 994
 995        if (S_ISGITLINK($mode)) {
 996                return 'm---------';
 997        } elsif (S_ISDIR($mode & S_IFMT)) {
 998                return 'drwxr-xr-x';
 999        } elsif (S_ISLNK($mode)) {
1000                return 'lrwxrwxrwx';
1001        } elsif (S_ISREG($mode)) {
1002                # git cares only about the executable bit
1003                if ($mode & S_IXUSR) {
1004                        return '-rwxr-xr-x';
1005                } else {
1006                        return '-rw-r--r--';
1007                };
1008        } else {
1009                return '----------';
1010        }
1011}
1012
1013# convert file mode in octal to file type string
1014sub file_type {
1015        my $mode = shift;
1016
1017        if ($mode !~ m/^[0-7]+$/) {
1018                return $mode;
1019        } else {
1020                $mode = oct $mode;
1021        }
1022
1023        if (S_ISGITLINK($mode)) {
1024                return "submodule";
1025        } elsif (S_ISDIR($mode & S_IFMT)) {
1026                return "directory";
1027        } elsif (S_ISLNK($mode)) {
1028                return "symlink";
1029        } elsif (S_ISREG($mode)) {
1030                return "file";
1031        } else {
1032                return "unknown";
1033        }
1034}
1035
1036# convert file mode in octal to file type description string
1037sub file_type_long {
1038        my $mode = shift;
1039
1040        if ($mode !~ m/^[0-7]+$/) {
1041                return $mode;
1042        } else {
1043                $mode = oct $mode;
1044        }
1045
1046        if (S_ISGITLINK($mode)) {
1047                return "submodule";
1048        } elsif (S_ISDIR($mode & S_IFMT)) {
1049                return "directory";
1050        } elsif (S_ISLNK($mode)) {
1051                return "symlink";
1052        } elsif (S_ISREG($mode)) {
1053                if ($mode & S_IXUSR) {
1054                        return "executable";
1055                } else {
1056                        return "file";
1057                };
1058        } else {
1059                return "unknown";
1060        }
1061}
1062
1063
1064## ----------------------------------------------------------------------
1065## functions returning short HTML fragments, or transforming HTML fragments
1066## which don't belong to other sections
1067
1068# format line of commit message.
1069sub format_log_line_html {
1070        my $line = shift;
1071
1072        $line = esc_html($line, -nbsp=>1);
1073        if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1074                my $hash_text = $1;
1075                my $link =
1076                        $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1077                                -class => "text"}, $hash_text);
1078                $line =~ s/$hash_text/$link/;
1079        }
1080        return $line;
1081}
1082
1083# format marker of refs pointing to given object
1084sub format_ref_marker {
1085        my ($refs, $id) = @_;
1086        my $markers = '';
1087
1088        if (defined $refs->{$id}) {
1089                foreach my $ref (@{$refs->{$id}}) {
1090                        my ($type, $name) = qw();
1091                        # e.g. tags/v2.6.11 or heads/next
1092                        if ($ref =~ m!^(.*?)s?/(.*)$!) {
1093                                $type = $1;
1094                                $name = $2;
1095                        } else {
1096                                $type = "ref";
1097                                $name = $ref;
1098                        }
1099
1100                        $markers .= " <span class=\"$type\" title=\"$ref\">" .
1101                                    esc_html($name) . "</span>";
1102                }
1103        }
1104
1105        if ($markers) {
1106                return ' <span class="refs">'. $markers . '</span>';
1107        } else {
1108                return "";
1109        }
1110}
1111
1112# format, perhaps shortened and with markers, title line
1113sub format_subject_html {
1114        my ($long, $short, $href, $extra) = @_;
1115        $extra = '' unless defined($extra);
1116
1117        if (length($short) < length($long)) {
1118                return $cgi->a({-href => $href, -class => "list subject",
1119                                -title => to_utf8($long)},
1120                       esc_html($short) . $extra);
1121        } else {
1122                return $cgi->a({-href => $href, -class => "list subject"},
1123                       esc_html($long)  . $extra);
1124        }
1125}
1126
1127# format git diff header line, i.e. "diff --(git|combined|cc) ..."
1128sub format_git_diff_header_line {
1129        my $line = shift;
1130        my $diffinfo = shift;
1131        my ($from, $to) = @_;
1132
1133        if ($diffinfo->{'nparents'}) {
1134                # combined diff
1135                $line =~ s!^(diff (.*?) )"?.*$!$1!;
1136                if ($to->{'href'}) {
1137                        $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1138                                         esc_path($to->{'file'}));
1139                } else { # file was deleted (no href)
1140                        $line .= esc_path($to->{'file'});
1141                }
1142        } else {
1143                # "ordinary" diff
1144                $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1145                if ($from->{'href'}) {
1146                        $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1147                                         'a/' . esc_path($from->{'file'}));
1148                } else { # file was added (no href)
1149                        $line .= 'a/' . esc_path($from->{'file'});
1150                }
1151                $line .= ' ';
1152                if ($to->{'href'}) {
1153                        $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1154                                         'b/' . esc_path($to->{'file'}));
1155                } else { # file was deleted
1156                        $line .= 'b/' . esc_path($to->{'file'});
1157                }
1158        }
1159
1160        return "<div class=\"diff header\">$line</div>\n";
1161}
1162
1163# format extended diff header line, before patch itself
1164sub format_extended_diff_header_line {
1165        my $line = shift;
1166        my $diffinfo = shift;
1167        my ($from, $to) = @_;
1168
1169        # match <path>
1170        if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1171                $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1172                                       esc_path($from->{'file'}));
1173        }
1174        if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1175                $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1176                                 esc_path($to->{'file'}));
1177        }
1178        # match single <mode>
1179        if ($line =~ m/\s(\d{6})$/) {
1180                $line .= '<span class="info"> (' .
1181                         file_type_long($1) .
1182                         ')</span>';
1183        }
1184        # match <hash>
1185        if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1186                # can match only for combined diff
1187                $line = 'index ';
1188                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1189                        if ($from->{'href'}[$i]) {
1190                                $line .= $cgi->a({-href=>$from->{'href'}[$i],
1191                                                  -class=>"hash"},
1192                                                 substr($diffinfo->{'from_id'}[$i],0,7));
1193                        } else {
1194                                $line .= '0' x 7;
1195                        }
1196                        # separator
1197                        $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1198                }
1199                $line .= '..';
1200                if ($to->{'href'}) {
1201                        $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1202                                         substr($diffinfo->{'to_id'},0,7));
1203                } else {
1204                        $line .= '0' x 7;
1205                }
1206
1207        } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1208                # can match only for ordinary diff
1209                my ($from_link, $to_link);
1210                if ($from->{'href'}) {
1211                        $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1212                                             substr($diffinfo->{'from_id'},0,7));
1213                } else {
1214                        $from_link = '0' x 7;
1215                }
1216                if ($to->{'href'}) {
1217                        $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1218                                           substr($diffinfo->{'to_id'},0,7));
1219                } else {
1220                        $to_link = '0' x 7;
1221                }
1222                my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1223                $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1224        }
1225
1226        return $line . "<br/>\n";
1227}
1228
1229# format from-file/to-file diff header
1230sub format_diff_from_to_header {
1231        my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1232        my $line;
1233        my $result = '';
1234
1235        $line = $from_line;
1236        #assert($line =~ m/^---/) if DEBUG;
1237        # no extra formatting for "^--- /dev/null"
1238        if (! $diffinfo->{'nparents'}) {
1239                # ordinary (single parent) diff
1240                if ($line =~ m!^--- "?a/!) {
1241                        if ($from->{'href'}) {
1242                                $line = '--- a/' .
1243                                        $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1244                                                esc_path($from->{'file'}));
1245                        } else {
1246                                $line = '--- a/' .
1247                                        esc_path($from->{'file'});
1248                        }
1249                }
1250                $result .= qq!<div class="diff from_file">$line</div>\n!;
1251
1252        } else {
1253                # combined diff (merge commit)
1254                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1255                        if ($from->{'href'}[$i]) {
1256                                $line = '--- ' .
1257                                        $cgi->a({-href=>href(action=>"blobdiff",
1258                                                             hash_parent=>$diffinfo->{'from_id'}[$i],
1259                                                             hash_parent_base=>$parents[$i],
1260                                                             file_parent=>$from->{'file'}[$i],
1261                                                             hash=>$diffinfo->{'to_id'},
1262                                                             hash_base=>$hash,
1263                                                             file_name=>$to->{'file'}),
1264                                                 -class=>"path",
1265                                                 -title=>"diff" . ($i+1)},
1266                                                $i+1) .
1267                                        '/' .
1268                                        $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1269                                                esc_path($from->{'file'}[$i]));
1270                        } else {
1271                                $line = '--- /dev/null';
1272                        }
1273                        $result .= qq!<div class="diff from_file">$line</div>\n!;
1274                }
1275        }
1276
1277        $line = $to_line;
1278        #assert($line =~ m/^\+\+\+/) if DEBUG;
1279        # no extra formatting for "^+++ /dev/null"
1280        if ($line =~ m!^\+\+\+ "?b/!) {
1281                if ($to->{'href'}) {
1282                        $line = '+++ b/' .
1283                                $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1284                                        esc_path($to->{'file'}));
1285                } else {
1286                        $line = '+++ b/' .
1287                                esc_path($to->{'file'});
1288                }
1289        }
1290        $result .= qq!<div class="diff to_file">$line</div>\n!;
1291
1292        return $result;
1293}
1294
1295# create note for patch simplified by combined diff
1296sub format_diff_cc_simplified {
1297        my ($diffinfo, @parents) = @_;
1298        my $result = '';
1299
1300        $result .= "<div class=\"diff header\">" .
1301                   "diff --cc ";
1302        if (!is_deleted($diffinfo)) {
1303                $result .= $cgi->a({-href => href(action=>"blob",
1304                                                  hash_base=>$hash,
1305                                                  hash=>$diffinfo->{'to_id'},
1306                                                  file_name=>$diffinfo->{'to_file'}),
1307                                    -class => "path"},
1308                                   esc_path($diffinfo->{'to_file'}));
1309        } else {
1310                $result .= esc_path($diffinfo->{'to_file'});
1311        }
1312        $result .= "</div>\n" . # class="diff header"
1313                   "<div class=\"diff nodifferences\">" .
1314                   "Simple merge" .
1315                   "</div>\n"; # class="diff nodifferences"
1316
1317        return $result;
1318}
1319
1320# format patch (diff) line (not to be used for diff headers)
1321sub format_diff_line {
1322        my $line = shift;
1323        my ($from, $to) = @_;
1324        my $diff_class = "";
1325
1326        chomp $line;
1327
1328        if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1329                # combined diff
1330                my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1331                if ($line =~ m/^\@{3}/) {
1332                        $diff_class = " chunk_header";
1333                } elsif ($line =~ m/^\\/) {
1334                        $diff_class = " incomplete";
1335                } elsif ($prefix =~ tr/+/+/) {
1336                        $diff_class = " add";
1337                } elsif ($prefix =~ tr/-/-/) {
1338                        $diff_class = " rem";
1339                }
1340        } else {
1341                # assume ordinary diff
1342                my $char = substr($line, 0, 1);
1343                if ($char eq '+') {
1344                        $diff_class = " add";
1345                } elsif ($char eq '-') {
1346                        $diff_class = " rem";
1347                } elsif ($char eq '@') {
1348                        $diff_class = " chunk_header";
1349                } elsif ($char eq "\\") {
1350                        $diff_class = " incomplete";
1351                }
1352        }
1353        $line = untabify($line);
1354        if ($from && $to && $line =~ m/^\@{2} /) {
1355                my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1356                        $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1357
1358                $from_lines = 0 unless defined $from_lines;
1359                $to_lines   = 0 unless defined $to_lines;
1360
1361                if ($from->{'href'}) {
1362                        $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1363                                             -class=>"list"}, $from_text);
1364                }
1365                if ($to->{'href'}) {
1366                        $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1367                                             -class=>"list"}, $to_text);
1368                }
1369                $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1370                        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1371                return "<div class=\"diff$diff_class\">$line</div>\n";
1372        } elsif ($from && $to && $line =~ m/^\@{3}/) {
1373                my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1374                my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1375
1376                @from_text = split(' ', $ranges);
1377                for (my $i = 0; $i < @from_text; ++$i) {
1378                        ($from_start[$i], $from_nlines[$i]) =
1379                                (split(',', substr($from_text[$i], 1)), 0);
1380                }
1381
1382                $to_text   = pop @from_text;
1383                $to_start  = pop @from_start;
1384                $to_nlines = pop @from_nlines;
1385
1386                $line = "<span class=\"chunk_info\">$prefix ";
1387                for (my $i = 0; $i < @from_text; ++$i) {
1388                        if ($from->{'href'}[$i]) {
1389                                $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1390                                                  -class=>"list"}, $from_text[$i]);
1391                        } else {
1392                                $line .= $from_text[$i];
1393                        }
1394                        $line .= " ";
1395                }
1396                if ($to->{'href'}) {
1397                        $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1398                                          -class=>"list"}, $to_text);
1399                } else {
1400                        $line .= $to_text;
1401                }
1402                $line .= " $prefix</span>" .
1403                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1404                return "<div class=\"diff$diff_class\">$line</div>\n";
1405        }
1406        return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1407}
1408
1409# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1410# linked.  Pass the hash of the tree/commit to snapshot.
1411sub format_snapshot_links {
1412        my ($hash) = @_;
1413        my @snapshot_fmts = gitweb_check_feature('snapshot');
1414        @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1415        my $num_fmts = @snapshot_fmts;
1416        if ($num_fmts > 1) {
1417                # A parenthesized list of links bearing format names.
1418                # e.g. "snapshot (_tar.gz_ _zip_)"
1419                return "snapshot (" . join(' ', map
1420                        $cgi->a({
1421                                -href => href(
1422                                        action=>"snapshot",
1423                                        hash=>$hash,
1424                                        snapshot_format=>$_
1425                                )
1426                        }, $known_snapshot_formats{$_}{'display'})
1427                , @snapshot_fmts) . ")";
1428        } elsif ($num_fmts == 1) {
1429                # A single "snapshot" link whose tooltip bears the format name.
1430                # i.e. "_snapshot_"
1431                my ($fmt) = @snapshot_fmts;
1432                return
1433                        $cgi->a({
1434                                -href => href(
1435                                        action=>"snapshot",
1436                                        hash=>$hash,
1437                                        snapshot_format=>$fmt
1438                                ),
1439                                -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1440                        }, "snapshot");
1441        } else { # $num_fmts == 0
1442                return undef;
1443        }
1444}
1445
1446## ----------------------------------------------------------------------
1447## git utility subroutines, invoking git commands
1448
1449# returns path to the core git executable and the --git-dir parameter as list
1450sub git_cmd {
1451        return $GIT, '--git-dir='.$git_dir;
1452}
1453
1454# returns path to the core git executable and the --git-dir parameter as string
1455sub git_cmd_str {
1456        return join(' ', git_cmd());
1457}
1458
1459# get HEAD ref of given project as hash
1460sub git_get_head_hash {
1461        my $project = shift;
1462        my $o_git_dir = $git_dir;
1463        my $retval = undef;
1464        $git_dir = "$projectroot/$project";
1465        if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1466                my $head = <$fd>;
1467                close $fd;
1468                if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1469                        $retval = $1;
1470                }
1471        }
1472        if (defined $o_git_dir) {
1473                $git_dir = $o_git_dir;
1474        }
1475        return $retval;
1476}
1477
1478# get type of given object
1479sub git_get_type {
1480        my $hash = shift;
1481
1482        open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1483        my $type = <$fd>;
1484        close $fd or return;
1485        chomp $type;
1486        return $type;
1487}
1488
1489# repository configuration
1490our $config_file = '';
1491our %config;
1492
1493# store multiple values for single key as anonymous array reference
1494# single values stored directly in the hash, not as [ <value> ]
1495sub hash_set_multi {
1496        my ($hash, $key, $value) = @_;
1497
1498        if (!exists $hash->{$key}) {
1499                $hash->{$key} = $value;
1500        } elsif (!ref $hash->{$key}) {
1501                $hash->{$key} = [ $hash->{$key}, $value ];
1502        } else {
1503                push @{$hash->{$key}}, $value;
1504        }
1505}
1506
1507# return hash of git project configuration
1508# optionally limited to some section, e.g. 'gitweb'
1509sub git_parse_project_config {
1510        my $section_regexp = shift;
1511        my %config;
1512
1513        local $/ = "\0";
1514
1515        open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1516                or return;
1517
1518        while (my $keyval = <$fh>) {
1519                chomp $keyval;
1520                my ($key, $value) = split(/\n/, $keyval, 2);
1521
1522                hash_set_multi(\%config, $key, $value)
1523                        if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1524        }
1525        close $fh;
1526
1527        return %config;
1528}
1529
1530# convert config value to boolean, 'true' or 'false'
1531# no value, number > 0, 'true' and 'yes' values are true
1532# rest of values are treated as false (never as error)
1533sub config_to_bool {
1534        my $val = shift;
1535
1536        # strip leading and trailing whitespace
1537        $val =~ s/^\s+//;
1538        $val =~ s/\s+$//;
1539
1540        return (!defined $val ||               # section.key
1541                ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1542                ($val =~ /^(?:true|yes)$/i));  # section.key = true
1543}
1544
1545# convert config value to simple decimal number
1546# an optional value suffix of 'k', 'm', or 'g' will cause the value
1547# to be multiplied by 1024, 1048576, or 1073741824
1548sub config_to_int {
1549        my $val = shift;
1550
1551        # strip leading and trailing whitespace
1552        $val =~ s/^\s+//;
1553        $val =~ s/\s+$//;
1554
1555        if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1556                $unit = lc($unit);
1557                # unknown unit is treated as 1
1558                return $num * ($unit eq 'g' ? 1073741824 :
1559                               $unit eq 'm' ?    1048576 :
1560                               $unit eq 'k' ?       1024 : 1);
1561        }
1562        return $val;
1563}
1564
1565# convert config value to array reference, if needed
1566sub config_to_multi {
1567        my $val = shift;
1568
1569        return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1570}
1571
1572sub git_get_project_config {
1573        my ($key, $type) = @_;
1574
1575        # key sanity check
1576        return unless ($key);
1577        $key =~ s/^gitweb\.//;
1578        return if ($key =~ m/\W/);
1579
1580        # type sanity check
1581        if (defined $type) {
1582                $type =~ s/^--//;
1583                $type = undef
1584                        unless ($type eq 'bool' || $type eq 'int');
1585        }
1586
1587        # get config
1588        if (!defined $config_file ||
1589            $config_file ne "$git_dir/config") {
1590                %config = git_parse_project_config('gitweb');
1591                $config_file = "$git_dir/config";
1592        }
1593
1594        # ensure given type
1595        if (!defined $type) {
1596                return $config{"gitweb.$key"};
1597        } elsif ($type eq 'bool') {
1598                # backward compatibility: 'git config --bool' returns true/false
1599                return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1600        } elsif ($type eq 'int') {
1601                return config_to_int($config{"gitweb.$key"});
1602        }
1603        return $config{"gitweb.$key"};
1604}
1605
1606# get hash of given path at given ref
1607sub git_get_hash_by_path {
1608        my $base = shift;
1609        my $path = shift || return undef;
1610        my $type = shift;
1611
1612        $path =~ s,/+$,,;
1613
1614        open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1615                or die_error(undef, "Open git-ls-tree failed");
1616        my $line = <$fd>;
1617        close $fd or return undef;
1618
1619        if (!defined $line) {
1620                # there is no tree or hash given by $path at $base
1621                return undef;
1622        }
1623
1624        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1625        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1626        if (defined $type && $type ne $2) {
1627                # type doesn't match
1628                return undef;
1629        }
1630        return $3;
1631}
1632
1633# get path of entry with given hash at given tree-ish (ref)
1634# used to get 'from' filename for combined diff (merge commit) for renames
1635sub git_get_path_by_hash {
1636        my $base = shift || return;
1637        my $hash = shift || return;
1638
1639        local $/ = "\0";
1640
1641        open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1642                or return undef;
1643        while (my $line = <$fd>) {
1644                chomp $line;
1645
1646                #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
1647                #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
1648                if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1649                        close $fd;
1650                        return $1;
1651                }
1652        }
1653        close $fd;
1654        return undef;
1655}
1656
1657## ......................................................................
1658## git utility functions, directly accessing git repository
1659
1660sub git_get_project_description {
1661        my $path = shift;
1662
1663        $git_dir = "$projectroot/$path";
1664        open my $fd, "$git_dir/description"
1665                or return git_get_project_config('description');
1666        my $descr = <$fd>;
1667        close $fd;
1668        if (defined $descr) {
1669                chomp $descr;
1670        }
1671        return $descr;
1672}
1673
1674sub git_get_project_url_list {
1675        my $path = shift;
1676
1677        $git_dir = "$projectroot/$path";
1678        open my $fd, "$git_dir/cloneurl"
1679                or return wantarray ?
1680                @{ config_to_multi(git_get_project_config('url')) } :
1681                   config_to_multi(git_get_project_config('url'));
1682        my @git_project_url_list = map { chomp; $_ } <$fd>;
1683        close $fd;
1684
1685        return wantarray ? @git_project_url_list : \@git_project_url_list;
1686}
1687
1688sub git_get_projects_list {
1689        my ($filter) = @_;
1690        my @list;
1691
1692        $filter ||= '';
1693        $filter =~ s/\.git$//;
1694
1695        my ($check_forks) = gitweb_check_feature('forks');
1696
1697        if (-d $projects_list) {
1698                # search in directory
1699                my $dir = $projects_list . ($filter ? "/$filter" : '');
1700                # remove the trailing "/"
1701                $dir =~ s!/+$!!;
1702                my $pfxlen = length("$dir");
1703                my $pfxdepth = ($dir =~ tr!/!!);
1704
1705                File::Find::find({
1706                        follow_fast => 1, # follow symbolic links
1707                        follow_skip => 2, # ignore duplicates
1708                        dangling_symlinks => 0, # ignore dangling symlinks, silently
1709                        wanted => sub {
1710                                # skip project-list toplevel, if we get it.
1711                                return if (m!^[/.]$!);
1712                                # only directories can be git repositories
1713                                return unless (-d $_);
1714                                # don't traverse too deep (Find is super slow on os x)
1715                                if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1716                                        $File::Find::prune = 1;
1717                                        return;
1718                                }
1719
1720                                my $subdir = substr($File::Find::name, $pfxlen + 1);
1721                                # we check related file in $projectroot
1722                                if ($check_forks and $subdir =~ m#/.#) {
1723                                        $File::Find::prune = 1;
1724                                } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1725                                        push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1726                                        $File::Find::prune = 1;
1727                                }
1728                        },
1729                }, "$dir");
1730
1731        } elsif (-f $projects_list) {
1732                # read from file(url-encoded):
1733                # 'git%2Fgit.git Linus+Torvalds'
1734                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1735                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1736                my %paths;
1737                open my ($fd), $projects_list or return;
1738        PROJECT:
1739                while (my $line = <$fd>) {
1740                        chomp $line;
1741                        my ($path, $owner) = split ' ', $line;
1742                        $path = unescape($path);
1743                        $owner = unescape($owner);
1744                        if (!defined $path) {
1745                                next;
1746                        }
1747                        if ($filter ne '') {
1748                                # looking for forks;
1749                                my $pfx = substr($path, 0, length($filter));
1750                                if ($pfx ne $filter) {
1751                                        next PROJECT;
1752                                }
1753                                my $sfx = substr($path, length($filter));
1754                                if ($sfx !~ /^\/.*\.git$/) {
1755                                        next PROJECT;
1756                                }
1757                        } elsif ($check_forks) {
1758                        PATH:
1759                                foreach my $filter (keys %paths) {
1760                                        # looking for forks;
1761                                        my $pfx = substr($path, 0, length($filter));
1762                                        if ($pfx ne $filter) {
1763                                                next PATH;
1764                                        }
1765                                        my $sfx = substr($path, length($filter));
1766                                        if ($sfx !~ /^\/.*\.git$/) {
1767                                                next PATH;
1768                                        }
1769                                        # is a fork, don't include it in
1770                                        # the list
1771                                        next PROJECT;
1772                                }
1773                        }
1774                        if (check_export_ok("$projectroot/$path")) {
1775                                my $pr = {
1776                                        path => $path,
1777                                        owner => to_utf8($owner),
1778                                };
1779                                push @list, $pr;
1780                                (my $forks_path = $path) =~ s/\.git$//;
1781                                $paths{$forks_path}++;
1782                        }
1783                }
1784                close $fd;
1785        }
1786        return @list;
1787}
1788
1789our $gitweb_project_owner = undef;
1790sub git_get_project_list_from_file {
1791
1792        return if (defined $gitweb_project_owner);
1793
1794        $gitweb_project_owner = {};
1795        # read from file (url-encoded):
1796        # 'git%2Fgit.git Linus+Torvalds'
1797        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1798        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1799        if (-f $projects_list) {
1800                open (my $fd , $projects_list);
1801                while (my $line = <$fd>) {
1802                        chomp $line;
1803                        my ($pr, $ow) = split ' ', $line;
1804                        $pr = unescape($pr);
1805                        $ow = unescape($ow);
1806                        $gitweb_project_owner->{$pr} = to_utf8($ow);
1807                }
1808                close $fd;
1809        }
1810}
1811
1812sub git_get_project_owner {
1813        my $project = shift;
1814        my $owner;
1815
1816        return undef unless $project;
1817        $git_dir = "$projectroot/$project";
1818
1819        if (!defined $gitweb_project_owner) {
1820                git_get_project_list_from_file();
1821        }
1822
1823        if (exists $gitweb_project_owner->{$project}) {
1824                $owner = $gitweb_project_owner->{$project};
1825        }
1826        if (!defined $owner){
1827                $owner = git_get_project_config('owner');
1828        }
1829        if (!defined $owner) {
1830                $owner = get_file_owner("$git_dir");
1831        }
1832
1833        return $owner;
1834}
1835
1836sub git_get_last_activity {
1837        my ($path) = @_;
1838        my $fd;
1839
1840        $git_dir = "$projectroot/$path";
1841        open($fd, "-|", git_cmd(), 'for-each-ref',
1842             '--format=%(committer)',
1843             '--sort=-committerdate',
1844             '--count=1',
1845             'refs/heads') or return;
1846        my $most_recent = <$fd>;
1847        close $fd or return;
1848        if (defined $most_recent &&
1849            $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1850                my $timestamp = $1;
1851                my $age = time - $timestamp;
1852                return ($age, age_string($age));
1853        }
1854        return (undef, undef);
1855}
1856
1857sub git_get_references {
1858        my $type = shift || "";
1859        my %refs;
1860        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1861        # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1862        open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
1863                ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1864                or return;
1865
1866        while (my $line = <$fd>) {
1867                chomp $line;
1868                if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
1869                        if (defined $refs{$1}) {
1870                                push @{$refs{$1}}, $2;
1871                        } else {
1872                                $refs{$1} = [ $2 ];
1873                        }
1874                }
1875        }
1876        close $fd or return;
1877        return \%refs;
1878}
1879
1880sub git_get_rev_name_tags {
1881        my $hash = shift || return undef;
1882
1883        open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
1884                or return;
1885        my $name_rev = <$fd>;
1886        close $fd;
1887
1888        if ($name_rev =~ m|^$hash tags/(.*)$|) {
1889                return $1;
1890        } else {
1891                # catches also '$hash undefined' output
1892                return undef;
1893        }
1894}
1895
1896## ----------------------------------------------------------------------
1897## parse to hash functions
1898
1899sub parse_date {
1900        my $epoch = shift;
1901        my $tz = shift || "-0000";
1902
1903        my %date;
1904        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1905        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1906        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
1907        $date{'hour'} = $hour;
1908        $date{'minute'} = $min;
1909        $date{'mday'} = $mday;
1910        $date{'day'} = $days[$wday];
1911        $date{'month'} = $months[$mon];
1912        $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
1913                             $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
1914        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
1915                             $mday, $months[$mon], $hour ,$min;
1916        $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
1917                             1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
1918
1919        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
1920        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
1921        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
1922        $date{'hour_local'} = $hour;
1923        $date{'minute_local'} = $min;
1924        $date{'tz_local'} = $tz;
1925        $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
1926                                  1900+$year, $mon+1, $mday,
1927                                  $hour, $min, $sec, $tz);
1928        return %date;
1929}
1930
1931sub parse_tag {
1932        my $tag_id = shift;
1933        my %tag;
1934        my @comment;
1935
1936        open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
1937        $tag{'id'} = $tag_id;
1938        while (my $line = <$fd>) {
1939                chomp $line;
1940                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
1941                        $tag{'object'} = $1;
1942                } elsif ($line =~ m/^type (.+)$/) {
1943                        $tag{'type'} = $1;
1944                } elsif ($line =~ m/^tag (.+)$/) {
1945                        $tag{'name'} = $1;
1946                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
1947                        $tag{'author'} = $1;
1948                        $tag{'epoch'} = $2;
1949                        $tag{'tz'} = $3;
1950                } elsif ($line =~ m/--BEGIN/) {
1951                        push @comment, $line;
1952                        last;
1953                } elsif ($line eq "") {
1954                        last;
1955                }
1956        }
1957        push @comment, <$fd>;
1958        $tag{'comment'} = \@comment;
1959        close $fd or return;
1960        if (!defined $tag{'name'}) {
1961                return
1962        };
1963        return %tag
1964}
1965
1966sub parse_commit_text {
1967        my ($commit_text, $withparents) = @_;
1968        my @commit_lines = split '\n', $commit_text;
1969        my %co;
1970
1971        pop @commit_lines; # Remove '\0'
1972
1973        if (! @commit_lines) {
1974                return;
1975        }
1976
1977        my $header = shift @commit_lines;
1978        if ($header !~ m/^[0-9a-fA-F]{40}/) {
1979                return;
1980        }
1981        ($co{'id'}, my @parents) = split ' ', $header;
1982        while (my $line = shift @commit_lines) {
1983                last if $line eq "\n";
1984                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
1985                        $co{'tree'} = $1;
1986                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
1987                        push @parents, $1;
1988                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
1989                        $co{'author'} = $1;
1990                        $co{'author_epoch'} = $2;
1991                        $co{'author_tz'} = $3;
1992                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
1993                                $co{'author_name'}  = $1;
1994                                $co{'author_email'} = $2;
1995                        } else {
1996                                $co{'author_name'} = $co{'author'};
1997                        }
1998                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
1999                        $co{'committer'} = $1;
2000                        $co{'committer_epoch'} = $2;
2001                        $co{'committer_tz'} = $3;
2002                        $co{'committer_name'} = $co{'committer'};
2003                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2004                                $co{'committer_name'}  = $1;
2005                                $co{'committer_email'} = $2;
2006                        } else {
2007                                $co{'committer_name'} = $co{'committer'};
2008                        }
2009                }
2010        }
2011        if (!defined $co{'tree'}) {
2012                return;
2013        };
2014        $co{'parents'} = \@parents;
2015        $co{'parent'} = $parents[0];
2016
2017        foreach my $title (@commit_lines) {
2018                $title =~ s/^    //;
2019                if ($title ne "") {
2020                        $co{'title'} = chop_str($title, 80, 5);
2021                        # remove leading stuff of merges to make the interesting part visible
2022                        if (length($title) > 50) {
2023                                $title =~ s/^Automatic //;
2024                                $title =~ s/^merge (of|with) /Merge ... /i;
2025                                if (length($title) > 50) {
2026                                        $title =~ s/(http|rsync):\/\///;
2027                                }
2028                                if (length($title) > 50) {
2029                                        $title =~ s/(master|www|rsync)\.//;
2030                                }
2031                                if (length($title) > 50) {
2032                                        $title =~ s/kernel.org:?//;
2033                                }
2034                                if (length($title) > 50) {
2035                                        $title =~ s/\/pub\/scm//;
2036                                }
2037                        }
2038                        $co{'title_short'} = chop_str($title, 50, 5);
2039                        last;
2040                }
2041        }
2042        if ($co{'title'} eq "") {
2043                $co{'title'} = $co{'title_short'} = '(no commit message)';
2044        }
2045        # remove added spaces
2046        foreach my $line (@commit_lines) {
2047                $line =~ s/^    //;
2048        }
2049        $co{'comment'} = \@commit_lines;
2050
2051        my $age = time - $co{'committer_epoch'};
2052        $co{'age'} = $age;
2053        $co{'age_string'} = age_string($age);
2054        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2055        if ($age > 60*60*24*7*2) {
2056                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2057                $co{'age_string_age'} = $co{'age_string'};
2058        } else {
2059                $co{'age_string_date'} = $co{'age_string'};
2060                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2061        }
2062        return %co;
2063}
2064
2065sub parse_commit {
2066        my ($commit_id) = @_;
2067        my %co;
2068
2069        local $/ = "\0";
2070
2071        open my $fd, "-|", git_cmd(), "rev-list",
2072                "--parents",
2073                "--header",
2074                "--max-count=1",
2075                $commit_id,
2076                "--",
2077                or die_error(undef, "Open git-rev-list failed");
2078        %co = parse_commit_text(<$fd>, 1);
2079        close $fd;
2080
2081        return %co;
2082}
2083
2084sub parse_commits {
2085        my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2086        my @cos;
2087
2088        $maxcount ||= 1;
2089        $skip ||= 0;
2090
2091        local $/ = "\0";
2092
2093        open my $fd, "-|", git_cmd(), "rev-list",
2094                "--header",
2095                @args,
2096                ("--max-count=" . $maxcount),
2097                ("--skip=" . $skip),
2098                @extra_options,
2099                $commit_id,
2100                "--",
2101                ($filename ? ($filename) : ())
2102                or die_error(undef, "Open git-rev-list failed");
2103        while (my $line = <$fd>) {
2104                my %co = parse_commit_text($line);
2105                push @cos, \%co;
2106        }
2107        close $fd;
2108
2109        return wantarray ? @cos : \@cos;
2110}
2111
2112# parse ref from ref_file, given by ref_id, with given type
2113sub parse_ref {
2114        my $ref_file = shift;
2115        my $ref_id = shift;
2116        my $type = shift || git_get_type($ref_id);
2117        my %ref_item;
2118
2119        $ref_item{'type'} = $type;
2120        $ref_item{'id'} = $ref_id;
2121        $ref_item{'epoch'} = 0;
2122        $ref_item{'age'} = "unknown";
2123        if ($type eq "tag") {
2124                my %tag = parse_tag($ref_id);
2125                $ref_item{'comment'} = $tag{'comment'};
2126                if ($tag{'type'} eq "commit") {
2127                        my %co = parse_commit($tag{'object'});
2128                        $ref_item{'epoch'} = $co{'committer_epoch'};
2129                        $ref_item{'age'} = $co{'age_string'};
2130                } elsif (defined($tag{'epoch'})) {
2131                        my $age = time - $tag{'epoch'};
2132                        $ref_item{'epoch'} = $tag{'epoch'};
2133                        $ref_item{'age'} = age_string($age);
2134                }
2135                $ref_item{'reftype'} = $tag{'type'};
2136                $ref_item{'name'} = $tag{'name'};
2137                $ref_item{'refid'} = $tag{'object'};
2138        } elsif ($type eq "commit"){
2139                my %co = parse_commit($ref_id);
2140                $ref_item{'reftype'} = "commit";
2141                $ref_item{'name'} = $ref_file;
2142                $ref_item{'title'} = $co{'title'};
2143                $ref_item{'refid'} = $ref_id;
2144                $ref_item{'epoch'} = $co{'committer_epoch'};
2145                $ref_item{'age'} = $co{'age_string'};
2146        } else {
2147                $ref_item{'reftype'} = $type;
2148                $ref_item{'name'} = $ref_file;
2149                $ref_item{'refid'} = $ref_id;
2150        }
2151
2152        return %ref_item;
2153}
2154
2155# parse line of git-diff-tree "raw" output
2156sub parse_difftree_raw_line {
2157        my $line = shift;
2158        my %res;
2159
2160        # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2161        # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2162        if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2163                $res{'from_mode'} = $1;
2164                $res{'to_mode'} = $2;
2165                $res{'from_id'} = $3;
2166                $res{'to_id'} = $4;
2167                $res{'status'} = $5;
2168                $res{'similarity'} = $6;
2169                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2170                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2171                } else {
2172                        $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2173                }
2174        }
2175        # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2176        # combined diff (for merge commit)
2177        elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2178                $res{'nparents'}  = length($1);
2179                $res{'from_mode'} = [ split(' ', $2) ];
2180                $res{'to_mode'} = pop @{$res{'from_mode'}};
2181                $res{'from_id'} = [ split(' ', $3) ];
2182                $res{'to_id'} = pop @{$res{'from_id'}};
2183                $res{'status'} = [ split('', $4) ];
2184                $res{'to_file'} = unquote($5);
2185        }
2186        # 'c512b523472485aef4fff9e57b229d9d243c967f'
2187        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2188                $res{'commit'} = $1;
2189        }
2190
2191        return wantarray ? %res : \%res;
2192}
2193
2194# wrapper: return parsed line of git-diff-tree "raw" output
2195# (the argument might be raw line, or parsed info)
2196sub parsed_difftree_line {
2197        my $line_or_ref = shift;
2198
2199        if (ref($line_or_ref) eq "HASH") {
2200                # pre-parsed (or generated by hand)
2201                return $line_or_ref;
2202        } else {
2203                return parse_difftree_raw_line($line_or_ref);
2204        }
2205}
2206
2207# parse line of git-ls-tree output
2208sub parse_ls_tree_line ($;%) {
2209        my $line = shift;
2210        my %opts = @_;
2211        my %res;
2212
2213        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2214        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2215
2216        $res{'mode'} = $1;
2217        $res{'type'} = $2;
2218        $res{'hash'} = $3;
2219        if ($opts{'-z'}) {
2220                $res{'name'} = $4;
2221        } else {
2222                $res{'name'} = unquote($4);
2223        }
2224
2225        return wantarray ? %res : \%res;
2226}
2227
2228# generates _two_ hashes, references to which are passed as 2 and 3 argument
2229sub parse_from_to_diffinfo {
2230        my ($diffinfo, $from, $to, @parents) = @_;
2231
2232        if ($diffinfo->{'nparents'}) {
2233                # combined diff
2234                $from->{'file'} = [];
2235                $from->{'href'} = [];
2236                fill_from_file_info($diffinfo, @parents)
2237                        unless exists $diffinfo->{'from_file'};
2238                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2239                        $from->{'file'}[$i] =
2240                                defined $diffinfo->{'from_file'}[$i] ?
2241                                        $diffinfo->{'from_file'}[$i] :
2242                                        $diffinfo->{'to_file'};
2243                        if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2244                                $from->{'href'}[$i] = href(action=>"blob",
2245                                                           hash_base=>$parents[$i],
2246                                                           hash=>$diffinfo->{'from_id'}[$i],
2247                                                           file_name=>$from->{'file'}[$i]);
2248                        } else {
2249                                $from->{'href'}[$i] = undef;
2250                        }
2251                }
2252        } else {
2253                # ordinary (not combined) diff
2254                $from->{'file'} = $diffinfo->{'from_file'};
2255                if ($diffinfo->{'status'} ne "A") { # not new (added) file
2256                        $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2257                                               hash=>$diffinfo->{'from_id'},
2258                                               file_name=>$from->{'file'});
2259                } else {
2260                        delete $from->{'href'};
2261                }
2262        }
2263
2264        $to->{'file'} = $diffinfo->{'to_file'};
2265        if (!is_deleted($diffinfo)) { # file exists in result
2266                $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2267                                     hash=>$diffinfo->{'to_id'},
2268                                     file_name=>$to->{'file'});
2269        } else {
2270                delete $to->{'href'};
2271        }
2272}
2273
2274## ......................................................................
2275## parse to array of hashes functions
2276
2277sub git_get_heads_list {
2278        my $limit = shift;
2279        my @headslist;
2280
2281        open my $fd, '-|', git_cmd(), 'for-each-ref',
2282                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2283                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2284                'refs/heads'
2285                or return;
2286        while (my $line = <$fd>) {
2287                my %ref_item;
2288
2289                chomp $line;
2290                my ($refinfo, $committerinfo) = split(/\0/, $line);
2291                my ($hash, $name, $title) = split(' ', $refinfo, 3);
2292                my ($committer, $epoch, $tz) =
2293                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2294                $ref_item{'fullname'}  = $name;
2295                $name =~ s!^refs/heads/!!;
2296
2297                $ref_item{'name'}  = $name;
2298                $ref_item{'id'}    = $hash;
2299                $ref_item{'title'} = $title || '(no commit message)';
2300                $ref_item{'epoch'} = $epoch;
2301                if ($epoch) {
2302                        $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2303                } else {
2304                        $ref_item{'age'} = "unknown";
2305                }
2306
2307                push @headslist, \%ref_item;
2308        }
2309        close $fd;
2310
2311        return wantarray ? @headslist : \@headslist;
2312}
2313
2314sub git_get_tags_list {
2315        my $limit = shift;
2316        my @tagslist;
2317
2318        open my $fd, '-|', git_cmd(), 'for-each-ref',
2319                ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2320                '--format=%(objectname) %(objecttype) %(refname) '.
2321                '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2322                'refs/tags'
2323                or return;
2324        while (my $line = <$fd>) {
2325                my %ref_item;
2326
2327                chomp $line;
2328                my ($refinfo, $creatorinfo) = split(/\0/, $line);
2329                my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2330                my ($creator, $epoch, $tz) =
2331                        ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2332                $ref_item{'fullname'} = $name;
2333                $name =~ s!^refs/tags/!!;
2334
2335                $ref_item{'type'} = $type;
2336                $ref_item{'id'} = $id;
2337                $ref_item{'name'} = $name;
2338                if ($type eq "tag") {
2339                        $ref_item{'subject'} = $title;
2340                        $ref_item{'reftype'} = $reftype;
2341                        $ref_item{'refid'}   = $refid;
2342                } else {
2343                        $ref_item{'reftype'} = $type;
2344                        $ref_item{'refid'}   = $id;
2345                }
2346
2347                if ($type eq "tag" || $type eq "commit") {
2348                        $ref_item{'epoch'} = $epoch;
2349                        if ($epoch) {
2350                                $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2351                        } else {
2352                                $ref_item{'age'} = "unknown";
2353                        }
2354                }
2355
2356                push @tagslist, \%ref_item;
2357        }
2358        close $fd;
2359
2360        return wantarray ? @tagslist : \@tagslist;
2361}
2362
2363## ----------------------------------------------------------------------
2364## filesystem-related functions
2365
2366sub get_file_owner {
2367        my $path = shift;
2368
2369        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2370        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2371        if (!defined $gcos) {
2372                return undef;
2373        }
2374        my $owner = $gcos;
2375        $owner =~ s/[,;].*$//;
2376        return to_utf8($owner);
2377}
2378
2379## ......................................................................
2380## mimetype related functions
2381
2382sub mimetype_guess_file {
2383        my $filename = shift;
2384        my $mimemap = shift;
2385        -r $mimemap or return undef;
2386
2387        my %mimemap;
2388        open(MIME, $mimemap) or return undef;
2389        while (<MIME>) {
2390                next if m/^#/; # skip comments
2391                my ($mime, $exts) = split(/\t+/);
2392                if (defined $exts) {
2393                        my @exts = split(/\s+/, $exts);
2394                        foreach my $ext (@exts) {
2395                                $mimemap{$ext} = $mime;
2396                        }
2397                }
2398        }
2399        close(MIME);
2400
2401        $filename =~ /\.([^.]*)$/;
2402        return $mimemap{$1};
2403}
2404
2405sub mimetype_guess {
2406        my $filename = shift;
2407        my $mime;
2408        $filename =~ /\./ or return undef;
2409
2410        if ($mimetypes_file) {
2411                my $file = $mimetypes_file;
2412                if ($file !~ m!^/!) { # if it is relative path
2413                        # it is relative to project
2414                        $file = "$projectroot/$project/$file";
2415                }
2416                $mime = mimetype_guess_file($filename, $file);
2417        }
2418        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2419        return $mime;
2420}
2421
2422sub blob_mimetype {
2423        my $fd = shift;
2424        my $filename = shift;
2425
2426        if ($filename) {
2427                my $mime = mimetype_guess($filename);
2428                $mime and return $mime;
2429        }
2430
2431        # just in case
2432        return $default_blob_plain_mimetype unless $fd;
2433
2434        if (-T $fd) {
2435                return 'text/plain' .
2436                       ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
2437        } elsif (! $filename) {
2438                return 'application/octet-stream';
2439        } elsif ($filename =~ m/\.png$/i) {
2440                return 'image/png';
2441        } elsif ($filename =~ m/\.gif$/i) {
2442                return 'image/gif';
2443        } elsif ($filename =~ m/\.jpe?g$/i) {
2444                return 'image/jpeg';
2445        } else {
2446                return 'application/octet-stream';
2447        }
2448}
2449
2450## ======================================================================
2451## functions printing HTML: header, footer, error page
2452
2453sub git_header_html {
2454        my $status = shift || "200 OK";
2455        my $expires = shift;
2456
2457        my $title = "$site_name";
2458        if (defined $project) {
2459                $title .= " - " . to_utf8($project);
2460                if (defined $action) {
2461                        $title .= "/$action";
2462                        if (defined $file_name) {
2463                                $title .= " - " . esc_path($file_name);
2464                                if ($action eq "tree" && $file_name !~ m|/$|) {
2465                                        $title .= "/";
2466                                }
2467                        }
2468                }
2469        }
2470        my $content_type;
2471        # require explicit support from the UA if we are to send the page as
2472        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2473        # we have to do this because MSIE sometimes globs '*/*', pretending to
2474        # support xhtml+xml but choking when it gets what it asked for.
2475        if (defined $cgi->http('HTTP_ACCEPT') &&
2476            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2477            $cgi->Accept('application/xhtml+xml') != 0) {
2478                $content_type = 'application/xhtml+xml';
2479        } else {
2480                $content_type = 'text/html';
2481        }
2482        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2483                           -status=> $status, -expires => $expires);
2484        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2485        print <<EOF;
2486<?xml version="1.0" encoding="utf-8"?>
2487<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2488<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2489<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2490<!-- git core binaries version $git_version -->
2491<head>
2492<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2493<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2494<meta name="robots" content="index, nofollow"/>
2495<title>$title</title>
2496EOF
2497# print out each stylesheet that exist
2498        if (defined $stylesheet) {
2499#provides backwards capability for those people who define style sheet in a config file
2500                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2501        } else {
2502                foreach my $stylesheet (@stylesheets) {
2503                        next unless $stylesheet;
2504                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2505                }
2506        }
2507        if (defined $project) {
2508                printf('<link rel="alternate" title="%s log RSS feed" '.
2509                       'href="%s" type="application/rss+xml" />'."\n",
2510                       esc_param($project), href(action=>"rss"));
2511                printf('<link rel="alternate" title="%s log RSS feed (no merges)" '.
2512                       'href="%s" type="application/rss+xml" />'."\n",
2513                       esc_param($project), href(action=>"rss",
2514                                                 extra_options=>"--no-merges"));
2515                printf('<link rel="alternate" title="%s log Atom feed" '.
2516                       'href="%s" type="application/atom+xml" />'."\n",
2517                       esc_param($project), href(action=>"atom"));
2518                printf('<link rel="alternate" title="%s log Atom feed (no merges)" '.
2519                       'href="%s" type="application/atom+xml" />'."\n",
2520                       esc_param($project), href(action=>"atom",
2521                                                 extra_options=>"--no-merges"));
2522        } else {
2523                printf('<link rel="alternate" title="%s projects list" '.
2524                       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
2525                       $site_name, href(project=>undef, action=>"project_index"));
2526                printf('<link rel="alternate" title="%s projects feeds" '.
2527                       'href="%s" type="text/x-opml"/>'."\n",
2528                       $site_name, href(project=>undef, action=>"opml"));
2529        }
2530        if (defined $favicon) {
2531                print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
2532        }
2533
2534        print "</head>\n" .
2535              "<body>\n";
2536
2537        if (-f $site_header) {
2538                open (my $fd, $site_header);
2539                print <$fd>;
2540                close $fd;
2541        }
2542
2543        print "<div class=\"page_header\">\n" .
2544              $cgi->a({-href => esc_url($logo_url),
2545                       -title => $logo_label},
2546                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2547        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2548        if (defined $project) {
2549                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2550                if (defined $action) {
2551                        print " / $action";
2552                }
2553                print "\n";
2554        }
2555        print "</div>\n";
2556
2557        my ($have_search) = gitweb_check_feature('search');
2558        if ((defined $project) && ($have_search)) {
2559                if (!defined $searchtext) {
2560                        $searchtext = "";
2561                }
2562                my $search_hash;
2563                if (defined $hash_base) {
2564                        $search_hash = $hash_base;
2565                } elsif (defined $hash) {
2566                        $search_hash = $hash;
2567                } else {
2568                        $search_hash = "HEAD";
2569                }
2570                my $action = $my_uri;
2571                my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2572                if ($use_pathinfo) {
2573                        $action .= "/".esc_url($project);
2574                } else {
2575                        $cgi->param("p", $project);
2576                }
2577                $cgi->param("a", "search");
2578                $cgi->param("h", $search_hash);
2579                print $cgi->startform(-method => "get", -action => $action) .
2580                      "<div class=\"search\">\n" .
2581                      (!$use_pathinfo && $cgi->hidden(-name => "p") . "\n") .
2582                      $cgi->hidden(-name => "a") . "\n" .
2583                      $cgi->hidden(-name => "h") . "\n" .
2584                      $cgi->popup_menu(-name => 'st', -default => 'commit',
2585                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2586                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2587                      " search:\n",
2588                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2589                      "<span title=\"Extended regular expression\">" .
2590                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2591                                     -checked => $search_use_regexp) .
2592                      "</span>" .
2593                      "</div>" .
2594                      $cgi->end_form() . "\n";
2595        }
2596}
2597
2598sub git_footer_html {
2599        print "<div class=\"page_footer\">\n";
2600        if (defined $project) {
2601                my $descr = git_get_project_description($project);
2602                if (defined $descr) {
2603                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2604                }
2605                print $cgi->a({-href => href(action=>"rss"),
2606                              -class => "rss_logo"}, "RSS") . " ";
2607                print $cgi->a({-href => href(action=>"atom"),
2608                              -class => "rss_logo"}, "Atom") . "\n";
2609        } else {
2610                print $cgi->a({-href => href(project=>undef, action=>"opml"),
2611                              -class => "rss_logo"}, "OPML") . " ";
2612                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2613                              -class => "rss_logo"}, "TXT") . "\n";
2614        }
2615        print "</div>\n" ;
2616
2617        if (-f $site_footer) {
2618                open (my $fd, $site_footer);
2619                print <$fd>;
2620                close $fd;
2621        }
2622
2623        print "</body>\n" .
2624              "</html>";
2625}
2626
2627sub die_error {
2628        my $status = shift || "403 Forbidden";
2629        my $error = shift || "Malformed query, file missing or permission denied";
2630
2631        git_header_html($status);
2632        print <<EOF;
2633<div class="page_body">
2634<br /><br />
2635$status - $error
2636<br />
2637</div>
2638EOF
2639        git_footer_html();
2640        exit;
2641}
2642
2643## ----------------------------------------------------------------------
2644## functions printing or outputting HTML: navigation
2645
2646sub git_print_page_nav {
2647        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2648        $extra = '' if !defined $extra; # pager or formats
2649
2650        my @navs = qw(summary shortlog log commit commitdiff tree);
2651        if ($suppress) {
2652                @navs = grep { $_ ne $suppress } @navs;
2653        }
2654
2655        my %arg = map { $_ => {action=>$_} } @navs;
2656        if (defined $head) {
2657                for (qw(commit commitdiff)) {
2658                        $arg{$_}{'hash'} = $head;
2659                }
2660                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2661                        for (qw(shortlog log)) {
2662                                $arg{$_}{'hash'} = $head;
2663                        }
2664                }
2665        }
2666        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2667        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2668
2669        print "<div class=\"page_nav\">\n" .
2670                (join " | ",
2671                 map { $_ eq $current ?
2672                       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2673                 } @navs);
2674        print "<br/>\n$extra<br/>\n" .
2675              "</div>\n";
2676}
2677
2678sub format_paging_nav {
2679        my ($action, $hash, $head, $page, $nrevs) = @_;
2680        my $paging_nav;
2681
2682
2683        if ($hash ne $head || $page) {
2684                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2685        } else {
2686                $paging_nav .= "HEAD";
2687        }
2688
2689        if ($page > 0) {
2690                $paging_nav .= " &sdot; " .
2691                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
2692                                 -accesskey => "p", -title => "Alt-p"}, "prev");
2693        } else {
2694                $paging_nav .= " &sdot; prev";
2695        }
2696
2697        if ($nrevs >= (100 * ($page+1)-1)) {
2698                $paging_nav .= " &sdot; " .
2699                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
2700                                 -accesskey => "n", -title => "Alt-n"}, "next");
2701        } else {
2702                $paging_nav .= " &sdot; next";
2703        }
2704
2705        return $paging_nav;
2706}
2707
2708## ......................................................................
2709## functions printing or outputting HTML: div
2710
2711sub git_print_header_div {
2712        my ($action, $title, $hash, $hash_base) = @_;
2713        my %args = ();
2714
2715        $args{'action'} = $action;
2716        $args{'hash'} = $hash if $hash;
2717        $args{'hash_base'} = $hash_base if $hash_base;
2718
2719        print "<div class=\"header\">\n" .
2720              $cgi->a({-href => href(%args), -class => "title"},
2721              $title ? $title : $action) .
2722              "\n</div>\n";
2723}
2724
2725#sub git_print_authorship (\%) {
2726sub git_print_authorship {
2727        my $co = shift;
2728
2729        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2730        print "<div class=\"author_date\">" .
2731              esc_html($co->{'author_name'}) .
2732              " [$ad{'rfc2822'}";
2733        if ($ad{'hour_local'} < 6) {
2734                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2735                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2736        } else {
2737                printf(" (%02d:%02d %s)",
2738                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2739        }
2740        print "]</div>\n";
2741}
2742
2743sub git_print_page_path {
2744        my $name = shift;
2745        my $type = shift;
2746        my $hb = shift;
2747
2748
2749        print "<div class=\"page_path\">";
2750        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2751                      -title => 'tree root'}, to_utf8("[$project]"));
2752        print " / ";
2753        if (defined $name) {
2754                my @dirname = split '/', $name;
2755                my $basename = pop @dirname;
2756                my $fullname = '';
2757
2758                foreach my $dir (@dirname) {
2759                        $fullname .= ($fullname ? '/' : '') . $dir;
2760                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2761                                                     hash_base=>$hb),
2762                                      -title => $fullname}, esc_path($dir));
2763                        print " / ";
2764                }
2765                if (defined $type && $type eq 'blob') {
2766                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2767                                                     hash_base=>$hb),
2768                                      -title => $name}, esc_path($basename));
2769                } elsif (defined $type && $type eq 'tree') {
2770                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2771                                                     hash_base=>$hb),
2772                                      -title => $name}, esc_path($basename));
2773                        print " / ";
2774                } else {
2775                        print esc_path($basename);
2776                }
2777        }
2778        print "<br/></div>\n";
2779}
2780
2781# sub git_print_log (\@;%) {
2782sub git_print_log ($;%) {
2783        my $log = shift;
2784        my %opts = @_;
2785
2786        if ($opts{'-remove_title'}) {
2787                # remove title, i.e. first line of log
2788                shift @$log;
2789        }
2790        # remove leading empty lines
2791        while (defined $log->[0] && $log->[0] eq "") {
2792                shift @$log;
2793        }
2794
2795        # print log
2796        my $signoff = 0;
2797        my $empty = 0;
2798        foreach my $line (@$log) {
2799                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2800                        $signoff = 1;
2801                        $empty = 0;
2802                        if (! $opts{'-remove_signoff'}) {
2803                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2804                                next;
2805                        } else {
2806                                # remove signoff lines
2807                                next;
2808                        }
2809                } else {
2810                        $signoff = 0;
2811                }
2812
2813                # print only one empty line
2814                # do not print empty line after signoff
2815                if ($line eq "") {
2816                        next if ($empty || $signoff);
2817                        $empty = 1;
2818                } else {
2819                        $empty = 0;
2820                }
2821
2822                print format_log_line_html($line) . "<br/>\n";
2823        }
2824
2825        if ($opts{'-final_empty_line'}) {
2826                # end with single empty line
2827                print "<br/>\n" unless $empty;
2828        }
2829}
2830
2831# return link target (what link points to)
2832sub git_get_link_target {
2833        my $hash = shift;
2834        my $link_target;
2835
2836        # read link
2837        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2838                or return;
2839        {
2840                local $/;
2841                $link_target = <$fd>;
2842        }
2843        close $fd
2844                or return;
2845
2846        return $link_target;
2847}
2848
2849# given link target, and the directory (basedir) the link is in,
2850# return target of link relative to top directory (top tree);
2851# return undef if it is not possible (including absolute links).
2852sub normalize_link_target {
2853        my ($link_target, $basedir, $hash_base) = @_;
2854
2855        # we can normalize symlink target only if $hash_base is provided
2856        return unless $hash_base;
2857
2858        # absolute symlinks (beginning with '/') cannot be normalized
2859        return if (substr($link_target, 0, 1) eq '/');
2860
2861        # normalize link target to path from top (root) tree (dir)
2862        my $path;
2863        if ($basedir) {
2864                $path = $basedir . '/' . $link_target;
2865        } else {
2866                # we are in top (root) tree (dir)
2867                $path = $link_target;
2868        }
2869
2870        # remove //, /./, and /../
2871        my @path_parts;
2872        foreach my $part (split('/', $path)) {
2873                # discard '.' and ''
2874                next if (!$part || $part eq '.');
2875                # handle '..'
2876                if ($part eq '..') {
2877                        if (@path_parts) {
2878                                pop @path_parts;
2879                        } else {
2880                                # link leads outside repository (outside top dir)
2881                                return;
2882                        }
2883                } else {
2884                        push @path_parts, $part;
2885                }
2886        }
2887        $path = join('/', @path_parts);
2888
2889        return $path;
2890}
2891
2892# print tree entry (row of git_tree), but without encompassing <tr> element
2893sub git_print_tree_entry {
2894        my ($t, $basedir, $hash_base, $have_blame) = @_;
2895
2896        my %base_key = ();
2897        $base_key{'hash_base'} = $hash_base if defined $hash_base;
2898
2899        # The format of a table row is: mode list link.  Where mode is
2900        # the mode of the entry, list is the name of the entry, an href,
2901        # and link is the action links of the entry.
2902
2903        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
2904        if ($t->{'type'} eq "blob") {
2905                print "<td class=\"list\">" .
2906                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2907                                               file_name=>"$basedir$t->{'name'}", %base_key),
2908                                -class => "list"}, esc_path($t->{'name'}));
2909                if (S_ISLNK(oct $t->{'mode'})) {
2910                        my $link_target = git_get_link_target($t->{'hash'});
2911                        if ($link_target) {
2912                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
2913                                if (defined $norm_target) {
2914                                        print " -> " .
2915                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
2916                                                                     file_name=>$norm_target),
2917                                                       -title => $norm_target}, esc_path($link_target));
2918                                } else {
2919                                        print " -> " . esc_path($link_target);
2920                                }
2921                        }
2922                }
2923                print "</td>\n";
2924                print "<td class=\"link\">";
2925                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2926                                             file_name=>"$basedir$t->{'name'}", %base_key)},
2927                              "blob");
2928                if ($have_blame) {
2929                        print " | " .
2930                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
2931                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
2932                                      "blame");
2933                }
2934                if (defined $hash_base) {
2935                        print " | " .
2936                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
2937                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
2938                                      "history");
2939                }
2940                print " | " .
2941                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
2942                                               file_name=>"$basedir$t->{'name'}")},
2943                                "raw");
2944                print "</td>\n";
2945
2946        } elsif ($t->{'type'} eq "tree") {
2947                print "<td class=\"list\">";
2948                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
2949                                             file_name=>"$basedir$t->{'name'}", %base_key)},
2950                              esc_path($t->{'name'}));
2951                print "</td>\n";
2952                print "<td class=\"link\">";
2953                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
2954                                             file_name=>"$basedir$t->{'name'}", %base_key)},
2955                              "tree");
2956                if (defined $hash_base) {
2957                        print " | " .
2958                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
2959                                                     file_name=>"$basedir$t->{'name'}")},
2960                                      "history");
2961                }
2962                print "</td>\n";
2963        } else {
2964                # unknown object: we can only present history for it
2965                # (this includes 'commit' object, i.e. submodule support)
2966                print "<td class=\"list\">" .
2967                      esc_path($t->{'name'}) .
2968                      "</td>\n";
2969                print "<td class=\"link\">";
2970                if (defined $hash_base) {
2971                        print $cgi->a({-href => href(action=>"history",
2972                                                     hash_base=>$hash_base,
2973                                                     file_name=>"$basedir$t->{'name'}")},
2974                                      "history");
2975                }
2976                print "</td>\n";
2977        }
2978}
2979
2980## ......................................................................
2981## functions printing large fragments of HTML
2982
2983# get pre-image filenames for merge (combined) diff
2984sub fill_from_file_info {
2985        my ($diff, @parents) = @_;
2986
2987        $diff->{'from_file'} = [ ];
2988        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
2989        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
2990                if ($diff->{'status'}[$i] eq 'R' ||
2991                    $diff->{'status'}[$i] eq 'C') {
2992                        $diff->{'from_file'}[$i] =
2993                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
2994                }
2995        }
2996
2997        return $diff;
2998}
2999
3000# is current raw difftree line of file deletion
3001sub is_deleted {
3002        my $diffinfo = shift;
3003
3004        return $diffinfo->{'to_id'} eq ('0' x 40);
3005}
3006
3007# does patch correspond to [previous] difftree raw line
3008# $diffinfo  - hashref of parsed raw diff format
3009# $patchinfo - hashref of parsed patch diff format
3010#              (the same keys as in $diffinfo)
3011sub is_patch_split {
3012        my ($diffinfo, $patchinfo) = @_;
3013
3014        return defined $diffinfo && defined $patchinfo
3015                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3016}
3017
3018
3019sub git_difftree_body {
3020        my ($difftree, $hash, @parents) = @_;
3021        my ($parent) = $parents[0];
3022        my ($have_blame) = gitweb_check_feature('blame');
3023        print "<div class=\"list_head\">\n";
3024        if ($#{$difftree} > 10) {
3025                print(($#{$difftree} + 1) . " files changed:\n");
3026        }
3027        print "</div>\n";
3028
3029        print "<table class=\"" .
3030              (@parents > 1 ? "combined " : "") .
3031              "diff_tree\">\n";
3032
3033        # header only for combined diff in 'commitdiff' view
3034        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3035        if ($has_header) {
3036                # table header
3037                print "<thead><tr>\n" .
3038                       "<th></th><th></th>\n"; # filename, patchN link
3039                for (my $i = 0; $i < @parents; $i++) {
3040                        my $par = $parents[$i];
3041                        print "<th>" .
3042                              $cgi->a({-href => href(action=>"commitdiff",
3043                                                     hash=>$hash, hash_parent=>$par),
3044                                       -title => 'commitdiff to parent number ' .
3045                                                  ($i+1) . ': ' . substr($par,0,7)},
3046                                      $i+1) .
3047                              "&nbsp;</th>\n";
3048                }
3049                print "</tr></thead>\n<tbody>\n";
3050        }
3051
3052        my $alternate = 1;
3053        my $patchno = 0;
3054        foreach my $line (@{$difftree}) {
3055                my $diff = parsed_difftree_line($line);
3056
3057                if ($alternate) {
3058                        print "<tr class=\"dark\">\n";
3059                } else {
3060                        print "<tr class=\"light\">\n";
3061                }
3062                $alternate ^= 1;
3063
3064                if (exists $diff->{'nparents'}) { # combined diff
3065
3066                        fill_from_file_info($diff, @parents)
3067                                unless exists $diff->{'from_file'};
3068
3069                        if (!is_deleted($diff)) {
3070                                # file exists in the result (child) commit
3071                                print "<td>" .
3072                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3073                                                             file_name=>$diff->{'to_file'},
3074                                                             hash_base=>$hash),
3075                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3076                                      "</td>\n";
3077                        } else {
3078                                print "<td>" .
3079                                      esc_path($diff->{'to_file'}) .
3080                                      "</td>\n";
3081                        }
3082
3083                        if ($action eq 'commitdiff') {
3084                                # link to patch
3085                                $patchno++;
3086                                print "<td class=\"link\">" .
3087                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3088                                      " | " .
3089                                      "</td>\n";
3090                        }
3091
3092                        my $has_history = 0;
3093                        my $not_deleted = 0;
3094                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3095                                my $hash_parent = $parents[$i];
3096                                my $from_hash = $diff->{'from_id'}[$i];
3097                                my $from_path = $diff->{'from_file'}[$i];
3098                                my $status = $diff->{'status'}[$i];
3099
3100                                $has_history ||= ($status ne 'A');
3101                                $not_deleted ||= ($status ne 'D');
3102
3103                                if ($status eq 'A') {
3104                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3105                                } elsif ($status eq 'D') {
3106                                        print "<td class=\"link\">" .
3107                                              $cgi->a({-href => href(action=>"blob",
3108                                                                     hash_base=>$hash,
3109                                                                     hash=>$from_hash,
3110                                                                     file_name=>$from_path)},
3111                                                      "blob" . ($i+1)) .
3112                                              " | </td>\n";
3113                                } else {
3114                                        if ($diff->{'to_id'} eq $from_hash) {
3115                                                print "<td class=\"link nochange\">";
3116                                        } else {
3117                                                print "<td class=\"link\">";
3118                                        }
3119                                        print $cgi->a({-href => href(action=>"blobdiff",
3120                                                                     hash=>$diff->{'to_id'},
3121                                                                     hash_parent=>$from_hash,
3122                                                                     hash_base=>$hash,
3123                                                                     hash_parent_base=>$hash_parent,
3124                                                                     file_name=>$diff->{'to_file'},
3125                                                                     file_parent=>$from_path)},
3126                                                      "diff" . ($i+1)) .
3127                                              " | </td>\n";
3128                                }
3129                        }
3130
3131                        print "<td class=\"link\">";
3132                        if ($not_deleted) {
3133                                print $cgi->a({-href => href(action=>"blob",
3134                                                             hash=>$diff->{'to_id'},
3135                                                             file_name=>$diff->{'to_file'},
3136                                                             hash_base=>$hash)},
3137                                              "blob");
3138                                print " | " if ($has_history);
3139                        }
3140                        if ($has_history) {
3141                                print $cgi->a({-href => href(action=>"history",
3142                                                             file_name=>$diff->{'to_file'},
3143                                                             hash_base=>$hash)},
3144                                              "history");
3145                        }
3146                        print "</td>\n";
3147
3148                        print "</tr>\n";
3149                        next; # instead of 'else' clause, to avoid extra indent
3150                }
3151                # else ordinary diff
3152
3153                my ($to_mode_oct, $to_mode_str, $to_file_type);
3154                my ($from_mode_oct, $from_mode_str, $from_file_type);
3155                if ($diff->{'to_mode'} ne ('0' x 6)) {
3156                        $to_mode_oct = oct $diff->{'to_mode'};
3157                        if (S_ISREG($to_mode_oct)) { # only for regular file
3158                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3159                        }
3160                        $to_file_type = file_type($diff->{'to_mode'});
3161                }
3162                if ($diff->{'from_mode'} ne ('0' x 6)) {
3163                        $from_mode_oct = oct $diff->{'from_mode'};
3164                        if (S_ISREG($to_mode_oct)) { # only for regular file
3165                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3166                        }
3167                        $from_file_type = file_type($diff->{'from_mode'});
3168                }
3169
3170                if ($diff->{'status'} eq "A") { # created
3171                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3172                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3173                        $mode_chng   .= "]</span>";
3174                        print "<td>";
3175                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3176                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3177                                      -class => "list"}, esc_path($diff->{'file'}));
3178                        print "</td>\n";
3179                        print "<td>$mode_chng</td>\n";
3180                        print "<td class=\"link\">";
3181                        if ($action eq 'commitdiff') {
3182                                # link to patch
3183                                $patchno++;
3184                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3185                                print " | ";
3186                        }
3187                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3188                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3189                                      "blob");
3190                        print "</td>\n";
3191
3192                } elsif ($diff->{'status'} eq "D") { # deleted
3193                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3194                        print "<td>";
3195                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3196                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3197                                       -class => "list"}, esc_path($diff->{'file'}));
3198                        print "</td>\n";
3199                        print "<td>$mode_chng</td>\n";
3200                        print "<td class=\"link\">";
3201                        if ($action eq 'commitdiff') {
3202                                # link to patch
3203                                $patchno++;
3204                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3205                                print " | ";
3206                        }
3207                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3208                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3209                                      "blob") . " | ";
3210                        if ($have_blame) {
3211                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3212                                                             file_name=>$diff->{'file'})},
3213                                              "blame") . " | ";
3214                        }
3215                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3216                                                     file_name=>$diff->{'file'})},
3217                                      "history");
3218                        print "</td>\n";
3219
3220                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3221                        my $mode_chnge = "";
3222                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3223                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3224                                if ($from_file_type ne $to_file_type) {
3225                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3226                                }
3227                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3228                                        if ($from_mode_str && $to_mode_str) {
3229                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3230                                        } elsif ($to_mode_str) {
3231                                                $mode_chnge .= " mode: $to_mode_str";
3232                                        }
3233                                }
3234                                $mode_chnge .= "]</span>\n";
3235                        }
3236                        print "<td>";
3237                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3238                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3239                                      -class => "list"}, esc_path($diff->{'file'}));
3240                        print "</td>\n";
3241                        print "<td>$mode_chnge</td>\n";
3242                        print "<td class=\"link\">";
3243                        if ($action eq 'commitdiff') {
3244                                # link to patch
3245                                $patchno++;
3246                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3247                                      " | ";
3248                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3249                                # "commit" view and modified file (not onlu mode changed)
3250                                print $cgi->a({-href => href(action=>"blobdiff",
3251                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3252                                                             hash_base=>$hash, hash_parent_base=>$parent,
3253                                                             file_name=>$diff->{'file'})},
3254                                              "diff") .
3255                                      " | ";
3256                        }
3257                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3258                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3259                                       "blob") . " | ";
3260                        if ($have_blame) {
3261                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3262                                                             file_name=>$diff->{'file'})},
3263                                              "blame") . " | ";
3264                        }
3265                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3266                                                     file_name=>$diff->{'file'})},
3267                                      "history");
3268                        print "</td>\n";
3269
3270                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3271                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3272                        my $nstatus = $status_name{$diff->{'status'}};
3273                        my $mode_chng = "";
3274                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3275                                # mode also for directories, so we cannot use $to_mode_str
3276                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3277                        }
3278                        print "<td>" .
3279                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3280                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3281                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3282                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3283                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3284                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3285                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3286                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3287                              "<td class=\"link\">";
3288                        if ($action eq 'commitdiff') {
3289                                # link to patch
3290                                $patchno++;
3291                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3292                                      " | ";
3293                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3294                                # "commit" view and modified file (not only pure rename or copy)
3295                                print $cgi->a({-href => href(action=>"blobdiff",
3296                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3297                                                             hash_base=>$hash, hash_parent_base=>$parent,
3298                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3299                                              "diff") .
3300                                      " | ";
3301                        }
3302                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3303                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3304                                      "blob") . " | ";
3305                        if ($have_blame) {
3306                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3307                                                             file_name=>$diff->{'to_file'})},
3308                                              "blame") . " | ";
3309                        }
3310                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3311                                                    file_name=>$diff->{'to_file'})},
3312                                      "history");
3313                        print "</td>\n";
3314
3315                } # we should not encounter Unmerged (U) or Unknown (X) status
3316                print "</tr>\n";
3317        }
3318        print "</tbody>" if $has_header;
3319        print "</table>\n";
3320}
3321
3322sub git_patchset_body {
3323        my ($fd, $difftree, $hash, @hash_parents) = @_;
3324        my ($hash_parent) = $hash_parents[0];
3325
3326        my $is_combined = (@hash_parents > 1);
3327        my $patch_idx = 0;
3328        my $patch_number = 0;
3329        my $patch_line;
3330        my $diffinfo;
3331        my $to_name;
3332        my (%from, %to);
3333
3334        print "<div class=\"patchset\">\n";
3335
3336        # skip to first patch
3337        while ($patch_line = <$fd>) {
3338                chomp $patch_line;
3339
3340                last if ($patch_line =~ m/^diff /);
3341        }
3342
3343 PATCH:
3344        while ($patch_line) {
3345
3346                # parse "git diff" header line
3347                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3348                        # $1 is from_name, which we do not use
3349                        $to_name = unquote($2);
3350                        $to_name =~ s!^b/!!;
3351                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3352                        # $1 is 'cc' or 'combined', which we do not use
3353                        $to_name = unquote($2);
3354                } else {
3355                        $to_name = undef;
3356                }
3357
3358                # check if current patch belong to current raw line
3359                # and parse raw git-diff line if needed
3360                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3361                        # this is continuation of a split patch
3362                        print "<div class=\"patch cont\">\n";
3363                } else {
3364                        # advance raw git-diff output if needed
3365                        $patch_idx++ if defined $diffinfo;
3366
3367                        # read and prepare patch information
3368                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3369
3370                        # compact combined diff output can have some patches skipped
3371                        # find which patch (using pathname of result) we are at now;
3372                        if ($is_combined) {
3373                                while ($to_name ne $diffinfo->{'to_file'}) {
3374                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3375                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3376                                              "</div>\n";  # class="patch"
3377
3378                                        $patch_idx++;
3379                                        $patch_number++;
3380
3381                                        last if $patch_idx > $#$difftree;
3382                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3383                                }
3384                        }
3385
3386                        # modifies %from, %to hashes
3387                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3388
3389                        # this is first patch for raw difftree line with $patch_idx index
3390                        # we index @$difftree array from 0, but number patches from 1
3391                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3392                }
3393
3394                # git diff header
3395                #assert($patch_line =~ m/^diff /) if DEBUG;
3396                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3397                $patch_number++;
3398                # print "git diff" header
3399                print format_git_diff_header_line($patch_line, $diffinfo,
3400                                                  \%from, \%to);
3401
3402                # print extended diff header
3403                print "<div class=\"diff extended_header\">\n";
3404        EXTENDED_HEADER:
3405                while ($patch_line = <$fd>) {
3406                        chomp $patch_line;
3407
3408                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3409
3410                        print format_extended_diff_header_line($patch_line, $diffinfo,
3411                                                               \%from, \%to);
3412                }
3413                print "</div>\n"; # class="diff extended_header"
3414
3415                # from-file/to-file diff header
3416                if (! $patch_line) {
3417                        print "</div>\n"; # class="patch"
3418                        last PATCH;
3419                }
3420                next PATCH if ($patch_line =~ m/^diff /);
3421                #assert($patch_line =~ m/^---/) if DEBUG;
3422
3423                my $last_patch_line = $patch_line;
3424                $patch_line = <$fd>;
3425                chomp $patch_line;
3426                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3427
3428                print format_diff_from_to_header($last_patch_line, $patch_line,
3429                                                 $diffinfo, \%from, \%to,
3430                                                 @hash_parents);
3431
3432                # the patch itself
3433        LINE:
3434                while ($patch_line = <$fd>) {
3435                        chomp $patch_line;
3436
3437                        next PATCH if ($patch_line =~ m/^diff /);
3438
3439                        print format_diff_line($patch_line, \%from, \%to);
3440                }
3441
3442        } continue {
3443                print "</div>\n"; # class="patch"
3444        }
3445
3446        # for compact combined (--cc) format, with chunk and patch simpliciaction
3447        # patchset might be empty, but there might be unprocessed raw lines
3448        for (++$patch_idx if $patch_number > 0;
3449             $patch_idx < @$difftree;
3450             ++$patch_idx) {
3451                # read and prepare patch information
3452                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3453
3454                # generate anchor for "patch" links in difftree / whatchanged part
3455                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3456                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3457                      "</div>\n";  # class="patch"
3458
3459                $patch_number++;
3460        }
3461
3462        if ($patch_number == 0) {
3463                if (@hash_parents > 1) {
3464                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3465                } else {
3466                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3467                }
3468        }
3469
3470        print "</div>\n"; # class="patchset"
3471}
3472
3473# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3474
3475sub git_project_list_body {
3476        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3477
3478        my ($check_forks) = gitweb_check_feature('forks');
3479
3480        my @projects;
3481        foreach my $pr (@$projlist) {
3482                my (@aa) = git_get_last_activity($pr->{'path'});
3483                unless (@aa) {
3484                        next;
3485                }
3486                ($pr->{'age'}, $pr->{'age_string'}) = @aa;
3487                if (!defined $pr->{'descr'}) {
3488                        my $descr = git_get_project_description($pr->{'path'}) || "";
3489                        $pr->{'descr_long'} = to_utf8($descr);
3490                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3491                }
3492                if (!defined $pr->{'owner'}) {
3493                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3494                }
3495                if ($check_forks) {
3496                        my $pname = $pr->{'path'};
3497                        if (($pname =~ s/\.git$//) &&
3498                            ($pname !~ /\/$/) &&
3499                            (-d "$projectroot/$pname")) {
3500                                $pr->{'forks'} = "-d $projectroot/$pname";
3501                        }
3502                        else {
3503                                $pr->{'forks'} = 0;
3504                        }
3505                }
3506                push @projects, $pr;
3507        }
3508
3509        $order ||= $default_projects_order;
3510        $from = 0 unless defined $from;
3511        $to = $#projects if (!defined $to || $#projects < $to);
3512
3513        print "<table class=\"project_list\">\n";
3514        unless ($no_header) {
3515                print "<tr>\n";
3516                if ($check_forks) {
3517                        print "<th></th>\n";
3518                }
3519                if ($order eq "project") {
3520                        @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
3521                        print "<th>Project</th>\n";
3522                } else {
3523                        print "<th>" .
3524                              $cgi->a({-href => href(project=>undef, order=>'project'),
3525                                       -class => "header"}, "Project") .
3526                              "</th>\n";
3527                }
3528                if ($order eq "descr") {
3529                        @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
3530                        print "<th>Description</th>\n";
3531                } else {
3532                        print "<th>" .
3533                              $cgi->a({-href => href(project=>undef, order=>'descr'),
3534                                       -class => "header"}, "Description") .
3535                              "</th>\n";
3536                }
3537                if ($order eq "owner") {
3538                        @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
3539                        print "<th>Owner</th>\n";
3540                } else {
3541                        print "<th>" .
3542                              $cgi->a({-href => href(project=>undef, order=>'owner'),
3543                                       -class => "header"}, "Owner") .
3544                              "</th>\n";
3545                }
3546                if ($order eq "age") {
3547                        @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects;
3548                        print "<th>Last Change</th>\n";
3549                } else {
3550                        print "<th>" .
3551                              $cgi->a({-href => href(project=>undef, order=>'age'),
3552                                       -class => "header"}, "Last Change") .
3553                              "</th>\n";
3554                }
3555                print "<th></th>\n" .
3556                      "</tr>\n";
3557        }
3558        my $alternate = 1;
3559        for (my $i = $from; $i <= $to; $i++) {
3560                my $pr = $projects[$i];
3561                if ($alternate) {
3562                        print "<tr class=\"dark\">\n";
3563                } else {
3564                        print "<tr class=\"light\">\n";
3565                }
3566                $alternate ^= 1;
3567                if ($check_forks) {
3568                        print "<td>";
3569                        if ($pr->{'forks'}) {
3570                                print "<!-- $pr->{'forks'} -->\n";
3571                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3572                        }
3573                        print "</td>\n";
3574                }
3575                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3576                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3577                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3578                                        -class => "list", -title => $pr->{'descr_long'}},
3579                                        esc_html($pr->{'descr'})) . "</td>\n" .
3580                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3581                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3582                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3583                      "<td class=\"link\">" .
3584                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3585                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3586                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3587                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3588                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3589                      "</td>\n" .
3590                      "</tr>\n";
3591        }
3592        if (defined $extra) {
3593                print "<tr>\n";
3594                if ($check_forks) {
3595                        print "<td></td>\n";
3596                }
3597                print "<td colspan=\"5\">$extra</td>\n" .
3598                      "</tr>\n";
3599        }
3600        print "</table>\n";
3601}
3602
3603sub git_shortlog_body {
3604        # uses global variable $project
3605        my ($commitlist, $from, $to, $refs, $extra) = @_;
3606
3607        $from = 0 unless defined $from;
3608        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3609
3610        print "<table class=\"shortlog\">\n";
3611        my $alternate = 1;
3612        for (my $i = $from; $i <= $to; $i++) {
3613                my %co = %{$commitlist->[$i]};
3614                my $commit = $co{'id'};
3615                my $ref = format_ref_marker($refs, $commit);
3616                if ($alternate) {
3617                        print "<tr class=\"dark\">\n";
3618                } else {
3619                        print "<tr class=\"light\">\n";
3620                }
3621                $alternate ^= 1;
3622                my $author = chop_and_escape_str($co{'author_name'}, 10);
3623                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3624                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3625                      "<td><i>" . $author . "</i></td>\n" .
3626                      "<td>";
3627                print format_subject_html($co{'title'}, $co{'title_short'},
3628                                          href(action=>"commit", hash=>$commit), $ref);
3629                print "</td>\n" .
3630                      "<td class=\"link\">" .
3631                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3632                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3633                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3634                my $snapshot_links = format_snapshot_links($commit);
3635                if (defined $snapshot_links) {
3636                        print " | " . $snapshot_links;
3637                }
3638                print "</td>\n" .
3639                      "</tr>\n";
3640        }
3641        if (defined $extra) {
3642                print "<tr>\n" .
3643                      "<td colspan=\"4\">$extra</td>\n" .
3644                      "</tr>\n";
3645        }
3646        print "</table>\n";
3647}
3648
3649sub git_history_body {
3650        # Warning: assumes constant type (blob or tree) during history
3651        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3652
3653        $from = 0 unless defined $from;
3654        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3655
3656        print "<table class=\"history\">\n";
3657        my $alternate = 1;
3658        for (my $i = $from; $i <= $to; $i++) {
3659                my %co = %{$commitlist->[$i]};
3660                if (!%co) {
3661                        next;
3662                }
3663                my $commit = $co{'id'};
3664
3665                my $ref = format_ref_marker($refs, $commit);
3666
3667                if ($alternate) {
3668                        print "<tr class=\"dark\">\n";
3669                } else {
3670                        print "<tr class=\"light\">\n";
3671                }
3672                $alternate ^= 1;
3673        # shortlog uses      chop_str($co{'author_name'}, 10)
3674                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3675                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3676                      "<td><i>" . $author . "</i></td>\n" .
3677                      "<td>";
3678                # originally git_history used chop_str($co{'title'}, 50)
3679                print format_subject_html($co{'title'}, $co{'title_short'},
3680                                          href(action=>"commit", hash=>$commit), $ref);
3681                print "</td>\n" .
3682                      "<td class=\"link\">" .
3683                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3684                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3685
3686                if ($ftype eq 'blob') {
3687                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3688                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3689                        if (defined $blob_current && defined $blob_parent &&
3690                                        $blob_current ne $blob_parent) {
3691                                print " | " .
3692                                        $cgi->a({-href => href(action=>"blobdiff",
3693                                                               hash=>$blob_current, hash_parent=>$blob_parent,
3694                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
3695                                                               file_name=>$file_name)},
3696                                                "diff to current");
3697                        }
3698                }
3699                print "</td>\n" .
3700                      "</tr>\n";
3701        }
3702        if (defined $extra) {
3703                print "<tr>\n" .
3704                      "<td colspan=\"4\">$extra</td>\n" .
3705                      "</tr>\n";
3706        }
3707        print "</table>\n";
3708}
3709
3710sub git_tags_body {
3711        # uses global variable $project
3712        my ($taglist, $from, $to, $extra) = @_;
3713        $from = 0 unless defined $from;
3714        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3715
3716        print "<table class=\"tags\">\n";
3717        my $alternate = 1;
3718        for (my $i = $from; $i <= $to; $i++) {
3719                my $entry = $taglist->[$i];
3720                my %tag = %$entry;
3721                my $comment = $tag{'subject'};
3722                my $comment_short;
3723                if (defined $comment) {
3724                        $comment_short = chop_str($comment, 30, 5);
3725                }
3726                if ($alternate) {
3727                        print "<tr class=\"dark\">\n";
3728                } else {
3729                        print "<tr class=\"light\">\n";
3730                }
3731                $alternate ^= 1;
3732                if (defined $tag{'age'}) {
3733                        print "<td><i>$tag{'age'}</i></td>\n";
3734                } else {
3735                        print "<td></td>\n";
3736                }
3737                print "<td>" .
3738                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3739                               -class => "list name"}, esc_html($tag{'name'})) .
3740                      "</td>\n" .
3741                      "<td>";
3742                if (defined $comment) {
3743                        print format_subject_html($comment, $comment_short,
3744                                                  href(action=>"tag", hash=>$tag{'id'}));
3745                }
3746                print "</td>\n" .
3747                      "<td class=\"selflink\">";
3748                if ($tag{'type'} eq "tag") {
3749                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3750                } else {
3751                        print "&nbsp;";
3752                }
3753                print "</td>\n" .
3754                      "<td class=\"link\">" . " | " .
3755                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3756                if ($tag{'reftype'} eq "commit") {
3757                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3758                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3759                } elsif ($tag{'reftype'} eq "blob") {
3760                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3761                }
3762                print "</td>\n" .
3763                      "</tr>";
3764        }
3765        if (defined $extra) {
3766                print "<tr>\n" .
3767                      "<td colspan=\"5\">$extra</td>\n" .
3768                      "</tr>\n";
3769        }
3770        print "</table>\n";
3771}
3772
3773sub git_heads_body {
3774        # uses global variable $project
3775        my ($headlist, $head, $from, $to, $extra) = @_;
3776        $from = 0 unless defined $from;
3777        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3778
3779        print "<table class=\"heads\">\n";
3780        my $alternate = 1;
3781        for (my $i = $from; $i <= $to; $i++) {
3782                my $entry = $headlist->[$i];
3783                my %ref = %$entry;
3784                my $curr = $ref{'id'} eq $head;
3785                if ($alternate) {
3786                        print "<tr class=\"dark\">\n";
3787                } else {
3788                        print "<tr class=\"light\">\n";
3789                }
3790                $alternate ^= 1;
3791                print "<td><i>$ref{'age'}</i></td>\n" .
3792                      ($curr ? "<td class=\"current_head\">" : "<td>") .
3793                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
3794                               -class => "list name"},esc_html($ref{'name'})) .
3795                      "</td>\n" .
3796                      "<td class=\"link\">" .
3797                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
3798                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
3799                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
3800                      "</td>\n" .
3801                      "</tr>";
3802        }
3803        if (defined $extra) {
3804                print "<tr>\n" .
3805                      "<td colspan=\"3\">$extra</td>\n" .
3806                      "</tr>\n";
3807        }
3808        print "</table>\n";
3809}
3810
3811sub git_search_grep_body {
3812        my ($commitlist, $from, $to, $extra) = @_;
3813        $from = 0 unless defined $from;
3814        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3815
3816        print "<table class=\"commit_search\">\n";
3817        my $alternate = 1;
3818        for (my $i = $from; $i <= $to; $i++) {
3819                my %co = %{$commitlist->[$i]};
3820                if (!%co) {
3821                        next;
3822                }
3823                my $commit = $co{'id'};
3824                if ($alternate) {
3825                        print "<tr class=\"dark\">\n";
3826                } else {
3827                        print "<tr class=\"light\">\n";
3828                }
3829                $alternate ^= 1;
3830                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3831                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3832                      "<td><i>" . $author . "</i></td>\n" .
3833                      "<td>" .
3834                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3835                               -class => "list subject"},
3836                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
3837                my $comment = $co{'comment'};
3838                foreach my $line (@$comment) {
3839                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3840                                my ($lead, $match, $trail) = ($1, $2, $3);
3841                                $match = chop_str($match, 70, 5, 'center');
3842                                my $contextlen = int((80 - length($match))/2);
3843                                $contextlen = 30 if ($contextlen > 30);
3844                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
3845                                $trail = chop_str($trail, $contextlen, 10, 'right');
3846
3847                                $lead  = esc_html($lead);
3848                                $match = esc_html($match);
3849                                $trail = esc_html($trail);
3850
3851                                print "$lead<span class=\"match\">$match</span>$trail<br />";
3852                        }
3853                }
3854                print "</td>\n" .
3855                      "<td class=\"link\">" .
3856                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3857                      " | " .
3858                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
3859                      " | " .
3860                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3861                print "</td>\n" .
3862                      "</tr>\n";
3863        }
3864        if (defined $extra) {
3865                print "<tr>\n" .
3866                      "<td colspan=\"3\">$extra</td>\n" .
3867                      "</tr>\n";
3868        }
3869        print "</table>\n";
3870}
3871
3872## ======================================================================
3873## ======================================================================
3874## actions
3875
3876sub git_project_list {
3877        my $order = $cgi->param('o');
3878        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3879                die_error(undef, "Unknown order parameter");
3880        }
3881
3882        my @list = git_get_projects_list();
3883        if (!@list) {
3884                die_error(undef, "No projects found");
3885        }
3886
3887        git_header_html();
3888        if (-f $home_text) {
3889                print "<div class=\"index_include\">\n";
3890                open (my $fd, $home_text);
3891                print <$fd>;
3892                close $fd;
3893                print "</div>\n";
3894        }
3895        git_project_list_body(\@list, $order);
3896        git_footer_html();
3897}
3898
3899sub git_forks {
3900        my $order = $cgi->param('o');
3901        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3902                die_error(undef, "Unknown order parameter");
3903        }
3904
3905        my @list = git_get_projects_list($project);
3906        if (!@list) {
3907                die_error(undef, "No forks found");
3908        }
3909
3910        git_header_html();
3911        git_print_page_nav('','');
3912        git_print_header_div('summary', "$project forks");
3913        git_project_list_body(\@list, $order);
3914        git_footer_html();
3915}
3916
3917sub git_project_index {
3918        my @projects = git_get_projects_list($project);
3919
3920        print $cgi->header(
3921                -type => 'text/plain',
3922                -charset => 'utf-8',
3923                -content_disposition => 'inline; filename="index.aux"');
3924
3925        foreach my $pr (@projects) {
3926                if (!exists $pr->{'owner'}) {
3927                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
3928                }
3929
3930                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
3931                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
3932                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
3933                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
3934                $path  =~ s/ /\+/g;
3935                $owner =~ s/ /\+/g;
3936
3937                print "$path $owner\n";
3938        }
3939}
3940
3941sub git_summary {
3942        my $descr = git_get_project_description($project) || "none";
3943        my %co = parse_commit("HEAD");
3944        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
3945        my $head = $co{'id'};
3946
3947        my $owner = git_get_project_owner($project);
3948
3949        my $refs = git_get_references();
3950        # These get_*_list functions return one more to allow us to see if
3951        # there are more ...
3952        my @taglist  = git_get_tags_list(16);
3953        my @headlist = git_get_heads_list(16);
3954        my @forklist;
3955        my ($check_forks) = gitweb_check_feature('forks');
3956
3957        if ($check_forks) {
3958                @forklist = git_get_projects_list($project);
3959        }
3960
3961        git_header_html();
3962        git_print_page_nav('summary','', $head);
3963
3964        print "<div class=\"title\">&nbsp;</div>\n";
3965        print "<table class=\"projects_list\">\n" .
3966              "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
3967              "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
3968        if (defined $cd{'rfc2822'}) {
3969                print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
3970        }
3971
3972        # use per project git URL list in $projectroot/$project/cloneurl
3973        # or make project git URL from git base URL and project name
3974        my $url_tag = "URL";
3975        my @url_list = git_get_project_url_list($project);
3976        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
3977        foreach my $git_url (@url_list) {
3978                next unless $git_url;
3979                print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
3980                $url_tag = "";
3981        }
3982        print "</table>\n";
3983
3984        if (-s "$projectroot/$project/README.html") {
3985                if (open my $fd, "$projectroot/$project/README.html") {
3986                        print "<div class=\"title\">readme</div>\n" .
3987                              "<div class=\"readme\">\n";
3988                        print $_ while (<$fd>);
3989                        print "\n</div>\n"; # class="readme"
3990                        close $fd;
3991                }
3992        }
3993
3994        # we need to request one more than 16 (0..15) to check if
3995        # those 16 are all
3996        my @commitlist = $head ? parse_commits($head, 17) : ();
3997        if (@commitlist) {
3998                git_print_header_div('shortlog');
3999                git_shortlog_body(\@commitlist, 0, 15, $refs,
4000                                  $#commitlist <=  15 ? undef :
4001                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4002        }
4003
4004        if (@taglist) {
4005                git_print_header_div('tags');
4006                git_tags_body(\@taglist, 0, 15,
4007                              $#taglist <=  15 ? undef :
4008                              $cgi->a({-href => href(action=>"tags")}, "..."));
4009        }
4010
4011        if (@headlist) {
4012                git_print_header_div('heads');
4013                git_heads_body(\@headlist, $head, 0, 15,
4014                               $#headlist <= 15 ? undef :
4015                               $cgi->a({-href => href(action=>"heads")}, "..."));
4016        }
4017
4018        if (@forklist) {
4019                git_print_header_div('forks');
4020                git_project_list_body(\@forklist, undef, 0, 15,
4021                                      $#forklist <= 15 ? undef :
4022                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4023                                      'noheader');
4024        }
4025
4026        git_footer_html();
4027}
4028
4029sub git_tag {
4030        my $head = git_get_head_hash($project);
4031        git_header_html();
4032        git_print_page_nav('','', $head,undef,$head);
4033        my %tag = parse_tag($hash);
4034
4035        if (! %tag) {
4036                die_error(undef, "Unknown tag object");
4037        }
4038
4039        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4040        print "<div class=\"title_text\">\n" .
4041              "<table class=\"object_header\">\n" .
4042              "<tr>\n" .
4043              "<td>object</td>\n" .
4044              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4045                               $tag{'object'}) . "</td>\n" .
4046              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4047                                              $tag{'type'}) . "</td>\n" .
4048              "</tr>\n";
4049        if (defined($tag{'author'})) {
4050                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4051                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4052                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4053                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4054                        "</td></tr>\n";
4055        }
4056        print "</table>\n\n" .
4057              "</div>\n";
4058        print "<div class=\"page_body\">";
4059        my $comment = $tag{'comment'};
4060        foreach my $line (@$comment) {
4061                chomp $line;
4062                print esc_html($line, -nbsp=>1) . "<br/>\n";
4063        }
4064        print "</div>\n";
4065        git_footer_html();
4066}
4067
4068sub git_blame2 {
4069        my $fd;
4070        my $ftype;
4071
4072        my ($have_blame) = gitweb_check_feature('blame');
4073        if (!$have_blame) {
4074                die_error('403 Permission denied', "Permission denied");
4075        }
4076        die_error('404 Not Found', "File name not defined") if (!$file_name);
4077        $hash_base ||= git_get_head_hash($project);
4078        die_error(undef, "Couldn't find base commit") unless ($hash_base);
4079        my %co = parse_commit($hash_base)
4080                or die_error(undef, "Reading commit failed");
4081        if (!defined $hash) {
4082                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4083                        or die_error(undef, "Error looking up file");
4084        }
4085        $ftype = git_get_type($hash);
4086        if ($ftype !~ "blob") {
4087                die_error('400 Bad Request', "Object is not a blob");
4088        }
4089        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4090              $file_name, $hash_base)
4091                or die_error(undef, "Open git-blame failed");
4092        git_header_html();
4093        my $formats_nav =
4094                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4095                        "blob") .
4096                " | " .
4097                $cgi->a({-href => href(action=>"history", -replay=>1)},
4098                        "history") .
4099                " | " .
4100                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4101                        "HEAD");
4102        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4103        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4104        git_print_page_path($file_name, $ftype, $hash_base);
4105        my @rev_color = (qw(light2 dark2));
4106        my $num_colors = scalar(@rev_color);
4107        my $current_color = 0;
4108        my $last_rev;
4109        print <<HTML;
4110<div class="page_body">
4111<table class="blame">
4112<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4113HTML
4114        my %metainfo = ();
4115        while (1) {
4116                $_ = <$fd>;
4117                last unless defined $_;
4118                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4119                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4120                if (!exists $metainfo{$full_rev}) {
4121                        $metainfo{$full_rev} = {};
4122                }
4123                my $meta = $metainfo{$full_rev};
4124                while (<$fd>) {
4125                        last if (s/^\t//);
4126                        if (/^(\S+) (.*)$/) {
4127                                $meta->{$1} = $2;
4128                        }
4129                }
4130                my $data = $_;
4131                chomp $data;
4132                my $rev = substr($full_rev, 0, 8);
4133                my $author = $meta->{'author'};
4134                my %date = parse_date($meta->{'author-time'},
4135                                      $meta->{'author-tz'});
4136                my $date = $date{'iso-tz'};
4137                if ($group_size) {
4138                        $current_color = ++$current_color % $num_colors;
4139                }
4140                print "<tr class=\"$rev_color[$current_color]\">\n";
4141                if ($group_size) {
4142                        print "<td class=\"sha1\"";
4143                        print " title=\"". esc_html($author) . ", $date\"";
4144                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4145                        print ">";
4146                        print $cgi->a({-href => href(action=>"commit",
4147                                                     hash=>$full_rev,
4148                                                     file_name=>$file_name)},
4149                                      esc_html($rev));
4150                        print "</td>\n";
4151                }
4152                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4153                        or die_error(undef, "Open git-rev-parse failed");
4154                my $parent_commit = <$dd>;
4155                close $dd;
4156                chomp($parent_commit);
4157                my $blamed = href(action => 'blame',
4158                                  file_name => $meta->{'filename'},
4159                                  hash_base => $parent_commit);
4160                print "<td class=\"linenr\">";
4161                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4162                                -id => "l$lineno",
4163                                -class => "linenr" },
4164                              esc_html($lineno));
4165                print "</td>";
4166                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4167                print "</tr>\n";
4168        }
4169        print "</table>\n";
4170        print "</div>";
4171        close $fd
4172                or print "Reading blob failed\n";
4173        git_footer_html();
4174}
4175
4176sub git_blame {
4177        my $fd;
4178
4179        my ($have_blame) = gitweb_check_feature('blame');
4180        if (!$have_blame) {
4181                die_error('403 Permission denied', "Permission denied");
4182        }
4183        die_error('404 Not Found', "File name not defined") if (!$file_name);
4184        $hash_base ||= git_get_head_hash($project);
4185        die_error(undef, "Couldn't find base commit") unless ($hash_base);
4186        my %co = parse_commit($hash_base)
4187                or die_error(undef, "Reading commit failed");
4188        if (!defined $hash) {
4189                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4190                        or die_error(undef, "Error lookup file");
4191        }
4192        open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
4193                or die_error(undef, "Open git-annotate failed");
4194        git_header_html();
4195        my $formats_nav =
4196                $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
4197                        "blob") .
4198                " | " .
4199                $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
4200                        "history") .
4201                " | " .
4202                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4203                        "HEAD");
4204        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4205        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4206        git_print_page_path($file_name, 'blob', $hash_base);
4207        print "<div class=\"page_body\">\n";
4208        print <<HTML;
4209<table class="blame">
4210  <tr>
4211    <th>Commit</th>
4212    <th>Age</th>
4213    <th>Author</th>
4214    <th>Line</th>
4215    <th>Data</th>
4216  </tr>
4217HTML
4218        my @line_class = (qw(light dark));
4219        my $line_class_len = scalar (@line_class);
4220        my $line_class_num = $#line_class;
4221        while (my $line = <$fd>) {
4222                my $long_rev;
4223                my $short_rev;
4224                my $author;
4225                my $time;
4226                my $lineno;
4227                my $data;
4228                my $age;
4229                my $age_str;
4230                my $age_class;
4231
4232                chomp $line;
4233                $line_class_num = ($line_class_num + 1) % $line_class_len;
4234
4235                if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
4236                        $long_rev = $1;
4237                        $author   = $2;
4238                        $time     = $3;
4239                        $lineno   = $4;
4240                        $data     = $5;
4241                } else {
4242                        print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
4243                        next;
4244                }
4245                $short_rev  = substr ($long_rev, 0, 8);
4246                $age        = time () - $time;
4247                $age_str    = age_string ($age);
4248                $age_str    =~ s/ /&nbsp;/g;
4249                $age_class  = age_class($age);
4250                $author     = esc_html ($author);
4251                $author     =~ s/ /&nbsp;/g;
4252
4253                $data = untabify($data);
4254                $data = esc_html ($data);
4255
4256                print <<HTML;
4257  <tr class="$line_class[$line_class_num]">
4258    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
4259    <td class="$age_class">$age_str</td>
4260    <td>$author</td>
4261    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
4262    <td class="pre">$data</td>
4263  </tr>
4264HTML
4265        } # while (my $line = <$fd>)
4266        print "</table>\n\n";
4267        close $fd
4268                or print "Reading blob failed.\n";
4269        print "</div>";
4270        git_footer_html();
4271}
4272
4273sub git_tags {
4274        my $head = git_get_head_hash($project);
4275        git_header_html();
4276        git_print_page_nav('','', $head,undef,$head);
4277        git_print_header_div('summary', $project);
4278
4279        my @tagslist = git_get_tags_list();
4280        if (@tagslist) {
4281                git_tags_body(\@tagslist);
4282        }
4283        git_footer_html();
4284}
4285
4286sub git_heads {
4287        my $head = git_get_head_hash($project);
4288        git_header_html();
4289        git_print_page_nav('','', $head,undef,$head);
4290        git_print_header_div('summary', $project);
4291
4292        my @headslist = git_get_heads_list();
4293        if (@headslist) {
4294                git_heads_body(\@headslist, $head);
4295        }
4296        git_footer_html();
4297}
4298
4299sub git_blob_plain {
4300        my $expires;
4301
4302        if (!defined $hash) {
4303                if (defined $file_name) {
4304                        my $base = $hash_base || git_get_head_hash($project);
4305                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4306                                or die_error(undef, "Error lookup file");
4307                } else {
4308                        die_error(undef, "No file name defined");
4309                }
4310        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4311                # blobs defined by non-textual hash id's can be cached
4312                $expires = "+1d";
4313        }
4314
4315        my $type = shift;
4316        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4317                or die_error(undef, "Couldn't cat $file_name, $hash");
4318
4319        $type ||= blob_mimetype($fd, $file_name);
4320
4321        # save as filename, even when no $file_name is given
4322        my $save_as = "$hash";
4323        if (defined $file_name) {
4324                $save_as = $file_name;
4325        } elsif ($type =~ m/^text\//) {
4326                $save_as .= '.txt';
4327        }
4328
4329        print $cgi->header(
4330                -type => "$type",
4331                -expires=>$expires,
4332                -content_disposition => 'inline; filename="' . "$save_as" . '"');
4333        undef $/;
4334        binmode STDOUT, ':raw';
4335        print <$fd>;
4336        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4337        $/ = "\n";
4338        close $fd;
4339}
4340
4341sub git_blob {
4342        my $expires;
4343
4344        if (!defined $hash) {
4345                if (defined $file_name) {
4346                        my $base = $hash_base || git_get_head_hash($project);
4347                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4348                                or die_error(undef, "Error lookup file");
4349                } else {
4350                        die_error(undef, "No file name defined");
4351                }
4352        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4353                # blobs defined by non-textual hash id's can be cached
4354                $expires = "+1d";
4355        }
4356
4357        my ($have_blame) = gitweb_check_feature('blame');
4358        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4359                or die_error(undef, "Couldn't cat $file_name, $hash");
4360        my $mimetype = blob_mimetype($fd, $file_name);
4361        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4362                close $fd;
4363                return git_blob_plain($mimetype);
4364        }
4365        # we can have blame only for text/* mimetype
4366        $have_blame &&= ($mimetype =~ m!^text/!);
4367
4368        git_header_html(undef, $expires);
4369        my $formats_nav = '';
4370        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4371                if (defined $file_name) {
4372                        if ($have_blame) {
4373                                $formats_nav .=
4374                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4375                                                "blame") .
4376                                        " | ";
4377                        }
4378                        $formats_nav .=
4379                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4380                                        "history") .
4381                                " | " .
4382                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4383                                        "raw") .
4384                                " | " .
4385                                $cgi->a({-href => href(action=>"blob",
4386                                                       hash_base=>"HEAD", file_name=>$file_name)},
4387                                        "HEAD");
4388                } else {
4389                        $formats_nav .=
4390                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4391                                        "raw");
4392                }
4393                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4394                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4395        } else {
4396                print "<div class=\"page_nav\">\n" .
4397                      "<br/><br/></div>\n" .
4398                      "<div class=\"title\">$hash</div>\n";
4399        }
4400        git_print_page_path($file_name, "blob", $hash_base);
4401        print "<div class=\"page_body\">\n";
4402        if ($mimetype =~ m!^image/!) {
4403                print qq!<img type="$mimetype"!;
4404                if ($file_name) {
4405                        print qq! alt="$file_name" title="$file_name"!;
4406                }
4407                print qq! src="! .
4408                      href(action=>"blob_plain", hash=>$hash,
4409                           hash_base=>$hash_base, file_name=>$file_name) .
4410                      qq!" />\n!;
4411        } else {
4412                my $nr;
4413                while (my $line = <$fd>) {
4414                        chomp $line;
4415                        $nr++;
4416                        $line = untabify($line);
4417                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4418                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4419                }
4420        }
4421        close $fd
4422                or print "Reading blob failed.\n";
4423        print "</div>";
4424        git_footer_html();
4425}
4426
4427sub git_tree {
4428        if (!defined $hash_base) {
4429                $hash_base = "HEAD";
4430        }
4431        if (!defined $hash) {
4432                if (defined $file_name) {
4433                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4434                } else {
4435                        $hash = $hash_base;
4436                }
4437        }
4438        $/ = "\0";
4439        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4440                or die_error(undef, "Open git-ls-tree failed");
4441        my @entries = map { chomp; $_ } <$fd>;
4442        close $fd or die_error(undef, "Reading tree failed");
4443        $/ = "\n";
4444
4445        my $refs = git_get_references();
4446        my $ref = format_ref_marker($refs, $hash_base);
4447        git_header_html();
4448        my $basedir = '';
4449        my ($have_blame) = gitweb_check_feature('blame');
4450        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4451                my @views_nav = ();
4452                if (defined $file_name) {
4453                        push @views_nav,
4454                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4455                                        "history"),
4456                                $cgi->a({-href => href(action=>"tree",
4457                                                       hash_base=>"HEAD", file_name=>$file_name)},
4458                                        "HEAD"),
4459                }
4460                my $snapshot_links = format_snapshot_links($hash);
4461                if (defined $snapshot_links) {
4462                        # FIXME: Should be available when we have no hash base as well.
4463                        push @views_nav, $snapshot_links;
4464                }
4465                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4466                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4467        } else {
4468                undef $hash_base;
4469                print "<div class=\"page_nav\">\n";
4470                print "<br/><br/></div>\n";
4471                print "<div class=\"title\">$hash</div>\n";
4472        }
4473        if (defined $file_name) {
4474                $basedir = $file_name;
4475                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4476                        $basedir .= '/';
4477                }
4478        }
4479        git_print_page_path($file_name, 'tree', $hash_base);
4480        print "<div class=\"page_body\">\n";
4481        print "<table class=\"tree\">\n";
4482        my $alternate = 1;
4483        # '..' (top directory) link if possible
4484        if (defined $hash_base &&
4485            defined $file_name && $file_name =~ m![^/]+$!) {
4486                if ($alternate) {
4487                        print "<tr class=\"dark\">\n";
4488                } else {
4489                        print "<tr class=\"light\">\n";
4490                }
4491                $alternate ^= 1;
4492
4493                my $up = $file_name;
4494                $up =~ s!/?[^/]+$!!;
4495                undef $up unless $up;
4496                # based on git_print_tree_entry
4497                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4498                print '<td class="list">';
4499                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4500                                             file_name=>$up)},
4501                              "..");
4502                print "</td>\n";
4503                print "<td class=\"link\"></td>\n";
4504
4505                print "</tr>\n";
4506        }
4507        foreach my $line (@entries) {
4508                my %t = parse_ls_tree_line($line, -z => 1);
4509
4510                if ($alternate) {
4511                        print "<tr class=\"dark\">\n";
4512                } else {
4513                        print "<tr class=\"light\">\n";
4514                }
4515                $alternate ^= 1;
4516
4517                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4518
4519                print "</tr>\n";
4520        }
4521        print "</table>\n" .
4522              "</div>";
4523        git_footer_html();
4524}
4525
4526sub git_snapshot {
4527        my @supported_fmts = gitweb_check_feature('snapshot');
4528        @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4529
4530        my $format = $cgi->param('sf');
4531        if (!@supported_fmts) {
4532                die_error('403 Permission denied', "Permission denied");
4533        }
4534        # default to first supported snapshot format
4535        $format ||= $supported_fmts[0];
4536        if ($format !~ m/^[a-z0-9]+$/) {
4537                die_error(undef, "Invalid snapshot format parameter");
4538        } elsif (!exists($known_snapshot_formats{$format})) {
4539                die_error(undef, "Unknown snapshot format");
4540        } elsif (!grep($_ eq $format, @supported_fmts)) {
4541                die_error(undef, "Unsupported snapshot format");
4542        }
4543
4544        if (!defined $hash) {
4545                $hash = git_get_head_hash($project);
4546        }
4547
4548        my $git_command = git_cmd_str();
4549        my $name = $project;
4550        $name =~ s,([^/])/*\.git$,$1,;
4551        $name = basename($name);
4552        my $filename = to_utf8($name);
4553        $name =~ s/\047/\047\\\047\047/g;
4554        my $cmd;
4555        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4556        $cmd = "$git_command archive " .
4557                "--format=$known_snapshot_formats{$format}{'format'} " .
4558                "--prefix=\'$name\'/ $hash";
4559        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4560                $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}};
4561        }
4562
4563        print $cgi->header(
4564                -type => $known_snapshot_formats{$format}{'type'},
4565                -content_disposition => 'inline; filename="' . "$filename" . '"',
4566                -status => '200 OK');
4567
4568        open my $fd, "-|", $cmd
4569                or die_error(undef, "Execute git-archive failed");
4570        binmode STDOUT, ':raw';
4571        print <$fd>;
4572        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4573        close $fd;
4574}
4575
4576sub git_log {
4577        my $head = git_get_head_hash($project);
4578        if (!defined $hash) {
4579                $hash = $head;
4580        }
4581        if (!defined $page) {
4582                $page = 0;
4583        }
4584        my $refs = git_get_references();
4585
4586        my @commitlist = parse_commits($hash, 101, (100 * $page));
4587
4588        my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1)));
4589
4590        git_header_html();
4591        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4592
4593        if (!@commitlist) {
4594                my %co = parse_commit($hash);
4595
4596                git_print_header_div('summary', $project);
4597                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4598        }
4599        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4600        for (my $i = 0; $i <= $to; $i++) {
4601                my %co = %{$commitlist[$i]};
4602                next if !%co;
4603                my $commit = $co{'id'};
4604                my $ref = format_ref_marker($refs, $commit);
4605                my %ad = parse_date($co{'author_epoch'});
4606                git_print_header_div('commit',
4607                               "<span class=\"age\">$co{'age_string'}</span>" .
4608                               esc_html($co{'title'}) . $ref,
4609                               $commit);
4610                print "<div class=\"title_text\">\n" .
4611                      "<div class=\"log_link\">\n" .
4612                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4613                      " | " .
4614                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4615                      " | " .
4616                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4617                      "<br/>\n" .
4618                      "</div>\n" .
4619                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4620                      "</div>\n";
4621
4622                print "<div class=\"log_body\">\n";
4623                git_print_log($co{'comment'}, -final_empty_line=> 1);
4624                print "</div>\n";
4625        }
4626        if ($#commitlist >= 100) {
4627                print "<div class=\"page_nav\">\n";
4628                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4629                               -accesskey => "n", -title => "Alt-n"}, "next");
4630                print "</div>\n";
4631        }
4632        git_footer_html();
4633}
4634
4635sub git_commit {
4636        $hash ||= $hash_base || "HEAD";
4637        my %co = parse_commit($hash);
4638        if (!%co) {
4639                die_error(undef, "Unknown commit object");
4640        }
4641        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4642        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4643
4644        my $parent  = $co{'parent'};
4645        my $parents = $co{'parents'}; # listref
4646
4647        # we need to prepare $formats_nav before any parameter munging
4648        my $formats_nav;
4649        if (!defined $parent) {
4650                # --root commitdiff
4651                $formats_nav .= '(initial)';
4652        } elsif (@$parents == 1) {
4653                # single parent commit
4654                $formats_nav .=
4655                        '(parent: ' .
4656                        $cgi->a({-href => href(action=>"commit",
4657                                               hash=>$parent)},
4658                                esc_html(substr($parent, 0, 7))) .
4659                        ')';
4660        } else {
4661                # merge commit
4662                $formats_nav .=
4663                        '(merge: ' .
4664                        join(' ', map {
4665                                $cgi->a({-href => href(action=>"commit",
4666                                                       hash=>$_)},
4667                                        esc_html(substr($_, 0, 7)));
4668                        } @$parents ) .
4669                        ')';
4670        }
4671
4672        if (!defined $parent) {
4673                $parent = "--root";
4674        }
4675        my @difftree;
4676        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4677                @diff_opts,
4678                (@$parents <= 1 ? $parent : '-c'),
4679                $hash, "--"
4680                or die_error(undef, "Open git-diff-tree failed");
4681        @difftree = map { chomp; $_ } <$fd>;
4682        close $fd or die_error(undef, "Reading git-diff-tree failed");
4683
4684        # non-textual hash id's can be cached
4685        my $expires;
4686        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4687                $expires = "+1d";
4688        }
4689        my $refs = git_get_references();
4690        my $ref = format_ref_marker($refs, $co{'id'});
4691
4692        git_header_html(undef, $expires);
4693        git_print_page_nav('commit', '',
4694                           $hash, $co{'tree'}, $hash,
4695                           $formats_nav);
4696
4697        if (defined $co{'parent'}) {
4698                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4699        } else {
4700                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4701        }
4702        print "<div class=\"title_text\">\n" .
4703              "<table class=\"object_header\">\n";
4704        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4705              "<tr>" .
4706              "<td></td><td> $ad{'rfc2822'}";
4707        if ($ad{'hour_local'} < 6) {
4708                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4709                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4710        } else {
4711                printf(" (%02d:%02d %s)",
4712                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4713        }
4714        print "</td>" .
4715              "</tr>\n";
4716        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4717        print "<tr><td></td><td> $cd{'rfc2822'}" .
4718              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4719              "</td></tr>\n";
4720        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4721        print "<tr>" .
4722              "<td>tree</td>" .
4723              "<td class=\"sha1\">" .
4724              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4725                       class => "list"}, $co{'tree'}) .
4726              "</td>" .
4727              "<td class=\"link\">" .
4728              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4729                      "tree");
4730        my $snapshot_links = format_snapshot_links($hash);
4731        if (defined $snapshot_links) {
4732                print " | " . $snapshot_links;
4733        }
4734        print "</td>" .
4735              "</tr>\n";
4736
4737        foreach my $par (@$parents) {
4738                print "<tr>" .
4739                      "<td>parent</td>" .
4740                      "<td class=\"sha1\">" .
4741                      $cgi->a({-href => href(action=>"commit", hash=>$par),
4742                               class => "list"}, $par) .
4743                      "</td>" .
4744                      "<td class=\"link\">" .
4745                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4746                      " | " .
4747                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4748                      "</td>" .
4749                      "</tr>\n";
4750        }
4751        print "</table>".
4752              "</div>\n";
4753
4754        print "<div class=\"page_body\">\n";
4755        git_print_log($co{'comment'});
4756        print "</div>\n";
4757
4758        git_difftree_body(\@difftree, $hash, @$parents);
4759
4760        git_footer_html();
4761}
4762
4763sub git_object {
4764        # object is defined by:
4765        # - hash or hash_base alone
4766        # - hash_base and file_name
4767        my $type;
4768
4769        # - hash or hash_base alone
4770        if ($hash || ($hash_base && !defined $file_name)) {
4771                my $object_id = $hash || $hash_base;
4772
4773                my $git_command = git_cmd_str();
4774                open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null"
4775                        or die_error('404 Not Found', "Object does not exist");
4776                $type = <$fd>;
4777                chomp $type;
4778                close $fd
4779                        or die_error('404 Not Found', "Object does not exist");
4780
4781        # - hash_base and file_name
4782        } elsif ($hash_base && defined $file_name) {
4783                $file_name =~ s,/+$,,;
4784
4785                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4786                        or die_error('404 Not Found', "Base object does not exist");
4787
4788                # here errors should not hapen
4789                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4790                        or die_error(undef, "Open git-ls-tree failed");
4791                my $line = <$fd>;
4792                close $fd;
4793
4794                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4795                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4796                        die_error('404 Not Found', "File or directory for given base does not exist");
4797                }
4798                $type = $2;
4799                $hash = $3;
4800        } else {
4801                die_error('404 Not Found', "Not enough information to find object");
4802        }
4803
4804        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4805                                          hash=>$hash, hash_base=>$hash_base,
4806                                          file_name=>$file_name),
4807                             -status => '302 Found');
4808}
4809
4810sub git_blobdiff {
4811        my $format = shift || 'html';
4812
4813        my $fd;
4814        my @difftree;
4815        my %diffinfo;
4816        my $expires;
4817
4818        # preparing $fd and %diffinfo for git_patchset_body
4819        # new style URI
4820        if (defined $hash_base && defined $hash_parent_base) {
4821                if (defined $file_name) {
4822                        # read raw output
4823                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4824                                $hash_parent_base, $hash_base,
4825                                "--", (defined $file_parent ? $file_parent : ()), $file_name
4826                                or die_error(undef, "Open git-diff-tree failed");
4827                        @difftree = map { chomp; $_ } <$fd>;
4828                        close $fd
4829                                or die_error(undef, "Reading git-diff-tree failed");
4830                        @difftree
4831                                or die_error('404 Not Found', "Blob diff not found");
4832
4833                } elsif (defined $hash &&
4834                         $hash =~ /[0-9a-fA-F]{40}/) {
4835                        # try to find filename from $hash
4836
4837                        # read filtered raw output
4838                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4839                                $hash_parent_base, $hash_base, "--"
4840                                or die_error(undef, "Open git-diff-tree failed");
4841                        @difftree =
4842                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4843                                # $hash == to_id
4844                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4845                                map { chomp; $_ } <$fd>;
4846                        close $fd
4847                                or die_error(undef, "Reading git-diff-tree failed");
4848                        @difftree
4849                                or die_error('404 Not Found', "Blob diff not found");
4850
4851                } else {
4852                        die_error('404 Not Found', "Missing one of the blob diff parameters");
4853                }
4854
4855                if (@difftree > 1) {
4856                        die_error('404 Not Found', "Ambiguous blob diff specification");
4857                }
4858
4859                %diffinfo = parse_difftree_raw_line($difftree[0]);
4860                $file_parent ||= $diffinfo{'from_file'} || $file_name;
4861                $file_name   ||= $diffinfo{'to_file'};
4862
4863                $hash_parent ||= $diffinfo{'from_id'};
4864                $hash        ||= $diffinfo{'to_id'};
4865
4866                # non-textual hash id's can be cached
4867                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4868                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4869                        $expires = '+1d';
4870                }
4871
4872                # open patch output
4873                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4874                        '-p', ($format eq 'html' ? "--full-index" : ()),
4875                        $hash_parent_base, $hash_base,
4876                        "--", (defined $file_parent ? $file_parent : ()), $file_name
4877                        or die_error(undef, "Open git-diff-tree failed");
4878        }
4879
4880        # old/legacy style URI
4881        if (!%diffinfo && # if new style URI failed
4882            defined $hash && defined $hash_parent) {
4883                # fake git-diff-tree raw output
4884                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4885                $diffinfo{'from_id'} = $hash_parent;
4886                $diffinfo{'to_id'}   = $hash;
4887                if (defined $file_name) {
4888                        if (defined $file_parent) {
4889                                $diffinfo{'status'} = '2';
4890                                $diffinfo{'from_file'} = $file_parent;
4891                                $diffinfo{'to_file'}   = $file_name;
4892                        } else { # assume not renamed
4893                                $diffinfo{'status'} = '1';
4894                                $diffinfo{'from_file'} = $file_name;
4895                                $diffinfo{'to_file'}   = $file_name;
4896                        }
4897                } else { # no filename given
4898                        $diffinfo{'status'} = '2';
4899                        $diffinfo{'from_file'} = $hash_parent;
4900                        $diffinfo{'to_file'}   = $hash;
4901                }
4902
4903                # non-textual hash id's can be cached
4904                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4905                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4906                        $expires = '+1d';
4907                }
4908
4909                # open patch output
4910                open $fd, "-|", git_cmd(), "diff", @diff_opts,
4911                        '-p', ($format eq 'html' ? "--full-index" : ()),
4912                        $hash_parent, $hash, "--"
4913                        or die_error(undef, "Open git-diff failed");
4914        } else  {
4915                die_error('404 Not Found', "Missing one of the blob diff parameters")
4916                        unless %diffinfo;
4917        }
4918
4919        # header
4920        if ($format eq 'html') {
4921                my $formats_nav =
4922                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
4923                                "raw");
4924                git_header_html(undef, $expires);
4925                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4926                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4927                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4928                } else {
4929                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4930                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4931                }
4932                if (defined $file_name) {
4933                        git_print_page_path($file_name, "blob", $hash_base);
4934                } else {
4935                        print "<div class=\"page_path\"></div>\n";
4936                }
4937
4938        } elsif ($format eq 'plain') {
4939                print $cgi->header(
4940                        -type => 'text/plain',
4941                        -charset => 'utf-8',
4942                        -expires => $expires,
4943                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
4944
4945                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4946
4947        } else {
4948                die_error(undef, "Unknown blobdiff format");
4949        }
4950
4951        # patch
4952        if ($format eq 'html') {
4953                print "<div class=\"page_body\">\n";
4954
4955                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
4956                close $fd;
4957
4958                print "</div>\n"; # class="page_body"
4959                git_footer_html();
4960
4961        } else {
4962                while (my $line = <$fd>) {
4963                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4964                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4965
4966                        print $line;
4967
4968                        last if $line =~ m!^\+\+\+!;
4969                }
4970                local $/ = undef;
4971                print <$fd>;
4972                close $fd;
4973        }
4974}
4975
4976sub git_blobdiff_plain {
4977        git_blobdiff('plain');
4978}
4979
4980sub git_commitdiff {
4981        my $format = shift || 'html';
4982        $hash ||= $hash_base || "HEAD";
4983        my %co = parse_commit($hash);
4984        if (!%co) {
4985                die_error(undef, "Unknown commit object");
4986        }
4987
4988        # choose format for commitdiff for merge
4989        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
4990                $hash_parent = '--cc';
4991        }
4992        # we need to prepare $formats_nav before almost any parameter munging
4993        my $formats_nav;
4994        if ($format eq 'html') {
4995                $formats_nav =
4996                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
4997                                "raw");
4998
4999                if (defined $hash_parent &&
5000                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5001                        # commitdiff with two commits given
5002                        my $hash_parent_short = $hash_parent;
5003                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5004                                $hash_parent_short = substr($hash_parent, 0, 7);
5005                        }
5006                        $formats_nav .=
5007                                ' (from';
5008                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5009                                if ($co{'parents'}[$i] eq $hash_parent) {
5010                                        $formats_nav .= ' parent ' . ($i+1);
5011                                        last;
5012                                }
5013                        }
5014                        $formats_nav .= ': ' .
5015                                $cgi->a({-href => href(action=>"commitdiff",
5016                                                       hash=>$hash_parent)},
5017                                        esc_html($hash_parent_short)) .
5018                                ')';
5019                } elsif (!$co{'parent'}) {
5020                        # --root commitdiff
5021                        $formats_nav .= ' (initial)';
5022                } elsif (scalar @{$co{'parents'}} == 1) {
5023                        # single parent commit
5024                        $formats_nav .=
5025                                ' (parent: ' .
5026                                $cgi->a({-href => href(action=>"commitdiff",
5027                                                       hash=>$co{'parent'})},
5028                                        esc_html(substr($co{'parent'}, 0, 7))) .
5029                                ')';
5030                } else {
5031                        # merge commit
5032                        if ($hash_parent eq '--cc') {
5033                                $formats_nav .= ' | ' .
5034                                        $cgi->a({-href => href(action=>"commitdiff",
5035                                                               hash=>$hash, hash_parent=>'-c')},
5036                                                'combined');
5037                        } else { # $hash_parent eq '-c'
5038                                $formats_nav .= ' | ' .
5039                                        $cgi->a({-href => href(action=>"commitdiff",
5040                                                               hash=>$hash, hash_parent=>'--cc')},
5041                                                'compact');
5042                        }
5043                        $formats_nav .=
5044                                ' (merge: ' .
5045                                join(' ', map {
5046                                        $cgi->a({-href => href(action=>"commitdiff",
5047                                                               hash=>$_)},
5048                                                esc_html(substr($_, 0, 7)));
5049                                } @{$co{'parents'}} ) .
5050                                ')';
5051                }
5052        }
5053
5054        my $hash_parent_param = $hash_parent;
5055        if (!defined $hash_parent_param) {
5056                # --cc for multiple parents, --root for parentless
5057                $hash_parent_param =
5058                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5059        }
5060
5061        # read commitdiff
5062        my $fd;
5063        my @difftree;
5064        if ($format eq 'html') {
5065                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5066                        "--no-commit-id", "--patch-with-raw", "--full-index",
5067                        $hash_parent_param, $hash, "--"
5068                        or die_error(undef, "Open git-diff-tree failed");
5069
5070                while (my $line = <$fd>) {
5071                        chomp $line;
5072                        # empty line ends raw part of diff-tree output
5073                        last unless $line;
5074                        push @difftree, scalar parse_difftree_raw_line($line);
5075                }
5076
5077        } elsif ($format eq 'plain') {
5078                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5079                        '-p', $hash_parent_param, $hash, "--"
5080                        or die_error(undef, "Open git-diff-tree failed");
5081
5082        } else {
5083                die_error(undef, "Unknown commitdiff format");
5084        }
5085
5086        # non-textual hash id's can be cached
5087        my $expires;
5088        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5089                $expires = "+1d";
5090        }
5091
5092        # write commit message
5093        if ($format eq 'html') {
5094                my $refs = git_get_references();
5095                my $ref = format_ref_marker($refs, $co{'id'});
5096
5097                git_header_html(undef, $expires);
5098                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5099                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5100                git_print_authorship(\%co);
5101                print "<div class=\"page_body\">\n";
5102                if (@{$co{'comment'}} > 1) {
5103                        print "<div class=\"log\">\n";
5104                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5105                        print "</div>\n"; # class="log"
5106                }
5107
5108        } elsif ($format eq 'plain') {
5109                my $refs = git_get_references("tags");
5110                my $tagname = git_get_rev_name_tags($hash);
5111                my $filename = basename($project) . "-$hash.patch";
5112
5113                print $cgi->header(
5114                        -type => 'text/plain',
5115                        -charset => 'utf-8',
5116                        -expires => $expires,
5117                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5118                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5119                print "From: " . to_utf8($co{'author'}) . "\n";
5120                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5121                print "Subject: " . to_utf8($co{'title'}) . "\n";
5122
5123                print "X-Git-Tag: $tagname\n" if $tagname;
5124                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5125
5126                foreach my $line (@{$co{'comment'}}) {
5127                        print to_utf8($line) . "\n";
5128                }
5129                print "---\n\n";
5130        }
5131
5132        # write patch
5133        if ($format eq 'html') {
5134                my $use_parents = !defined $hash_parent ||
5135                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5136                git_difftree_body(\@difftree, $hash,
5137                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5138                print "<br/>\n";
5139
5140                git_patchset_body($fd, \@difftree, $hash,
5141                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5142                close $fd;
5143                print "</div>\n"; # class="page_body"
5144                git_footer_html();
5145
5146        } elsif ($format eq 'plain') {
5147                local $/ = undef;
5148                print <$fd>;
5149                close $fd
5150                        or print "Reading git-diff-tree failed\n";
5151        }
5152}
5153
5154sub git_commitdiff_plain {
5155        git_commitdiff('plain');
5156}
5157
5158sub git_history {
5159        if (!defined $hash_base) {
5160                $hash_base = git_get_head_hash($project);
5161        }
5162        if (!defined $page) {
5163                $page = 0;
5164        }
5165        my $ftype;
5166        my %co = parse_commit($hash_base);
5167        if (!%co) {
5168                die_error(undef, "Unknown commit object");
5169        }
5170
5171        my $refs = git_get_references();
5172        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5173
5174        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5175                                       $file_name, "--full-history");
5176        if (!@commitlist) {
5177                die_error('404 Not Found', "No such file or directory on given branch");
5178        }
5179
5180        if (!defined $hash && defined $file_name) {
5181                # some commits could have deleted file in question,
5182                # and not have it in tree, but one of them has to have it
5183                for (my $i = 0; $i <= @commitlist; $i++) {
5184                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5185                        last if defined $hash;
5186                }
5187        }
5188        if (defined $hash) {
5189                $ftype = git_get_type($hash);
5190        }
5191        if (!defined $ftype) {
5192                die_error(undef, "Unknown type of object");
5193        }
5194
5195        my $paging_nav = '';
5196        if ($page > 0) {
5197                $paging_nav .=
5198                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5199                                               file_name=>$file_name)},
5200                                "first");
5201                $paging_nav .= " &sdot; " .
5202                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5203                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5204        } else {
5205                $paging_nav .= "first";
5206                $paging_nav .= " &sdot; prev";
5207        }
5208        my $next_link = '';
5209        if ($#commitlist >= 100) {
5210                $next_link =
5211                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5212                                 -accesskey => "n", -title => "Alt-n"}, "next");
5213                $paging_nav .= " &sdot; $next_link";
5214        } else {
5215                $paging_nav .= " &sdot; next";
5216        }
5217
5218        git_header_html();
5219        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5220        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5221        git_print_page_path($file_name, $ftype, $hash_base);
5222
5223        git_history_body(\@commitlist, 0, 99,
5224                         $refs, $hash_base, $ftype, $next_link);
5225
5226        git_footer_html();
5227}
5228
5229sub git_search {
5230        my ($have_search) = gitweb_check_feature('search');
5231        if (!$have_search) {
5232                die_error('403 Permission denied', "Permission denied");
5233        }
5234        if (!defined $searchtext) {
5235                die_error(undef, "Text field empty");
5236        }
5237        if (!defined $hash) {
5238                $hash = git_get_head_hash($project);
5239        }
5240        my %co = parse_commit($hash);
5241        if (!%co) {
5242                die_error(undef, "Unknown commit object");
5243        }
5244        if (!defined $page) {
5245                $page = 0;
5246        }
5247
5248        $searchtype ||= 'commit';
5249        if ($searchtype eq 'pickaxe') {
5250                # pickaxe may take all resources of your box and run for several minutes
5251                # with every query - so decide by yourself how public you make this feature
5252                my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5253                if (!$have_pickaxe) {
5254                        die_error('403 Permission denied', "Permission denied");
5255                }
5256        }
5257        if ($searchtype eq 'grep') {
5258                my ($have_grep) = gitweb_check_feature('grep');
5259                if (!$have_grep) {
5260                        die_error('403 Permission denied', "Permission denied");
5261                }
5262        }
5263
5264        git_header_html();
5265
5266        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5267                my $greptype;
5268                if ($searchtype eq 'commit') {
5269                        $greptype = "--grep=";
5270                } elsif ($searchtype eq 'author') {
5271                        $greptype = "--author=";
5272                } elsif ($searchtype eq 'committer') {
5273                        $greptype = "--committer=";
5274                }
5275                $greptype .= $searchtext;
5276                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5277                                               $greptype, '--regexp-ignore-case',
5278                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5279
5280                my $paging_nav = '';
5281                if ($page > 0) {
5282                        $paging_nav .=
5283                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5284                                                       searchtext=>$searchtext,
5285                                                       searchtype=>$searchtype)},
5286                                        "first");
5287                        $paging_nav .= " &sdot; " .
5288                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5289                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5290                } else {
5291                        $paging_nav .= "first";
5292                        $paging_nav .= " &sdot; prev";
5293                }
5294                my $next_link = '';
5295                if ($#commitlist >= 100) {
5296                        $next_link =
5297                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5298                                         -accesskey => "n", -title => "Alt-n"}, "next");
5299                        $paging_nav .= " &sdot; $next_link";
5300                } else {
5301                        $paging_nav .= " &sdot; next";
5302                }
5303
5304                if ($#commitlist >= 100) {
5305                }
5306
5307                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5308                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5309                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5310        }
5311
5312        if ($searchtype eq 'pickaxe') {
5313                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5314                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5315
5316                print "<table class=\"pickaxe search\">\n";
5317                my $alternate = 1;
5318                $/ = "\n";
5319                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5320                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5321                        ($search_use_regexp ? '--pickaxe-regex' : ());
5322                undef %co;
5323                my @files;
5324                while (my $line = <$fd>) {
5325                        chomp $line;
5326                        next unless $line;
5327
5328                        my %set = parse_difftree_raw_line($line);
5329                        if (defined $set{'commit'}) {
5330                                # finish previous commit
5331                                if (%co) {
5332                                        print "</td>\n" .
5333                                              "<td class=\"link\">" .
5334                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5335                                              " | " .
5336                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5337                                        print "</td>\n" .
5338                                              "</tr>\n";
5339                                }
5340
5341                                if ($alternate) {
5342                                        print "<tr class=\"dark\">\n";
5343                                } else {
5344                                        print "<tr class=\"light\">\n";
5345                                }
5346                                $alternate ^= 1;
5347                                %co = parse_commit($set{'commit'});
5348                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5349                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5350                                      "<td><i>$author</i></td>\n" .
5351                                      "<td>" .
5352                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5353                                              -class => "list subject"},
5354                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5355                        } elsif (defined $set{'to_id'}) {
5356                                next if ($set{'to_id'} =~ m/^0{40}$/);
5357
5358                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5359                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5360                                              -class => "list"},
5361                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5362                                      "<br/>\n";
5363                        }
5364                }
5365                close $fd;
5366
5367                # finish last commit (warning: repetition!)
5368                if (%co) {
5369                        print "</td>\n" .
5370                              "<td class=\"link\">" .
5371                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5372                              " | " .
5373                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5374                        print "</td>\n" .
5375                              "</tr>\n";
5376                }
5377
5378                print "</table>\n";
5379        }
5380
5381        if ($searchtype eq 'grep') {
5382                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5383                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5384
5385                print "<table class=\"grep_search\">\n";
5386                my $alternate = 1;
5387                my $matches = 0;
5388                $/ = "\n";
5389                open my $fd, "-|", git_cmd(), 'grep', '-n',
5390                        $search_use_regexp ? ('-E', '-i') : '-F',
5391                        $searchtext, $co{'tree'};
5392                my $lastfile = '';
5393                while (my $line = <$fd>) {
5394                        chomp $line;
5395                        my ($file, $lno, $ltext, $binary);
5396                        last if ($matches++ > 1000);
5397                        if ($line =~ /^Binary file (.+) matches$/) {
5398                                $file = $1;
5399                                $binary = 1;
5400                        } else {
5401                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5402                        }
5403                        if ($file ne $lastfile) {
5404                                $lastfile and print "</td></tr>\n";
5405                                if ($alternate++) {
5406                                        print "<tr class=\"dark\">\n";
5407                                } else {
5408                                        print "<tr class=\"light\">\n";
5409                                }
5410                                print "<td class=\"list\">".
5411                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5412                                                               file_name=>"$file"),
5413                                                -class => "list"}, esc_path($file));
5414                                print "</td><td>\n";
5415                                $lastfile = $file;
5416                        }
5417                        if ($binary) {
5418                                print "<div class=\"binary\">Binary file</div>\n";
5419                        } else {
5420                                $ltext = untabify($ltext);
5421                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5422                                        $ltext = esc_html($1, -nbsp=>1);
5423                                        $ltext .= '<span class="match">';
5424                                        $ltext .= esc_html($2, -nbsp=>1);
5425                                        $ltext .= '</span>';
5426                                        $ltext .= esc_html($3, -nbsp=>1);
5427                                } else {
5428                                        $ltext = esc_html($ltext, -nbsp=>1);
5429                                }
5430                                print "<div class=\"pre\">" .
5431                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5432                                                               file_name=>"$file").'#l'.$lno,
5433                                                -class => "linenr"}, sprintf('%4i', $lno))
5434                                        . ' ' .  $ltext . "</div>\n";
5435                        }
5436                }
5437                if ($lastfile) {
5438                        print "</td></tr>\n";
5439                        if ($matches > 1000) {
5440                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5441                        }
5442                } else {
5443                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5444                }
5445                close $fd;
5446
5447                print "</table>\n";
5448        }
5449        git_footer_html();
5450}
5451
5452sub git_search_help {
5453        git_header_html();
5454        git_print_page_nav('','', $hash,$hash,$hash);
5455        print <<EOT;
5456<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5457regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5458the pattern entered is recognized as the POSIX extended
5459<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5460insensitive).</p>
5461<dl>
5462<dt><b>commit</b></dt>
5463<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5464EOT
5465        my ($have_grep) = gitweb_check_feature('grep');
5466        if ($have_grep) {
5467                print <<EOT;
5468<dt><b>grep</b></dt>
5469<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5470    a different one) are searched for the given pattern. On large trees, this search can take
5471a while and put some strain on the server, so please use it with some consideration. Note that
5472due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5473case-sensitive.</dd>
5474EOT
5475        }
5476        print <<EOT;
5477<dt><b>author</b></dt>
5478<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5479<dt><b>committer</b></dt>
5480<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5481EOT
5482        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5483        if ($have_pickaxe) {
5484                print <<EOT;
5485<dt><b>pickaxe</b></dt>
5486<dd>All commits that caused the string to appear or disappear from any file (changes that
5487added, removed or "modified" the string) will be listed. This search can take a while and
5488takes a lot of strain on the server, so please use it wisely. Note that since you may be
5489interested even in changes just changing the case as well, this search is case sensitive.</dd>
5490EOT
5491        }
5492        print "</dl>\n";
5493        git_footer_html();
5494}
5495
5496sub git_shortlog {
5497        my $head = git_get_head_hash($project);
5498        if (!defined $hash) {
5499                $hash = $head;
5500        }
5501        if (!defined $page) {
5502                $page = 0;
5503        }
5504        my $refs = git_get_references();
5505
5506        my @commitlist = parse_commits($hash, 101, (100 * $page));
5507
5508        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1)));
5509        my $next_link = '';
5510        if ($#commitlist >= 100) {
5511                $next_link =
5512                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5513                                 -accesskey => "n", -title => "Alt-n"}, "next");
5514        }
5515
5516        git_header_html();
5517        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5518        git_print_header_div('summary', $project);
5519
5520        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5521
5522        git_footer_html();
5523}
5524
5525## ......................................................................
5526## feeds (RSS, Atom; OPML)
5527
5528sub git_feed {
5529        my $format = shift || 'atom';
5530        my ($have_blame) = gitweb_check_feature('blame');
5531
5532        # Atom: http://www.atomenabled.org/developers/syndication/
5533        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5534        if ($format ne 'rss' && $format ne 'atom') {
5535                die_error(undef, "Unknown web feed format");
5536        }
5537
5538        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5539        my $head = $hash || 'HEAD';
5540        my @commitlist = parse_commits($head, 150, 0, $file_name);
5541
5542        my %latest_commit;
5543        my %latest_date;
5544        my $content_type = "application/$format+xml";
5545        if (defined $cgi->http('HTTP_ACCEPT') &&
5546                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5547                # browser (feed reader) prefers text/xml
5548                $content_type = 'text/xml';
5549        }
5550        if (defined($commitlist[0])) {
5551                %latest_commit = %{$commitlist[0]};
5552                %latest_date   = parse_date($latest_commit{'author_epoch'});
5553                print $cgi->header(
5554                        -type => $content_type,
5555                        -charset => 'utf-8',
5556                        -last_modified => $latest_date{'rfc2822'});
5557        } else {
5558                print $cgi->header(
5559                        -type => $content_type,
5560                        -charset => 'utf-8');
5561        }
5562
5563        # Optimization: skip generating the body if client asks only
5564        # for Last-Modified date.
5565        return if ($cgi->request_method() eq 'HEAD');
5566
5567        # header variables
5568        my $title = "$site_name - $project/$action";
5569        my $feed_type = 'log';
5570        if (defined $hash) {
5571                $title .= " - '$hash'";
5572                $feed_type = 'branch log';
5573                if (defined $file_name) {
5574                        $title .= " :: $file_name";
5575                        $feed_type = 'history';
5576                }
5577        } elsif (defined $file_name) {
5578                $title .= " - $file_name";
5579                $feed_type = 'history';
5580        }
5581        $title .= " $feed_type";
5582        my $descr = git_get_project_description($project);
5583        if (defined $descr) {
5584                $descr = esc_html($descr);
5585        } else {
5586                $descr = "$project " .
5587                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5588                         " feed";
5589        }
5590        my $owner = git_get_project_owner($project);
5591        $owner = esc_html($owner);
5592
5593        #header
5594        my $alt_url;
5595        if (defined $file_name) {
5596                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5597        } elsif (defined $hash) {
5598                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5599        } else {
5600                $alt_url = href(-full=>1, action=>"summary");
5601        }
5602        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5603        if ($format eq 'rss') {
5604                print <<XML;
5605<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5606<channel>
5607XML
5608                print "<title>$title</title>\n" .
5609                      "<link>$alt_url</link>\n" .
5610                      "<description>$descr</description>\n" .
5611                      "<language>en</language>\n";
5612        } elsif ($format eq 'atom') {
5613                print <<XML;
5614<feed xmlns="http://www.w3.org/2005/Atom">
5615XML
5616                print "<title>$title</title>\n" .
5617                      "<subtitle>$descr</subtitle>\n" .
5618                      '<link rel="alternate" type="text/html" href="' .
5619                      $alt_url . '" />' . "\n" .
5620                      '<link rel="self" type="' . $content_type . '" href="' .
5621                      $cgi->self_url() . '" />' . "\n" .
5622                      "<id>" . href(-full=>1) . "</id>\n" .
5623                      # use project owner for feed author
5624                      "<author><name>$owner</name></author>\n";
5625                if (defined $favicon) {
5626                        print "<icon>" . esc_url($favicon) . "</icon>\n";
5627                }
5628                if (defined $logo_url) {
5629                        # not twice as wide as tall: 72 x 27 pixels
5630                        print "<logo>" . esc_url($logo) . "</logo>\n";
5631                }
5632                if (! %latest_date) {
5633                        # dummy date to keep the feed valid until commits trickle in:
5634                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
5635                } else {
5636                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
5637                }
5638        }
5639
5640        # contents
5641        for (my $i = 0; $i <= $#commitlist; $i++) {
5642                my %co = %{$commitlist[$i]};
5643                my $commit = $co{'id'};
5644                # we read 150, we always show 30 and the ones more recent than 48 hours
5645                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5646                        last;
5647                }
5648                my %cd = parse_date($co{'author_epoch'});
5649
5650                # get list of changed files
5651                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5652                        $co{'parent'} || "--root",
5653                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
5654                        or next;
5655                my @difftree = map { chomp; $_ } <$fd>;
5656                close $fd
5657                        or next;
5658
5659                # print element (entry, item)
5660                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5661                if ($format eq 'rss') {
5662                        print "<item>\n" .
5663                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
5664                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
5665                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5666                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5667                              "<link>$co_url</link>\n" .
5668                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
5669                              "<content:encoded>" .
5670                              "<![CDATA[\n";
5671                } elsif ($format eq 'atom') {
5672                        print "<entry>\n" .
5673                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5674                              "<updated>$cd{'iso-8601'}</updated>\n" .
5675                              "<author>\n" .
5676                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5677                        if ($co{'author_email'}) {
5678                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5679                        }
5680                        print "</author>\n" .
5681                              # use committer for contributor
5682                              "<contributor>\n" .
5683                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5684                        if ($co{'committer_email'}) {
5685                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5686                        }
5687                        print "</contributor>\n" .
5688                              "<published>$cd{'iso-8601'}</published>\n" .
5689                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5690                              "<id>$co_url</id>\n" .
5691                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5692                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5693                }
5694                my $comment = $co{'comment'};
5695                print "<pre>\n";
5696                foreach my $line (@$comment) {
5697                        $line = esc_html($line);
5698                        print "$line\n";
5699                }
5700                print "</pre><ul>\n";
5701                foreach my $difftree_line (@difftree) {
5702                        my %difftree = parse_difftree_raw_line($difftree_line);
5703                        next if !$difftree{'from_id'};
5704
5705                        my $file = $difftree{'file'} || $difftree{'to_file'};
5706
5707                        print "<li>" .
5708                              "[" .
5709                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5710                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5711                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5712                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
5713                                      -title => "diff"}, 'D');
5714                        if ($have_blame) {
5715                                print $cgi->a({-href => href(-full=>1, action=>"blame",
5716                                                             file_name=>$file, hash_base=>$commit),
5717                                              -title => "blame"}, 'B');
5718                        }
5719                        # if this is not a feed of a file history
5720                        if (!defined $file_name || $file_name ne $file) {
5721                                print $cgi->a({-href => href(-full=>1, action=>"history",
5722                                                             file_name=>$file, hash=>$commit),
5723                                              -title => "history"}, 'H');
5724                        }
5725                        $file = esc_path($file);
5726                        print "] ".
5727                              "$file</li>\n";
5728                }
5729                if ($format eq 'rss') {
5730                        print "</ul>]]>\n" .
5731                              "</content:encoded>\n" .
5732                              "</item>\n";
5733                } elsif ($format eq 'atom') {
5734                        print "</ul>\n</div>\n" .
5735                              "</content>\n" .
5736                              "</entry>\n";
5737                }
5738        }
5739
5740        # end of feed
5741        if ($format eq 'rss') {
5742                print "</channel>\n</rss>\n";
5743        }       elsif ($format eq 'atom') {
5744                print "</feed>\n";
5745        }
5746}
5747
5748sub git_rss {
5749        git_feed('rss');
5750}
5751
5752sub git_atom {
5753        git_feed('atom');
5754}
5755
5756sub git_opml {
5757        my @list = git_get_projects_list();
5758
5759        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5760        print <<XML;
5761<?xml version="1.0" encoding="utf-8"?>
5762<opml version="1.0">
5763<head>
5764  <title>$site_name OPML Export</title>
5765</head>
5766<body>
5767<outline text="git RSS feeds">
5768XML
5769
5770        foreach my $pr (@list) {
5771                my %proj = %$pr;
5772                my $head = git_get_head_hash($proj{'path'});
5773                if (!defined $head) {
5774                        next;
5775                }
5776                $git_dir = "$projectroot/$proj{'path'}";
5777                my %co = parse_commit($head);
5778                if (!%co) {
5779                        next;
5780                }
5781
5782                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5783                my $rss  = "$my_url?p=$proj{'path'};a=rss";
5784                my $html = "$my_url?p=$proj{'path'};a=summary";
5785                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5786        }
5787        print <<XML;
5788</outline>
5789</body>
5790</opml>
5791XML
5792}