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