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