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