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