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