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