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