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