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