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