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