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