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