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