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