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