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