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