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