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