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