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