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