00857347d9834629ae3c69c3a1e5925e87353827
   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                if ($my_url =~ s,\Q$path_info\E$,, &&
  58                    $my_uri =~ s,\Q$path_info\E$,, &&
  59                    defined $ENV{'SCRIPT_NAME'}) {
  60                        $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
  61                }
  62        }
  63
  64        # target of the home link on top of all pages
  65        our $home_link = $my_uri || "/";
  66}
  67
  68# core git executable to use
  69# this can just be "git" if your webserver has a sensible PATH
  70our $GIT = "++GIT_BINDIR++/git";
  71
  72# absolute fs-path which will be prepended to the project path
  73#our $projectroot = "/pub/scm";
  74our $projectroot = "++GITWEB_PROJECTROOT++";
  75
  76# fs traversing limit for getting project list
  77# the number is relative to the projectroot
  78our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
  79
  80# string of the home link on top of all pages
  81our $home_link_str = "++GITWEB_HOME_LINK_STR++";
  82
  83# name of your site or organization to appear in page titles
  84# replace this with something more descriptive for clearer bookmarks
  85our $site_name = "++GITWEB_SITENAME++"
  86                 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
  87
  88# html snippet to include in the <head> section of each page
  89our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
  90# filename of html text to include at top of each page
  91our $site_header = "++GITWEB_SITE_HEADER++";
  92# html text to include at home page
  93our $home_text = "++GITWEB_HOMETEXT++";
  94# filename of html text to include at bottom of each page
  95our $site_footer = "++GITWEB_SITE_FOOTER++";
  96
  97# URI of stylesheets
  98our @stylesheets = ("++GITWEB_CSS++");
  99# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
 100our $stylesheet = undef;
 101# URI of GIT logo (72x27 size)
 102our $logo = "++GITWEB_LOGO++";
 103# URI of GIT favicon, assumed to be image/png type
 104our $favicon = "++GITWEB_FAVICON++";
 105# URI of gitweb.js (JavaScript code for gitweb)
 106our $javascript = "++GITWEB_JS++";
 107
 108# URI and label (title) of GIT logo link
 109#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
 110#our $logo_label = "git documentation";
 111our $logo_url = "http://git-scm.com/";
 112our $logo_label = "git homepage";
 113
 114# source of projects list
 115our $projects_list = "++GITWEB_LIST++";
 116
 117# the width (in characters) of the projects list "Description" column
 118our $projects_list_description_width = 25;
 119
 120# group projects by category on the projects list
 121# (enabled if this variable evaluates to true)
 122our $projects_list_group_categories = 0;
 123
 124# default category if none specified
 125# (leave the empty string for no category)
 126our $project_list_default_category = "";
 127
 128# default order of projects list
 129# valid values are none, project, descr, owner, and age
 130our $default_projects_order = "project";
 131
 132# show repository only if this file exists
 133# (only effective if this variable evaluates to true)
 134our $export_ok = "++GITWEB_EXPORT_OK++";
 135
 136# don't generate age column on the projects list page
 137our $omit_age_column = 0;
 138
 139# show repository only if this subroutine returns true
 140# when given the path to the project, for example:
 141#    sub { return -e "$_[0]/git-daemon-export-ok"; }
 142our $export_auth_hook = undef;
 143
 144# only allow viewing of repositories also shown on the overview page
 145our $strict_export = "++GITWEB_STRICT_EXPORT++";
 146
 147# list of git base URLs used for URL to where fetch project from,
 148# i.e. full URL is "$git_base_url/$project"
 149our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
 150
 151# default blob_plain mimetype and default charset for text/plain blob
 152our $default_blob_plain_mimetype = 'text/plain';
 153our $default_text_plain_charset  = undef;
 154
 155# file to use for guessing MIME types before trying /etc/mime.types
 156# (relative to the current git repository)
 157our $mimetypes_file = undef;
 158
 159# assume this charset if line contains non-UTF-8 characters;
 160# it should be valid encoding (see Encoding::Supported(3pm) for list),
 161# for which encoding all byte sequences are valid, for example
 162# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
 163# could be even 'utf-8' for the old behavior)
 164our $fallback_encoding = 'latin1';
 165
 166# rename detection options for git-diff and git-diff-tree
 167# - default is '-M', with the cost proportional to
 168#   (number of removed files) * (number of new files).
 169# - more costly is '-C' (which implies '-M'), with the cost proportional to
 170#   (number of changed files + number of removed files) * (number of new files)
 171# - even more costly is '-C', '--find-copies-harder' with cost
 172#   (number of files in the original tree) * (number of new files)
 173# - one might want to include '-B' option, e.g. '-B', '-M'
 174our @diff_opts = ('-M'); # taken from git_commit
 175
 176# Disables features that would allow repository owners to inject script into
 177# the gitweb domain.
 178our $prevent_xss = 0;
 179
 180# Path to the highlight executable to use (must be the one from
 181# http://www.andre-simon.de due to assumptions about parameters and output).
 182# Useful if highlight is not installed on your webserver's PATH.
 183# [Default: highlight]
 184our $highlight_bin = "++HIGHLIGHT_BIN++";
 185
 186# information about snapshot formats that gitweb is capable of serving
 187our %known_snapshot_formats = (
 188        # name => {
 189        #       'display' => display name,
 190        #       'type' => mime type,
 191        #       'suffix' => filename suffix,
 192        #       'format' => --format for git-archive,
 193        #       'compressor' => [compressor command and arguments]
 194        #                       (array reference, optional)
 195        #       'disabled' => boolean (optional)}
 196        #
 197        'tgz' => {
 198                'display' => 'tar.gz',
 199                'type' => 'application/x-gzip',
 200                'suffix' => '.tar.gz',
 201                'format' => 'tar',
 202                'compressor' => ['gzip', '-n']},
 203
 204        'tbz2' => {
 205                'display' => 'tar.bz2',
 206                'type' => 'application/x-bzip2',
 207                'suffix' => '.tar.bz2',
 208                'format' => 'tar',
 209                'compressor' => ['bzip2']},
 210
 211        'txz' => {
 212                'display' => 'tar.xz',
 213                'type' => 'application/x-xz',
 214                'suffix' => '.tar.xz',
 215                'format' => 'tar',
 216                'compressor' => ['xz'],
 217                'disabled' => 1},
 218
 219        'zip' => {
 220                'display' => 'zip',
 221                'type' => 'application/x-zip',
 222                'suffix' => '.zip',
 223                'format' => 'zip'},
 224);
 225
 226# Aliases so we understand old gitweb.snapshot values in repository
 227# configuration.
 228our %known_snapshot_format_aliases = (
 229        'gzip'  => 'tgz',
 230        'bzip2' => 'tbz2',
 231        'xz'    => 'txz',
 232
 233        # backward compatibility: legacy gitweb config support
 234        'x-gzip' => undef, 'gz' => undef,
 235        'x-bzip2' => undef, 'bz2' => undef,
 236        'x-zip' => undef, '' => undef,
 237);
 238
 239# Pixel sizes for icons and avatars. If the default font sizes or lineheights
 240# are changed, it may be appropriate to change these values too via
 241# $GITWEB_CONFIG.
 242our %avatar_size = (
 243        'default' => 16,
 244        'double'  => 32
 245);
 246
 247# Used to set the maximum load that we will still respond to gitweb queries.
 248# If server load exceed this value then return "503 server busy" error.
 249# If gitweb cannot determined server load, it is taken to be 0.
 250# Leave it undefined (or set to 'undef') to turn off load checking.
 251our $maxload = 300;
 252
 253# configuration for 'highlight' (http://www.andre-simon.de/)
 254# match by basename
 255our %highlight_basename = (
 256        #'Program' => 'py',
 257        #'Library' => 'py',
 258        'SConstruct' => 'py', # SCons equivalent of Makefile
 259        'Makefile' => 'make',
 260);
 261# match by extension
 262our %highlight_ext = (
 263        # main extensions, defining name of syntax;
 264        # see files in /usr/share/highlight/langDefs/ directory
 265        map { $_ => $_ }
 266                qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
 267        # alternate extensions, see /etc/highlight/filetypes.conf
 268        'h' => 'c',
 269        map { $_ => 'sh'  } qw(bash zsh ksh),
 270        map { $_ => 'cpp' } qw(cxx c++ cc),
 271        map { $_ => 'php' } qw(php3 php4 php5 phps),
 272        map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi'
 273        map { $_ => 'make'} qw(mak mk),
 274        map { $_ => 'xml' } qw(xhtml html htm),
 275);
 276
 277# You define site-wide feature defaults here; override them with
 278# $GITWEB_CONFIG as necessary.
 279our %feature = (
 280        # feature => {
 281        #       'sub' => feature-sub (subroutine),
 282        #       'override' => allow-override (boolean),
 283        #       'default' => [ default options...] (array reference)}
 284        #
 285        # if feature is overridable (it means that allow-override has true value),
 286        # then feature-sub will be called with default options as parameters;
 287        # return value of feature-sub indicates if to enable specified feature
 288        #
 289        # if there is no 'sub' key (no feature-sub), then feature cannot be
 290        # overridden
 291        #
 292        # use gitweb_get_feature(<feature>) to retrieve the <feature> value
 293        # (an array) or gitweb_check_feature(<feature>) to check if <feature>
 294        # is enabled
 295
 296        # Enable the 'blame' blob view, showing the last commit that modified
 297        # each line in the file. This can be very CPU-intensive.
 298
 299        # To enable system wide have in $GITWEB_CONFIG
 300        # $feature{'blame'}{'default'} = [1];
 301        # To have project specific config enable override in $GITWEB_CONFIG
 302        # $feature{'blame'}{'override'} = 1;
 303        # and in project config gitweb.blame = 0|1;
 304        'blame' => {
 305                'sub' => sub { feature_bool('blame', @_) },
 306                'override' => 0,
 307                'default' => [0]},
 308
 309        # Enable the 'snapshot' link, providing a compressed archive of any
 310        # tree. This can potentially generate high traffic if you have large
 311        # project.
 312
 313        # Value is a list of formats defined in %known_snapshot_formats that
 314        # you wish to offer.
 315        # To disable system wide have in $GITWEB_CONFIG
 316        # $feature{'snapshot'}{'default'} = [];
 317        # To have project specific config enable override in $GITWEB_CONFIG
 318        # $feature{'snapshot'}{'override'} = 1;
 319        # and in project config, a comma-separated list of formats or "none"
 320        # to disable.  Example: gitweb.snapshot = tbz2,zip;
 321        'snapshot' => {
 322                'sub' => \&feature_snapshot,
 323                'override' => 0,
 324                'default' => ['tgz']},
 325
 326        # Enable text search, which will list the commits which match author,
 327        # committer or commit text to a given string.  Enabled by default.
 328        # Project specific override is not supported.
 329        #
 330        # Note that this controls all search features, which means that if
 331        # it is disabled, then 'grep' and 'pickaxe' search would also be
 332        # disabled.
 333        'search' => {
 334                'override' => 0,
 335                'default' => [1]},
 336
 337        # Enable grep search, which will list the files in currently selected
 338        # tree containing the given string. Enabled by default. This can be
 339        # potentially CPU-intensive, of course.
 340        # Note that you need to have 'search' feature enabled too.
 341
 342        # To enable system wide have in $GITWEB_CONFIG
 343        # $feature{'grep'}{'default'} = [1];
 344        # To have project specific config enable override in $GITWEB_CONFIG
 345        # $feature{'grep'}{'override'} = 1;
 346        # and in project config gitweb.grep = 0|1;
 347        'grep' => {
 348                'sub' => sub { feature_bool('grep', @_) },
 349                'override' => 0,
 350                'default' => [1]},
 351
 352        # Enable the pickaxe search, which will list the commits that modified
 353        # a given string in a file. This can be practical and quite faster
 354        # alternative to 'blame', but still potentially CPU-intensive.
 355        # Note that you need to have 'search' feature enabled too.
 356
 357        # To enable system wide have in $GITWEB_CONFIG
 358        # $feature{'pickaxe'}{'default'} = [1];
 359        # To have project specific config enable override in $GITWEB_CONFIG
 360        # $feature{'pickaxe'}{'override'} = 1;
 361        # and in project config gitweb.pickaxe = 0|1;
 362        'pickaxe' => {
 363                'sub' => sub { feature_bool('pickaxe', @_) },
 364                'override' => 0,
 365                'default' => [1]},
 366
 367        # Enable showing size of blobs in a 'tree' view, in a separate
 368        # column, similar to what 'ls -l' does.  This cost a bit of IO.
 369
 370        # To disable system wide have in $GITWEB_CONFIG
 371        # $feature{'show-sizes'}{'default'} = [0];
 372        # To have project specific config enable override in $GITWEB_CONFIG
 373        # $feature{'show-sizes'}{'override'} = 1;
 374        # and in project config gitweb.showsizes = 0|1;
 375        'show-sizes' => {
 376                'sub' => sub { feature_bool('showsizes', @_) },
 377                'override' => 0,
 378                'default' => [1]},
 379
 380        # Make gitweb use an alternative format of the URLs which can be
 381        # more readable and natural-looking: project name is embedded
 382        # directly in the path and the query string contains other
 383        # auxiliary information. All gitweb installations recognize
 384        # URL in either format; this configures in which formats gitweb
 385        # generates links.
 386
 387        # To enable system wide have in $GITWEB_CONFIG
 388        # $feature{'pathinfo'}{'default'} = [1];
 389        # Project specific override is not supported.
 390
 391        # Note that you will need to change the default location of CSS,
 392        # favicon, logo and possibly other files to an absolute URL. Also,
 393        # if gitweb.cgi serves as your indexfile, you will need to force
 394        # $my_uri to contain the script name in your $GITWEB_CONFIG.
 395        'pathinfo' => {
 396                'override' => 0,
 397                'default' => [0]},
 398
 399        # Make gitweb consider projects in project root subdirectories
 400        # to be forks of existing projects. Given project $projname.git,
 401        # projects matching $projname/*.git will not be shown in the main
 402        # projects list, instead a '+' mark will be added to $projname
 403        # there and a 'forks' view will be enabled for the project, listing
 404        # all the forks. If project list is taken from a file, forks have
 405        # to be listed after the main project.
 406
 407        # To enable system wide have in $GITWEB_CONFIG
 408        # $feature{'forks'}{'default'} = [1];
 409        # Project specific override is not supported.
 410        'forks' => {
 411                'override' => 0,
 412                'default' => [0]},
 413
 414        # Insert custom links to the action bar of all project pages.
 415        # This enables you mainly to link to third-party scripts integrating
 416        # into gitweb; e.g. git-browser for graphical history representation
 417        # or custom web-based repository administration interface.
 418
 419        # The 'default' value consists of a list of triplets in the form
 420        # (label, link, position) where position is the label after which
 421        # to insert the link and link is a format string where %n expands
 422        # to the project name, %f to the project path within the filesystem,
 423        # %h to the current hash (h gitweb parameter) and %b to the current
 424        # hash base (hb gitweb parameter); %% expands to %.
 425
 426        # To enable system wide have in $GITWEB_CONFIG e.g.
 427        # $feature{'actions'}{'default'} = [('graphiclog',
 428        #       '/git-browser/by-commit.html?r=%n', 'summary')];
 429        # Project specific override is not supported.
 430        'actions' => {
 431                'override' => 0,
 432                'default' => []},
 433
 434        # Allow gitweb scan project content tags of project repository,
 435        # and display the popular Web 2.0-ish "tag cloud" near the projects
 436        # list.  Note that this is something COMPLETELY different from the
 437        # normal Git tags.
 438
 439        # gitweb by itself can show existing tags, but it does not handle
 440        # tagging itself; you need to do it externally, outside gitweb.
 441        # The format is described in git_get_project_ctags() subroutine.
 442        # You may want to install the HTML::TagCloud Perl module to get
 443        # a pretty tag cloud instead of just a list of tags.
 444
 445        # To enable system wide have in $GITWEB_CONFIG
 446        # $feature{'ctags'}{'default'} = [1];
 447        # Project specific override is not supported.
 448
 449        # In the future whether ctags editing is enabled might depend
 450        # on the value, but using 1 should always mean no editing of ctags.
 451        'ctags' => {
 452                'override' => 0,
 453                'default' => [0]},
 454
 455        # The maximum number of patches in a patchset generated in patch
 456        # view. Set this to 0 or undef to disable patch view, or to a
 457        # negative number to remove any limit.
 458
 459        # To disable system wide have in $GITWEB_CONFIG
 460        # $feature{'patches'}{'default'} = [0];
 461        # To have project specific config enable override in $GITWEB_CONFIG
 462        # $feature{'patches'}{'override'} = 1;
 463        # and in project config gitweb.patches = 0|n;
 464        # where n is the maximum number of patches allowed in a patchset.
 465        'patches' => {
 466                'sub' => \&feature_patches,
 467                'override' => 0,
 468                'default' => [16]},
 469
 470        # Avatar support. When this feature is enabled, views such as
 471        # shortlog or commit will display an avatar associated with
 472        # the email of the committer(s) and/or author(s).
 473
 474        # Currently available providers are gravatar and picon.
 475        # If an unknown provider is specified, the feature is disabled.
 476
 477        # Gravatar depends on Digest::MD5.
 478        # Picon currently relies on the indiana.edu database.
 479
 480        # To enable system wide have in $GITWEB_CONFIG
 481        # $feature{'avatar'}{'default'} = ['<provider>'];
 482        # where <provider> is either gravatar or picon.
 483        # To have project specific config enable override in $GITWEB_CONFIG
 484        # $feature{'avatar'}{'override'} = 1;
 485        # and in project config gitweb.avatar = <provider>;
 486        'avatar' => {
 487                'sub' => \&feature_avatar,
 488                'override' => 0,
 489                'default' => ['']},
 490
 491        # Enable displaying how much time and how many git commands
 492        # it took to generate and display page.  Disabled by default.
 493        # Project specific override is not supported.
 494        'timed' => {
 495                'override' => 0,
 496                'default' => [0]},
 497
 498        # Enable turning some links into links to actions which require
 499        # JavaScript to run (like 'blame_incremental').  Not enabled by
 500        # default.  Project specific override is currently not supported.
 501        'javascript-actions' => {
 502                'override' => 0,
 503                'default' => [0]},
 504
 505        # Enable and configure ability to change common timezone for dates
 506        # in gitweb output via JavaScript.  Enabled by default.
 507        # Project specific override is not supported.
 508        'javascript-timezone' => {
 509                'override' => 0,
 510                'default' => [
 511                        'local',     # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
 512                                     # or undef to turn off this feature
 513                        'gitweb_tz', # name of cookie where to store selected timezone
 514                        'datetime',  # CSS class used to mark up dates for manipulation
 515                ]},
 516
 517        # Syntax highlighting support. This is based on Daniel Svensson's
 518        # and Sham Chukoury's work in gitweb-xmms2.git.
 519        # It requires the 'highlight' program present in $PATH,
 520        # and therefore is disabled by default.
 521
 522        # To enable system wide have in $GITWEB_CONFIG
 523        # $feature{'highlight'}{'default'} = [1];
 524
 525        'highlight' => {
 526                'sub' => sub { feature_bool('highlight', @_) },
 527                'override' => 0,
 528                'default' => [0]},
 529
 530        # Enable displaying of remote heads in the heads list
 531
 532        # To enable system wide have in $GITWEB_CONFIG
 533        # $feature{'remote_heads'}{'default'} = [1];
 534        # To have project specific config enable override in $GITWEB_CONFIG
 535        # $feature{'remote_heads'}{'override'} = 1;
 536        # and in project config gitweb.remote_heads = 0|1;
 537        'remote_heads' => {
 538                'sub' => sub { feature_bool('remote_heads', @_) },
 539                'override' => 0,
 540                'default' => [0]},
 541);
 542
 543sub gitweb_get_feature {
 544        my ($name) = @_;
 545        return unless exists $feature{$name};
 546        my ($sub, $override, @defaults) = (
 547                $feature{$name}{'sub'},
 548                $feature{$name}{'override'},
 549                @{$feature{$name}{'default'}});
 550        # project specific override is possible only if we have project
 551        our $git_dir; # global variable, declared later
 552        if (!$override || !defined $git_dir) {
 553                return @defaults;
 554        }
 555        if (!defined $sub) {
 556                warn "feature $name is not overridable";
 557                return @defaults;
 558        }
 559        return $sub->(@defaults);
 560}
 561
 562# A wrapper to check if a given feature is enabled.
 563# With this, you can say
 564#
 565#   my $bool_feat = gitweb_check_feature('bool_feat');
 566#   gitweb_check_feature('bool_feat') or somecode;
 567#
 568# instead of
 569#
 570#   my ($bool_feat) = gitweb_get_feature('bool_feat');
 571#   (gitweb_get_feature('bool_feat'))[0] or somecode;
 572#
 573sub gitweb_check_feature {
 574        return (gitweb_get_feature(@_))[0];
 575}
 576
 577
 578sub feature_bool {
 579        my $key = shift;
 580        my ($val) = git_get_project_config($key, '--bool');
 581
 582        if (!defined $val) {
 583                return ($_[0]);
 584        } elsif ($val eq 'true') {
 585                return (1);
 586        } elsif ($val eq 'false') {
 587                return (0);
 588        }
 589}
 590
 591sub feature_snapshot {
 592        my (@fmts) = @_;
 593
 594        my ($val) = git_get_project_config('snapshot');
 595
 596        if ($val) {
 597                @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
 598        }
 599
 600        return @fmts;
 601}
 602
 603sub feature_patches {
 604        my @val = (git_get_project_config('patches', '--int'));
 605
 606        if (@val) {
 607                return @val;
 608        }
 609
 610        return ($_[0]);
 611}
 612
 613sub feature_avatar {
 614        my @val = (git_get_project_config('avatar'));
 615
 616        return @val ? @val : @_;
 617}
 618
 619# checking HEAD file with -e is fragile if the repository was
 620# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
 621# and then pruned.
 622sub check_head_link {
 623        my ($dir) = @_;
 624        my $headfile = "$dir/HEAD";
 625        return ((-e $headfile) ||
 626                (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
 627}
 628
 629sub check_export_ok {
 630        my ($dir) = @_;
 631        return (check_head_link($dir) &&
 632                (!$export_ok || -e "$dir/$export_ok") &&
 633                (!$export_auth_hook || $export_auth_hook->($dir)));
 634}
 635
 636# process alternate names for backward compatibility
 637# filter out unsupported (unknown) snapshot formats
 638sub filter_snapshot_fmts {
 639        my @fmts = @_;
 640
 641        @fmts = map {
 642                exists $known_snapshot_format_aliases{$_} ?
 643                       $known_snapshot_format_aliases{$_} : $_} @fmts;
 644        @fmts = grep {
 645                exists $known_snapshot_formats{$_} &&
 646                !$known_snapshot_formats{$_}{'disabled'}} @fmts;
 647}
 648
 649# If it is set to code reference, it is code that it is to be run once per
 650# request, allowing updating configurations that change with each request,
 651# while running other code in config file only once.
 652#
 653# Otherwise, if it is false then gitweb would process config file only once;
 654# if it is true then gitweb config would be run for each request.
 655our $per_request_config = 1;
 656
 657# read and parse gitweb config file given by its parameter.
 658# returns true on success, false on recoverable error, allowing
 659# to chain this subroutine, using first file that exists.
 660# dies on errors during parsing config file, as it is unrecoverable.
 661sub read_config_file {
 662        my $filename = shift;
 663        return unless defined $filename;
 664        # die if there are errors parsing config file
 665        if (-e $filename) {
 666                do $filename;
 667                die $@ if $@;
 668                return 1;
 669        }
 670        return;
 671}
 672
 673our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
 674sub evaluate_gitweb_config {
 675        our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
 676        our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
 677        our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
 678
 679        # Protect agains duplications of file names, to not read config twice.
 680        # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
 681        # there possibility of duplication of filename there doesn't matter.
 682        $GITWEB_CONFIG = ""        if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
 683        $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
 684
 685        # Common system-wide settings for convenience.
 686        # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
 687        read_config_file($GITWEB_CONFIG_COMMON);
 688
 689        # Use first config file that exists.  This means use the per-instance
 690        # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
 691        read_config_file($GITWEB_CONFIG) and return;
 692        read_config_file($GITWEB_CONFIG_SYSTEM);
 693}
 694
 695# Get loadavg of system, to compare against $maxload.
 696# Currently it requires '/proc/loadavg' present to get loadavg;
 697# if it is not present it returns 0, which means no load checking.
 698sub get_loadavg {
 699        if( -e '/proc/loadavg' ){
 700                open my $fd, '<', '/proc/loadavg'
 701                        or return 0;
 702                my @load = split(/\s+/, scalar <$fd>);
 703                close $fd;
 704
 705                # The first three columns measure CPU and IO utilization of the last one,
 706                # five, and 10 minute periods.  The fourth column shows the number of
 707                # currently running processes and the total number of processes in the m/n
 708                # format.  The last column displays the last process ID used.
 709                return $load[0] || 0;
 710        }
 711        # additional checks for load average should go here for things that don't export
 712        # /proc/loadavg
 713
 714        return 0;
 715}
 716
 717# version of the core git binary
 718our $git_version;
 719sub evaluate_git_version {
 720        our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
 721        $number_of_git_cmds++;
 722}
 723
 724sub check_loadavg {
 725        if (defined $maxload && get_loadavg() > $maxload) {
 726                die_error(503, "The load average on the server is too high");
 727        }
 728}
 729
 730# ======================================================================
 731# input validation and dispatch
 732
 733# input parameters can be collected from a variety of sources (presently, CGI
 734# and PATH_INFO), so we define an %input_params hash that collects them all
 735# together during validation: this allows subsequent uses (e.g. href()) to be
 736# agnostic of the parameter origin
 737
 738our %input_params = ();
 739
 740# input parameters are stored with the long parameter name as key. This will
 741# also be used in the href subroutine to convert parameters to their CGI
 742# equivalent, and since the href() usage is the most frequent one, we store
 743# the name -> CGI key mapping here, instead of the reverse.
 744#
 745# XXX: Warning: If you touch this, check the search form for updating,
 746# too.
 747
 748our @cgi_param_mapping = (
 749        project => "p",
 750        action => "a",
 751        file_name => "f",
 752        file_parent => "fp",
 753        hash => "h",
 754        hash_parent => "hp",
 755        hash_base => "hb",
 756        hash_parent_base => "hpb",
 757        page => "pg",
 758        order => "o",
 759        searchtext => "s",
 760        searchtype => "st",
 761        snapshot_format => "sf",
 762        extra_options => "opt",
 763        search_use_regexp => "sr",
 764        ctag => "by_tag",
 765        diff_style => "ds",
 766        project_filter => "pf",
 767        # this must be last entry (for manipulation from JavaScript)
 768        javascript => "js"
 769);
 770our %cgi_param_mapping = @cgi_param_mapping;
 771
 772# we will also need to know the possible actions, for validation
 773our %actions = (
 774        "blame" => \&git_blame,
 775        "blame_incremental" => \&git_blame_incremental,
 776        "blame_data" => \&git_blame_data,
 777        "blobdiff" => \&git_blobdiff,
 778        "blobdiff_plain" => \&git_blobdiff_plain,
 779        "blob" => \&git_blob,
 780        "blob_plain" => \&git_blob_plain,
 781        "commitdiff" => \&git_commitdiff,
 782        "commitdiff_plain" => \&git_commitdiff_plain,
 783        "commit" => \&git_commit,
 784        "forks" => \&git_forks,
 785        "heads" => \&git_heads,
 786        "history" => \&git_history,
 787        "log" => \&git_log,
 788        "patch" => \&git_patch,
 789        "patches" => \&git_patches,
 790        "remotes" => \&git_remotes,
 791        "rss" => \&git_rss,
 792        "atom" => \&git_atom,
 793        "search" => \&git_search,
 794        "search_help" => \&git_search_help,
 795        "shortlog" => \&git_shortlog,
 796        "summary" => \&git_summary,
 797        "tag" => \&git_tag,
 798        "tags" => \&git_tags,
 799        "tree" => \&git_tree,
 800        "snapshot" => \&git_snapshot,
 801        "object" => \&git_object,
 802        # those below don't need $project
 803        "opml" => \&git_opml,
 804        "project_list" => \&git_project_list,
 805        "project_index" => \&git_project_index,
 806);
 807
 808# finally, we have the hash of allowed extra_options for the commands that
 809# allow them
 810our %allowed_options = (
 811        "--no-merges" => [ qw(rss atom log shortlog history) ],
 812);
 813
 814# fill %input_params with the CGI parameters. All values except for 'opt'
 815# should be single values, but opt can be an array. We should probably
 816# build an array of parameters that can be multi-valued, but since for the time
 817# being it's only this one, we just single it out
 818sub evaluate_query_params {
 819        our $cgi;
 820
 821        while (my ($name, $symbol) = each %cgi_param_mapping) {
 822                if ($symbol eq 'opt') {
 823                        $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ];
 824                } else {
 825                        $input_params{$name} = decode_utf8($cgi->param($symbol));
 826                }
 827        }
 828}
 829
 830# now read PATH_INFO and update the parameter list for missing parameters
 831sub evaluate_path_info {
 832        return if defined $input_params{'project'};
 833        return if !$path_info;
 834        $path_info =~ s,^/+,,;
 835        return if !$path_info;
 836
 837        # find which part of PATH_INFO is project
 838        my $project = $path_info;
 839        $project =~ s,/+$,,;
 840        while ($project && !check_head_link("$projectroot/$project")) {
 841                $project =~ s,/*[^/]*$,,;
 842        }
 843        return unless $project;
 844        $input_params{'project'} = $project;
 845
 846        # do not change any parameters if an action is given using the query string
 847        return if $input_params{'action'};
 848        $path_info =~ s,^\Q$project\E/*,,;
 849
 850        # next, check if we have an action
 851        my $action = $path_info;
 852        $action =~ s,/.*$,,;
 853        if (exists $actions{$action}) {
 854                $path_info =~ s,^$action/*,,;
 855                $input_params{'action'} = $action;
 856        }
 857
 858        # list of actions that want hash_base instead of hash, but can have no
 859        # pathname (f) parameter
 860        my @wants_base = (
 861                'tree',
 862                'history',
 863        );
 864
 865        # we want to catch, among others
 866        # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
 867        my ($parentrefname, $parentpathname, $refname, $pathname) =
 868                ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
 869
 870        # first, analyze the 'current' part
 871        if (defined $pathname) {
 872                # we got "branch:filename" or "branch:dir/"
 873                # we could use git_get_type(branch:pathname), but:
 874                # - it needs $git_dir
 875                # - it does a git() call
 876                # - the convention of terminating directories with a slash
 877                #   makes it superfluous
 878                # - embedding the action in the PATH_INFO would make it even
 879                #   more superfluous
 880                $pathname =~ s,^/+,,;
 881                if (!$pathname || substr($pathname, -1) eq "/") {
 882                        $input_params{'action'} ||= "tree";
 883                        $pathname =~ s,/$,,;
 884                } else {
 885                        # the default action depends on whether we had parent info
 886                        # or not
 887                        if ($parentrefname) {
 888                                $input_params{'action'} ||= "blobdiff_plain";
 889                        } else {
 890                                $input_params{'action'} ||= "blob_plain";
 891                        }
 892                }
 893                $input_params{'hash_base'} ||= $refname;
 894                $input_params{'file_name'} ||= $pathname;
 895        } elsif (defined $refname) {
 896                # we got "branch". In this case we have to choose if we have to
 897                # set hash or hash_base.
 898                #
 899                # Most of the actions without a pathname only want hash to be
 900                # set, except for the ones specified in @wants_base that want
 901                # hash_base instead. It should also be noted that hand-crafted
 902                # links having 'history' as an action and no pathname or hash
 903                # set will fail, but that happens regardless of PATH_INFO.
 904                if (defined $parentrefname) {
 905                        # if there is parent let the default be 'shortlog' action
 906                        # (for http://git.example.com/repo.git/A..B links); if there
 907                        # is no parent, dispatch will detect type of object and set
 908                        # action appropriately if required (if action is not set)
 909                        $input_params{'action'} ||= "shortlog";
 910                }
 911                if ($input_params{'action'} &&
 912                    grep { $_ eq $input_params{'action'} } @wants_base) {
 913                        $input_params{'hash_base'} ||= $refname;
 914                } else {
 915                        $input_params{'hash'} ||= $refname;
 916                }
 917        }
 918
 919        # next, handle the 'parent' part, if present
 920        if (defined $parentrefname) {
 921                # a missing pathspec defaults to the 'current' filename, allowing e.g.
 922                # someproject/blobdiff/oldrev..newrev:/filename
 923                if ($parentpathname) {
 924                        $parentpathname =~ s,^/+,,;
 925                        $parentpathname =~ s,/$,,;
 926                        $input_params{'file_parent'} ||= $parentpathname;
 927                } else {
 928                        $input_params{'file_parent'} ||= $input_params{'file_name'};
 929                }
 930                # we assume that hash_parent_base is wanted if a path was specified,
 931                # or if the action wants hash_base instead of hash
 932                if (defined $input_params{'file_parent'} ||
 933                        grep { $_ eq $input_params{'action'} } @wants_base) {
 934                        $input_params{'hash_parent_base'} ||= $parentrefname;
 935                } else {
 936                        $input_params{'hash_parent'} ||= $parentrefname;
 937                }
 938        }
 939
 940        # for the snapshot action, we allow URLs in the form
 941        # $project/snapshot/$hash.ext
 942        # where .ext determines the snapshot and gets removed from the
 943        # passed $refname to provide the $hash.
 944        #
 945        # To be able to tell that $refname includes the format extension, we
 946        # require the following two conditions to be satisfied:
 947        # - the hash input parameter MUST have been set from the $refname part
 948        #   of the URL (i.e. they must be equal)
 949        # - the snapshot format MUST NOT have been defined already (e.g. from
 950        #   CGI parameter sf)
 951        # It's also useless to try any matching unless $refname has a dot,
 952        # so we check for that too
 953        if (defined $input_params{'action'} &&
 954                $input_params{'action'} eq 'snapshot' &&
 955                defined $refname && index($refname, '.') != -1 &&
 956                $refname eq $input_params{'hash'} &&
 957                !defined $input_params{'snapshot_format'}) {
 958                # We loop over the known snapshot formats, checking for
 959                # extensions. Allowed extensions are both the defined suffix
 960                # (which includes the initial dot already) and the snapshot
 961                # format key itself, with a prepended dot
 962                while (my ($fmt, $opt) = each %known_snapshot_formats) {
 963                        my $hash = $refname;
 964                        unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
 965                                next;
 966                        }
 967                        my $sfx = $1;
 968                        # a valid suffix was found, so set the snapshot format
 969                        # and reset the hash parameter
 970                        $input_params{'snapshot_format'} = $fmt;
 971                        $input_params{'hash'} = $hash;
 972                        # we also set the format suffix to the one requested
 973                        # in the URL: this way a request for e.g. .tgz returns
 974                        # a .tgz instead of a .tar.gz
 975                        $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
 976                        last;
 977                }
 978        }
 979}
 980
 981our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
 982     $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
 983     $searchtext, $search_regexp, $project_filter);
 984sub evaluate_and_validate_params {
 985        our $action = $input_params{'action'};
 986        if (defined $action) {
 987                if (!validate_action($action)) {
 988                        die_error(400, "Invalid action parameter");
 989                }
 990        }
 991
 992        # parameters which are pathnames
 993        our $project = $input_params{'project'};
 994        if (defined $project) {
 995                if (!validate_project($project)) {
 996                        undef $project;
 997                        die_error(404, "No such project");
 998                }
 999        }
1000
1001        our $project_filter = $input_params{'project_filter'};
1002        if (defined $project_filter) {
1003                if (!validate_pathname($project_filter)) {
1004                        die_error(404, "Invalid project_filter parameter");
1005                }
1006        }
1007
1008        our $file_name = $input_params{'file_name'};
1009        if (defined $file_name) {
1010                if (!validate_pathname($file_name)) {
1011                        die_error(400, "Invalid file parameter");
1012                }
1013        }
1014
1015        our $file_parent = $input_params{'file_parent'};
1016        if (defined $file_parent) {
1017                if (!validate_pathname($file_parent)) {
1018                        die_error(400, "Invalid file parent parameter");
1019                }
1020        }
1021
1022        # parameters which are refnames
1023        our $hash = $input_params{'hash'};
1024        if (defined $hash) {
1025                if (!validate_refname($hash)) {
1026                        die_error(400, "Invalid hash parameter");
1027                }
1028        }
1029
1030        our $hash_parent = $input_params{'hash_parent'};
1031        if (defined $hash_parent) {
1032                if (!validate_refname($hash_parent)) {
1033                        die_error(400, "Invalid hash parent parameter");
1034                }
1035        }
1036
1037        our $hash_base = $input_params{'hash_base'};
1038        if (defined $hash_base) {
1039                if (!validate_refname($hash_base)) {
1040                        die_error(400, "Invalid hash base parameter");
1041                }
1042        }
1043
1044        our @extra_options = @{$input_params{'extra_options'}};
1045        # @extra_options is always defined, since it can only be (currently) set from
1046        # CGI, and $cgi->param() returns the empty array in array context if the param
1047        # is not set
1048        foreach my $opt (@extra_options) {
1049                if (not exists $allowed_options{$opt}) {
1050                        die_error(400, "Invalid option parameter");
1051                }
1052                if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1053                        die_error(400, "Invalid option parameter for this action");
1054                }
1055        }
1056
1057        our $hash_parent_base = $input_params{'hash_parent_base'};
1058        if (defined $hash_parent_base) {
1059                if (!validate_refname($hash_parent_base)) {
1060                        die_error(400, "Invalid hash parent base parameter");
1061                }
1062        }
1063
1064        # other parameters
1065        our $page = $input_params{'page'};
1066        if (defined $page) {
1067                if ($page =~ m/[^0-9]/) {
1068                        die_error(400, "Invalid page parameter");
1069                }
1070        }
1071
1072        our $searchtype = $input_params{'searchtype'};
1073        if (defined $searchtype) {
1074                if ($searchtype =~ m/[^a-z]/) {
1075                        die_error(400, "Invalid searchtype parameter");
1076                }
1077        }
1078
1079        our $search_use_regexp = $input_params{'search_use_regexp'};
1080
1081        our $searchtext = $input_params{'searchtext'};
1082        our $search_regexp;
1083        if (defined $searchtext) {
1084                if (length($searchtext) < 2) {
1085                        die_error(403, "At least two characters are required for search parameter");
1086                }
1087                if ($search_use_regexp) {
1088                        $search_regexp = $searchtext;
1089                        if (!eval { qr/$search_regexp/; 1; }) {
1090                                (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1091                                die_error(400, "Invalid search regexp '$search_regexp'",
1092                                          esc_html($error));
1093                        }
1094                } else {
1095                        $search_regexp = quotemeta $searchtext;
1096                }
1097        }
1098}
1099
1100# path to the current git repository
1101our $git_dir;
1102sub evaluate_git_dir {
1103        our $git_dir = "$projectroot/$project" if $project;
1104}
1105
1106our (@snapshot_fmts, $git_avatar);
1107sub configure_gitweb_features {
1108        # list of supported snapshot formats
1109        our @snapshot_fmts = gitweb_get_feature('snapshot');
1110        @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1111
1112        # check that the avatar feature is set to a known provider name,
1113        # and for each provider check if the dependencies are satisfied.
1114        # if the provider name is invalid or the dependencies are not met,
1115        # reset $git_avatar to the empty string.
1116        our ($git_avatar) = gitweb_get_feature('avatar');
1117        if ($git_avatar eq 'gravatar') {
1118                $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1119        } elsif ($git_avatar eq 'picon') {
1120                # no dependencies
1121        } else {
1122                $git_avatar = '';
1123        }
1124}
1125
1126# custom error handler: 'die <message>' is Internal Server Error
1127sub handle_errors_html {
1128        my $msg = shift; # it is already HTML escaped
1129
1130        # to avoid infinite loop where error occurs in die_error,
1131        # change handler to default handler, disabling handle_errors_html
1132        set_message("Error occured when inside die_error:\n$msg");
1133
1134        # you cannot jump out of die_error when called as error handler;
1135        # the subroutine set via CGI::Carp::set_message is called _after_
1136        # HTTP headers are already written, so it cannot write them itself
1137        die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1138}
1139set_message(\&handle_errors_html);
1140
1141# dispatch
1142sub dispatch {
1143        if (!defined $action) {
1144                if (defined $hash) {
1145                        $action = git_get_type($hash);
1146                        $action or die_error(404, "Object does not exist");
1147                } elsif (defined $hash_base && defined $file_name) {
1148                        $action = git_get_type("$hash_base:$file_name");
1149                        $action or die_error(404, "File or directory does not exist");
1150                } elsif (defined $project) {
1151                        $action = 'summary';
1152                } else {
1153                        $action = 'project_list';
1154                }
1155        }
1156        if (!defined($actions{$action})) {
1157                die_error(400, "Unknown action");
1158        }
1159        if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1160            !$project) {
1161                die_error(400, "Project needed");
1162        }
1163        $actions{$action}->();
1164}
1165
1166sub reset_timer {
1167        our $t0 = [ gettimeofday() ]
1168                if defined $t0;
1169        our $number_of_git_cmds = 0;
1170}
1171
1172our $first_request = 1;
1173sub run_request {
1174        reset_timer();
1175
1176        evaluate_uri();
1177        if ($first_request) {
1178                evaluate_gitweb_config();
1179                evaluate_git_version();
1180        }
1181        if ($per_request_config) {
1182                if (ref($per_request_config) eq 'CODE') {
1183                        $per_request_config->();
1184                } elsif (!$first_request) {
1185                        evaluate_gitweb_config();
1186                }
1187        }
1188        check_loadavg();
1189
1190        # $projectroot and $projects_list might be set in gitweb config file
1191        $projects_list ||= $projectroot;
1192
1193        evaluate_query_params();
1194        evaluate_path_info();
1195        evaluate_and_validate_params();
1196        evaluate_git_dir();
1197
1198        configure_gitweb_features();
1199
1200        dispatch();
1201}
1202
1203our $is_last_request = sub { 1 };
1204our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1205our $CGI = 'CGI';
1206our $cgi;
1207sub configure_as_fcgi {
1208        require CGI::Fast;
1209        our $CGI = 'CGI::Fast';
1210
1211        my $request_number = 0;
1212        # let each child service 100 requests
1213        our $is_last_request = sub { ++$request_number > 100 };
1214}
1215sub evaluate_argv {
1216        my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1217        configure_as_fcgi()
1218                if $script_name =~ /\.fcgi$/;
1219
1220        return unless (@ARGV);
1221
1222        require Getopt::Long;
1223        Getopt::Long::GetOptions(
1224                'fastcgi|fcgi|f' => \&configure_as_fcgi,
1225                'nproc|n=i' => sub {
1226                        my ($arg, $val) = @_;
1227                        return unless eval { require FCGI::ProcManager; 1; };
1228                        my $proc_manager = FCGI::ProcManager->new({
1229                                n_processes => $val,
1230                        });
1231                        our $pre_listen_hook    = sub { $proc_manager->pm_manage()        };
1232                        our $pre_dispatch_hook  = sub { $proc_manager->pm_pre_dispatch()  };
1233                        our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1234                },
1235        );
1236}
1237
1238sub run {
1239        evaluate_argv();
1240
1241        $first_request = 1;
1242        $pre_listen_hook->()
1243                if $pre_listen_hook;
1244
1245 REQUEST:
1246        while ($cgi = $CGI->new()) {
1247                $pre_dispatch_hook->()
1248                        if $pre_dispatch_hook;
1249
1250                run_request();
1251
1252                $post_dispatch_hook->()
1253                        if $post_dispatch_hook;
1254                $first_request = 0;
1255
1256                last REQUEST if ($is_last_request->());
1257        }
1258
1259 DONE_GITWEB:
1260        1;
1261}
1262
1263run();
1264
1265if (defined caller) {
1266        # wrapped in a subroutine processing requests,
1267        # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1268        return;
1269} else {
1270        # pure CGI script, serving single request
1271        exit;
1272}
1273
1274## ======================================================================
1275## action links
1276
1277# possible values of extra options
1278# -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)
1279# -replay => 1      - start from a current view (replay with modifications)
1280# -path_info => 0|1 - don't use/use path_info URL (if possible)
1281# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1282sub href {
1283        my %params = @_;
1284        # default is to use -absolute url() i.e. $my_uri
1285        my $href = $params{-full} ? $my_url : $my_uri;
1286
1287        # implicit -replay, must be first of implicit params
1288        $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1289
1290        $params{'project'} = $project unless exists $params{'project'};
1291
1292        if ($params{-replay}) {
1293                while (my ($name, $symbol) = each %cgi_param_mapping) {
1294                        if (!exists $params{$name}) {
1295                                $params{$name} = $input_params{$name};
1296                        }
1297                }
1298        }
1299
1300        my $use_pathinfo = gitweb_check_feature('pathinfo');
1301        if (defined $params{'project'} &&
1302            (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1303                # try to put as many parameters as possible in PATH_INFO:
1304                #   - project name
1305                #   - action
1306                #   - hash_parent or hash_parent_base:/file_parent
1307                #   - hash or hash_base:/filename
1308                #   - the snapshot_format as an appropriate suffix
1309
1310                # When the script is the root DirectoryIndex for the domain,
1311                # $href here would be something like http://gitweb.example.com/
1312                # Thus, we strip any trailing / from $href, to spare us double
1313                # slashes in the final URL
1314                $href =~ s,/$,,;
1315
1316                # Then add the project name, if present
1317                $href .= "/".esc_path_info($params{'project'});
1318                delete $params{'project'};
1319
1320                # since we destructively absorb parameters, we keep this
1321                # boolean that remembers if we're handling a snapshot
1322                my $is_snapshot = $params{'action'} eq 'snapshot';
1323
1324                # Summary just uses the project path URL, any other action is
1325                # added to the URL
1326                if (defined $params{'action'}) {
1327                        $href .= "/".esc_path_info($params{'action'})
1328                                unless $params{'action'} eq 'summary';
1329                        delete $params{'action'};
1330                }
1331
1332                # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1333                # stripping nonexistent or useless pieces
1334                $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1335                        || $params{'hash_parent'} || $params{'hash'});
1336                if (defined $params{'hash_base'}) {
1337                        if (defined $params{'hash_parent_base'}) {
1338                                $href .= esc_path_info($params{'hash_parent_base'});
1339                                # skip the file_parent if it's the same as the file_name
1340                                if (defined $params{'file_parent'}) {
1341                                        if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1342                                                delete $params{'file_parent'};
1343                                        } elsif ($params{'file_parent'} !~ /\.\./) {
1344                                                $href .= ":/".esc_path_info($params{'file_parent'});
1345                                                delete $params{'file_parent'};
1346                                        }
1347                                }
1348                                $href .= "..";
1349                                delete $params{'hash_parent'};
1350                                delete $params{'hash_parent_base'};
1351                        } elsif (defined $params{'hash_parent'}) {
1352                                $href .= esc_path_info($params{'hash_parent'}). "..";
1353                                delete $params{'hash_parent'};
1354                        }
1355
1356                        $href .= esc_path_info($params{'hash_base'});
1357                        if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1358                                $href .= ":/".esc_path_info($params{'file_name'});
1359                                delete $params{'file_name'};
1360                        }
1361                        delete $params{'hash'};
1362                        delete $params{'hash_base'};
1363                } elsif (defined $params{'hash'}) {
1364                        $href .= esc_path_info($params{'hash'});
1365                        delete $params{'hash'};
1366                }
1367
1368                # If the action was a snapshot, we can absorb the
1369                # snapshot_format parameter too
1370                if ($is_snapshot) {
1371                        my $fmt = $params{'snapshot_format'};
1372                        # snapshot_format should always be defined when href()
1373                        # is called, but just in case some code forgets, we
1374                        # fall back to the default
1375                        $fmt ||= $snapshot_fmts[0];
1376                        $href .= $known_snapshot_formats{$fmt}{'suffix'};
1377                        delete $params{'snapshot_format'};
1378                }
1379        }
1380
1381        # now encode the parameters explicitly
1382        my @result = ();
1383        for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1384                my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1385                if (defined $params{$name}) {
1386                        if (ref($params{$name}) eq "ARRAY") {
1387                                foreach my $par (@{$params{$name}}) {
1388                                        push @result, $symbol . "=" . esc_param($par);
1389                                }
1390                        } else {
1391                                push @result, $symbol . "=" . esc_param($params{$name});
1392                        }
1393                }
1394        }
1395        $href .= "?" . join(';', @result) if scalar @result;
1396
1397        # final transformation: trailing spaces must be escaped (URI-encoded)
1398        $href =~ s/(\s+)$/CGI::escape($1)/e;
1399
1400        if ($params{-anchor}) {
1401                $href .= "#".esc_param($params{-anchor});
1402        }
1403
1404        return $href;
1405}
1406
1407
1408## ======================================================================
1409## validation, quoting/unquoting and escaping
1410
1411sub validate_action {
1412        my $input = shift || return undef;
1413        return undef unless exists $actions{$input};
1414        return $input;
1415}
1416
1417sub validate_project {
1418        my $input = shift || return undef;
1419        if (!validate_pathname($input) ||
1420                !(-d "$projectroot/$input") ||
1421                !check_export_ok("$projectroot/$input") ||
1422                ($strict_export && !project_in_list($input))) {
1423                return undef;
1424        } else {
1425                return $input;
1426        }
1427}
1428
1429sub validate_pathname {
1430        my $input = shift || return undef;
1431
1432        # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1433        # at the beginning, at the end, and between slashes.
1434        # also this catches doubled slashes
1435        if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1436                return undef;
1437        }
1438        # no null characters
1439        if ($input =~ m!\0!) {
1440                return undef;
1441        }
1442        return $input;
1443}
1444
1445sub validate_refname {
1446        my $input = shift || return undef;
1447
1448        # textual hashes are O.K.
1449        if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1450                return $input;
1451        }
1452        # it must be correct pathname
1453        $input = validate_pathname($input)
1454                or return undef;
1455        # restrictions on ref name according to git-check-ref-format
1456        if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1457                return undef;
1458        }
1459        return $input;
1460}
1461
1462# decode sequences of octets in utf8 into Perl's internal form,
1463# which is utf-8 with utf8 flag set if needed.  gitweb writes out
1464# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1465sub to_utf8 {
1466        my $str = shift;
1467        return undef unless defined $str;
1468
1469        if (utf8::is_utf8($str) || utf8::decode($str)) {
1470                return $str;
1471        } else {
1472                return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1473        }
1474}
1475
1476# quote unsafe chars, but keep the slash, even when it's not
1477# correct, but quoted slashes look too horrible in bookmarks
1478sub esc_param {
1479        my $str = shift;
1480        return undef unless defined $str;
1481        $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1482        $str =~ s/ /\+/g;
1483        return $str;
1484}
1485
1486# the quoting rules for path_info fragment are slightly different
1487sub esc_path_info {
1488        my $str = shift;
1489        return undef unless defined $str;
1490
1491        # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1492        $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1493
1494        return $str;
1495}
1496
1497# quote unsafe chars in whole URL, so some characters cannot be quoted
1498sub esc_url {
1499        my $str = shift;
1500        return undef unless defined $str;
1501        $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1502        $str =~ s/ /\+/g;
1503        return $str;
1504}
1505
1506# quote unsafe characters in HTML attributes
1507sub esc_attr {
1508
1509        # for XHTML conformance escaping '"' to '&quot;' is not enough
1510        return esc_html(@_);
1511}
1512
1513# replace invalid utf8 character with SUBSTITUTION sequence
1514sub esc_html {
1515        my $str = shift;
1516        my %opts = @_;
1517
1518        return undef unless defined $str;
1519
1520        $str = to_utf8($str);
1521        $str = $cgi->escapeHTML($str);
1522        if ($opts{'-nbsp'}) {
1523                $str =~ s/ /&nbsp;/g;
1524        }
1525        $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1526        return $str;
1527}
1528
1529# quote control characters and escape filename to HTML
1530sub esc_path {
1531        my $str = shift;
1532        my %opts = @_;
1533
1534        return undef unless defined $str;
1535
1536        $str = to_utf8($str);
1537        $str = $cgi->escapeHTML($str);
1538        if ($opts{'-nbsp'}) {
1539                $str =~ s/ /&nbsp;/g;
1540        }
1541        $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1542        return $str;
1543}
1544
1545# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
1546sub sanitize {
1547        my $str = shift;
1548
1549        return undef unless defined $str;
1550
1551        $str = to_utf8($str);
1552        $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg;
1553        return $str;
1554}
1555
1556# Make control characters "printable", using character escape codes (CEC)
1557sub quot_cec {
1558        my $cntrl = shift;
1559        my %opts = @_;
1560        my %es = ( # character escape codes, aka escape sequences
1561                "\t" => '\t',   # tab            (HT)
1562                "\n" => '\n',   # line feed      (LF)
1563                "\r" => '\r',   # carrige return (CR)
1564                "\f" => '\f',   # form feed      (FF)
1565                "\b" => '\b',   # backspace      (BS)
1566                "\a" => '\a',   # alarm (bell)   (BEL)
1567                "\e" => '\e',   # escape         (ESC)
1568                "\013" => '\v', # vertical tab   (VT)
1569                "\000" => '\0', # nul character  (NUL)
1570        );
1571        my $chr = ( (exists $es{$cntrl})
1572                    ? $es{$cntrl}
1573                    : sprintf('\%2x', ord($cntrl)) );
1574        if ($opts{-nohtml}) {
1575                return $chr;
1576        } else {
1577                return "<span class=\"cntrl\">$chr</span>";
1578        }
1579}
1580
1581# Alternatively use unicode control pictures codepoints,
1582# Unicode "printable representation" (PR)
1583sub quot_upr {
1584        my $cntrl = shift;
1585        my %opts = @_;
1586
1587        my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1588        if ($opts{-nohtml}) {
1589                return $chr;
1590        } else {
1591                return "<span class=\"cntrl\">$chr</span>";
1592        }
1593}
1594
1595# git may return quoted and escaped filenames
1596sub unquote {
1597        my $str = shift;
1598
1599        sub unq {
1600                my $seq = shift;
1601                my %es = ( # character escape codes, aka escape sequences
1602                        't' => "\t",   # tab            (HT, TAB)
1603                        'n' => "\n",   # newline        (NL)
1604                        'r' => "\r",   # return         (CR)
1605                        'f' => "\f",   # form feed      (FF)
1606                        'b' => "\b",   # backspace      (BS)
1607                        'a' => "\a",   # alarm (bell)   (BEL)
1608                        'e' => "\e",   # escape         (ESC)
1609                        'v' => "\013", # vertical tab   (VT)
1610                );
1611
1612                if ($seq =~ m/^[0-7]{1,3}$/) {
1613                        # octal char sequence
1614                        return chr(oct($seq));
1615                } elsif (exists $es{$seq}) {
1616                        # C escape sequence, aka character escape code
1617                        return $es{$seq};
1618                }
1619                # quoted ordinary character
1620                return $seq;
1621        }
1622
1623        if ($str =~ m/^"(.*)"$/) {
1624                # needs unquoting
1625                $str = $1;
1626                $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1627        }
1628        return $str;
1629}
1630
1631# escape tabs (convert tabs to spaces)
1632sub untabify {
1633        my $line = shift;
1634
1635        while ((my $pos = index($line, "\t")) != -1) {
1636                if (my $count = (8 - ($pos % 8))) {
1637                        my $spaces = ' ' x $count;
1638                        $line =~ s/\t/$spaces/;
1639                }
1640        }
1641
1642        return $line;
1643}
1644
1645sub project_in_list {
1646        my $project = shift;
1647        my @list = git_get_projects_list();
1648        return @list && scalar(grep { $_->{'path'} eq $project } @list);
1649}
1650
1651## ----------------------------------------------------------------------
1652## HTML aware string manipulation
1653
1654# Try to chop given string on a word boundary between position
1655# $len and $len+$add_len. If there is no word boundary there,
1656# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1657# (marking chopped part) would be longer than given string.
1658sub chop_str {
1659        my $str = shift;
1660        my $len = shift;
1661        my $add_len = shift || 10;
1662        my $where = shift || 'right'; # 'left' | 'center' | 'right'
1663
1664        # Make sure perl knows it is utf8 encoded so we don't
1665        # cut in the middle of a utf8 multibyte char.
1666        $str = to_utf8($str);
1667
1668        # allow only $len chars, but don't cut a word if it would fit in $add_len
1669        # if it doesn't fit, cut it if it's still longer than the dots we would add
1670        # remove chopped character entities entirely
1671
1672        # when chopping in the middle, distribute $len into left and right part
1673        # return early if chopping wouldn't make string shorter
1674        if ($where eq 'center') {
1675                return $str if ($len + 5 >= length($str)); # filler is length 5
1676                $len = int($len/2);
1677        } else {
1678                return $str if ($len + 4 >= length($str)); # filler is length 4
1679        }
1680
1681        # regexps: ending and beginning with word part up to $add_len
1682        my $endre = qr/.{$len}\w{0,$add_len}/;
1683        my $begre = qr/\w{0,$add_len}.{$len}/;
1684
1685        if ($where eq 'left') {
1686                $str =~ m/^(.*?)($begre)$/;
1687                my ($lead, $body) = ($1, $2);
1688                if (length($lead) > 4) {
1689                        $lead = " ...";
1690                }
1691                return "$lead$body";
1692
1693        } elsif ($where eq 'center') {
1694                $str =~ m/^($endre)(.*)$/;
1695                my ($left, $str)  = ($1, $2);
1696                $str =~ m/^(.*?)($begre)$/;
1697                my ($mid, $right) = ($1, $2);
1698                if (length($mid) > 5) {
1699                        $mid = " ... ";
1700                }
1701                return "$left$mid$right";
1702
1703        } else {
1704                $str =~ m/^($endre)(.*)$/;
1705                my $body = $1;
1706                my $tail = $2;
1707                if (length($tail) > 4) {
1708                        $tail = "... ";
1709                }
1710                return "$body$tail";
1711        }
1712}
1713
1714# takes the same arguments as chop_str, but also wraps a <span> around the
1715# result with a title attribute if it does get chopped. Additionally, the
1716# string is HTML-escaped.
1717sub chop_and_escape_str {
1718        my ($str) = @_;
1719
1720        my $chopped = chop_str(@_);
1721        $str = to_utf8($str);
1722        if ($chopped eq $str) {
1723                return esc_html($chopped);
1724        } else {
1725                $str =~ s/[[:cntrl:]]/?/g;
1726                return $cgi->span({-title=>$str}, esc_html($chopped));
1727        }
1728}
1729
1730# Highlight selected fragments of string, using given CSS class,
1731# and escape HTML.  It is assumed that fragments do not overlap.
1732# Regions are passed as list of pairs (array references).
1733#
1734# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1735# '<span class="mark">foo</span>bar'
1736sub esc_html_hl_regions {
1737        my ($str, $css_class, @sel) = @_;
1738        return esc_html($str) unless @sel;
1739
1740        my $out = '';
1741        my $pos = 0;
1742
1743        for my $s (@sel) {
1744                $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
1745                        if ($s->[0] - $pos > 0);
1746                $out .= $cgi->span({-class => $css_class},
1747                                   esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
1748
1749                $pos = $s->[1];
1750        }
1751        $out .= esc_html(substr($str, $pos))
1752                if ($pos < length($str));
1753
1754        return $out;
1755}
1756
1757# return positions of beginning and end of each match
1758sub matchpos_list {
1759        my ($str, $regexp) = @_;
1760        return unless (defined $str && defined $regexp);
1761
1762        my @matches;
1763        while ($str =~ /$regexp/g) {
1764                push @matches, [$-[0], $+[0]];
1765        }
1766        return @matches;
1767}
1768
1769# highlight match (if any), and escape HTML
1770sub esc_html_match_hl {
1771        my ($str, $regexp) = @_;
1772        return esc_html($str) unless defined $regexp;
1773
1774        my @matches = matchpos_list($str, $regexp);
1775        return esc_html($str) unless @matches;
1776
1777        return esc_html_hl_regions($str, 'match', @matches);
1778}
1779
1780
1781# highlight match (if any) of shortened string, and escape HTML
1782sub esc_html_match_hl_chopped {
1783        my ($str, $chopped, $regexp) = @_;
1784        return esc_html_match_hl($str, $regexp) unless defined $chopped;
1785
1786        my @matches = matchpos_list($str, $regexp);
1787        return esc_html($chopped) unless @matches;
1788
1789        # filter matches so that we mark chopped string
1790        my $tail = "... "; # see chop_str
1791        unless ($chopped =~ s/\Q$tail\E$//) {
1792                $tail = '';
1793        }
1794        my $chop_len = length($chopped);
1795        my $tail_len = length($tail);
1796        my @filtered;
1797
1798        for my $m (@matches) {
1799                if ($m->[0] > $chop_len) {
1800                        push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
1801                        last;
1802                } elsif ($m->[1] > $chop_len) {
1803                        push @filtered, [ $m->[0], $chop_len + $tail_len ];
1804                        last;
1805                }
1806                push @filtered, $m;
1807        }
1808
1809        return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
1810}
1811
1812## ----------------------------------------------------------------------
1813## functions returning short strings
1814
1815# CSS class for given age value (in seconds)
1816sub age_class {
1817        my $age = shift;
1818
1819        if (!defined $age) {
1820                return "noage";
1821        } elsif ($age < 60*60*2) {
1822                return "age0";
1823        } elsif ($age < 60*60*24*2) {
1824                return "age1";
1825        } else {
1826                return "age2";
1827        }
1828}
1829
1830# convert age in seconds to "nn units ago" string
1831sub age_string {
1832        my $age = shift;
1833        my $age_str;
1834
1835        if ($age > 60*60*24*365*2) {
1836                $age_str = (int $age/60/60/24/365);
1837                $age_str .= " years ago";
1838        } elsif ($age > 60*60*24*(365/12)*2) {
1839                $age_str = int $age/60/60/24/(365/12);
1840                $age_str .= " months ago";
1841        } elsif ($age > 60*60*24*7*2) {
1842                $age_str = int $age/60/60/24/7;
1843                $age_str .= " weeks ago";
1844        } elsif ($age > 60*60*24*2) {
1845                $age_str = int $age/60/60/24;
1846                $age_str .= " days ago";
1847        } elsif ($age > 60*60*2) {
1848                $age_str = int $age/60/60;
1849                $age_str .= " hours ago";
1850        } elsif ($age > 60*2) {
1851                $age_str = int $age/60;
1852                $age_str .= " min ago";
1853        } elsif ($age > 2) {
1854                $age_str = int $age;
1855                $age_str .= " sec ago";
1856        } else {
1857                $age_str .= " right now";
1858        }
1859        return $age_str;
1860}
1861
1862use constant {
1863        S_IFINVALID => 0030000,
1864        S_IFGITLINK => 0160000,
1865};
1866
1867# submodule/subproject, a commit object reference
1868sub S_ISGITLINK {
1869        my $mode = shift;
1870
1871        return (($mode & S_IFMT) == S_IFGITLINK)
1872}
1873
1874# convert file mode in octal to symbolic file mode string
1875sub mode_str {
1876        my $mode = oct shift;
1877
1878        if (S_ISGITLINK($mode)) {
1879                return 'm---------';
1880        } elsif (S_ISDIR($mode & S_IFMT)) {
1881                return 'drwxr-xr-x';
1882        } elsif (S_ISLNK($mode)) {
1883                return 'lrwxrwxrwx';
1884        } elsif (S_ISREG($mode)) {
1885                # git cares only about the executable bit
1886                if ($mode & S_IXUSR) {
1887                        return '-rwxr-xr-x';
1888                } else {
1889                        return '-rw-r--r--';
1890                };
1891        } else {
1892                return '----------';
1893        }
1894}
1895
1896# convert file mode in octal to file type string
1897sub file_type {
1898        my $mode = shift;
1899
1900        if ($mode !~ m/^[0-7]+$/) {
1901                return $mode;
1902        } else {
1903                $mode = oct $mode;
1904        }
1905
1906        if (S_ISGITLINK($mode)) {
1907                return "submodule";
1908        } elsif (S_ISDIR($mode & S_IFMT)) {
1909                return "directory";
1910        } elsif (S_ISLNK($mode)) {
1911                return "symlink";
1912        } elsif (S_ISREG($mode)) {
1913                return "file";
1914        } else {
1915                return "unknown";
1916        }
1917}
1918
1919# convert file mode in octal to file type description string
1920sub file_type_long {
1921        my $mode = shift;
1922
1923        if ($mode !~ m/^[0-7]+$/) {
1924                return $mode;
1925        } else {
1926                $mode = oct $mode;
1927        }
1928
1929        if (S_ISGITLINK($mode)) {
1930                return "submodule";
1931        } elsif (S_ISDIR($mode & S_IFMT)) {
1932                return "directory";
1933        } elsif (S_ISLNK($mode)) {
1934                return "symlink";
1935        } elsif (S_ISREG($mode)) {
1936                if ($mode & S_IXUSR) {
1937                        return "executable";
1938                } else {
1939                        return "file";
1940                };
1941        } else {
1942                return "unknown";
1943        }
1944}
1945
1946
1947## ----------------------------------------------------------------------
1948## functions returning short HTML fragments, or transforming HTML fragments
1949## which don't belong to other sections
1950
1951# format line of commit message.
1952sub format_log_line_html {
1953        my $line = shift;
1954
1955        $line = esc_html($line, -nbsp=>1);
1956        $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1957                $cgi->a({-href => href(action=>"object", hash=>$1),
1958                                        -class => "text"}, $1);
1959        }eg;
1960
1961        return $line;
1962}
1963
1964# format marker of refs pointing to given object
1965
1966# the destination action is chosen based on object type and current context:
1967# - for annotated tags, we choose the tag view unless it's the current view
1968#   already, in which case we go to shortlog view
1969# - for other refs, we keep the current view if we're in history, shortlog or
1970#   log view, and select shortlog otherwise
1971sub format_ref_marker {
1972        my ($refs, $id) = @_;
1973        my $markers = '';
1974
1975        if (defined $refs->{$id}) {
1976                foreach my $ref (@{$refs->{$id}}) {
1977                        # this code exploits the fact that non-lightweight tags are the
1978                        # only indirect objects, and that they are the only objects for which
1979                        # we want to use tag instead of shortlog as action
1980                        my ($type, $name) = qw();
1981                        my $indirect = ($ref =~ s/\^\{\}$//);
1982                        # e.g. tags/v2.6.11 or heads/next
1983                        if ($ref =~ m!^(.*?)s?/(.*)$!) {
1984                                $type = $1;
1985                                $name = $2;
1986                        } else {
1987                                $type = "ref";
1988                                $name = $ref;
1989                        }
1990
1991                        my $class = $type;
1992                        $class .= " indirect" if $indirect;
1993
1994                        my $dest_action = "shortlog";
1995
1996                        if ($indirect) {
1997                                $dest_action = "tag" unless $action eq "tag";
1998                        } elsif ($action =~ /^(history|(short)?log)$/) {
1999                                $dest_action = $action;
2000                        }
2001
2002                        my $dest = "";
2003                        $dest .= "refs/" unless $ref =~ m!^refs/!;
2004                        $dest .= $ref;
2005
2006                        my $link = $cgi->a({
2007                                -href => href(
2008                                        action=>$dest_action,
2009                                        hash=>$dest
2010                                )}, $name);
2011
2012                        $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2013                                $link . "</span>";
2014                }
2015        }
2016
2017        if ($markers) {
2018                return ' <span class="refs">'. $markers . '</span>';
2019        } else {
2020                return "";
2021        }
2022}
2023
2024# format, perhaps shortened and with markers, title line
2025sub format_subject_html {
2026        my ($long, $short, $href, $extra) = @_;
2027        $extra = '' unless defined($extra);
2028
2029        if (length($short) < length($long)) {
2030                $long =~ s/[[:cntrl:]]/?/g;
2031                return $cgi->a({-href => $href, -class => "list subject",
2032                                -title => to_utf8($long)},
2033                       esc_html($short)) . $extra;
2034        } else {
2035                return $cgi->a({-href => $href, -class => "list subject"},
2036                       esc_html($long)) . $extra;
2037        }
2038}
2039
2040# Rather than recomputing the url for an email multiple times, we cache it
2041# after the first hit. This gives a visible benefit in views where the avatar
2042# for the same email is used repeatedly (e.g. shortlog).
2043# The cache is shared by all avatar engines (currently gravatar only), which
2044# are free to use it as preferred. Since only one avatar engine is used for any
2045# given page, there's no risk for cache conflicts.
2046our %avatar_cache = ();
2047
2048# Compute the picon url for a given email, by using the picon search service over at
2049# http://www.cs.indiana.edu/picons/search.html
2050sub picon_url {
2051        my $email = lc shift;
2052        if (!$avatar_cache{$email}) {
2053                my ($user, $domain) = split('@', $email);
2054                $avatar_cache{$email} =
2055                        "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2056                        "$domain/$user/" .
2057                        "users+domains+unknown/up/single";
2058        }
2059        return $avatar_cache{$email};
2060}
2061
2062# Compute the gravatar url for a given email, if it's not in the cache already.
2063# Gravatar stores only the part of the URL before the size, since that's the
2064# one computationally more expensive. This also allows reuse of the cache for
2065# different sizes (for this particular engine).
2066sub gravatar_url {
2067        my $email = lc shift;
2068        my $size = shift;
2069        $avatar_cache{$email} ||=
2070                "http://www.gravatar.com/avatar/" .
2071                        Digest::MD5::md5_hex($email) . "?s=";
2072        return $avatar_cache{$email} . $size;
2073}
2074
2075# Insert an avatar for the given $email at the given $size if the feature
2076# is enabled.
2077sub git_get_avatar {
2078        my ($email, %opts) = @_;
2079        my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
2080        my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
2081        $opts{-size} ||= 'default';
2082        my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2083        my $url = "";
2084        if ($git_avatar eq 'gravatar') {
2085                $url = gravatar_url($email, $size);
2086        } elsif ($git_avatar eq 'picon') {
2087                $url = picon_url($email);
2088        }
2089        # Other providers can be added by extending the if chain, defining $url
2090        # as needed. If no variant puts something in $url, we assume avatars
2091        # are completely disabled/unavailable.
2092        if ($url) {
2093                return $pre_white .
2094                       "<img width=\"$size\" " .
2095                            "class=\"avatar\" " .
2096                            "src=\"".esc_url($url)."\" " .
2097                            "alt=\"\" " .
2098                       "/>" . $post_white;
2099        } else {
2100                return "";
2101        }
2102}
2103
2104sub format_search_author {
2105        my ($author, $searchtype, $displaytext) = @_;
2106        my $have_search = gitweb_check_feature('search');
2107
2108        if ($have_search) {
2109                my $performed = "";
2110                if ($searchtype eq 'author') {
2111                        $performed = "authored";
2112                } elsif ($searchtype eq 'committer') {
2113                        $performed = "committed";
2114                }
2115
2116                return $cgi->a({-href => href(action=>"search", hash=>$hash,
2117                                searchtext=>$author,
2118                                searchtype=>$searchtype), class=>"list",
2119                                title=>"Search for commits $performed by $author"},
2120                                $displaytext);
2121
2122        } else {
2123                return $displaytext;
2124        }
2125}
2126
2127# format the author name of the given commit with the given tag
2128# the author name is chopped and escaped according to the other
2129# optional parameters (see chop_str).
2130sub format_author_html {
2131        my $tag = shift;
2132        my $co = shift;
2133        my $author = chop_and_escape_str($co->{'author_name'}, @_);
2134        return "<$tag class=\"author\">" .
2135               format_search_author($co->{'author_name'}, "author",
2136                       git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2137                       $author) .
2138               "</$tag>";
2139}
2140
2141# format git diff header line, i.e. "diff --(git|combined|cc) ..."
2142sub format_git_diff_header_line {
2143        my $line = shift;
2144        my $diffinfo = shift;
2145        my ($from, $to) = @_;
2146
2147        if ($diffinfo->{'nparents'}) {
2148                # combined diff
2149                $line =~ s!^(diff (.*?) )"?.*$!$1!;
2150                if ($to->{'href'}) {
2151                        $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2152                                         esc_path($to->{'file'}));
2153                } else { # file was deleted (no href)
2154                        $line .= esc_path($to->{'file'});
2155                }
2156        } else {
2157                # "ordinary" diff
2158                $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2159                if ($from->{'href'}) {
2160                        $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2161                                         'a/' . esc_path($from->{'file'}));
2162                } else { # file was added (no href)
2163                        $line .= 'a/' . esc_path($from->{'file'});
2164                }
2165                $line .= ' ';
2166                if ($to->{'href'}) {
2167                        $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2168                                         'b/' . esc_path($to->{'file'}));
2169                } else { # file was deleted
2170                        $line .= 'b/' . esc_path($to->{'file'});
2171                }
2172        }
2173
2174        return "<div class=\"diff header\">$line</div>\n";
2175}
2176
2177# format extended diff header line, before patch itself
2178sub format_extended_diff_header_line {
2179        my $line = shift;
2180        my $diffinfo = shift;
2181        my ($from, $to) = @_;
2182
2183        # match <path>
2184        if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2185                $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2186                                       esc_path($from->{'file'}));
2187        }
2188        if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2189                $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2190                                 esc_path($to->{'file'}));
2191        }
2192        # match single <mode>
2193        if ($line =~ m/\s(\d{6})$/) {
2194                $line .= '<span class="info"> (' .
2195                         file_type_long($1) .
2196                         ')</span>';
2197        }
2198        # match <hash>
2199        if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2200                # can match only for combined diff
2201                $line = 'index ';
2202                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2203                        if ($from->{'href'}[$i]) {
2204                                $line .= $cgi->a({-href=>$from->{'href'}[$i],
2205                                                  -class=>"hash"},
2206                                                 substr($diffinfo->{'from_id'}[$i],0,7));
2207                        } else {
2208                                $line .= '0' x 7;
2209                        }
2210                        # separator
2211                        $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2212                }
2213                $line .= '..';
2214                if ($to->{'href'}) {
2215                        $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2216                                         substr($diffinfo->{'to_id'},0,7));
2217                } else {
2218                        $line .= '0' x 7;
2219                }
2220
2221        } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2222                # can match only for ordinary diff
2223                my ($from_link, $to_link);
2224                if ($from->{'href'}) {
2225                        $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2226                                             substr($diffinfo->{'from_id'},0,7));
2227                } else {
2228                        $from_link = '0' x 7;
2229                }
2230                if ($to->{'href'}) {
2231                        $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2232                                           substr($diffinfo->{'to_id'},0,7));
2233                } else {
2234                        $to_link = '0' x 7;
2235                }
2236                my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2237                $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2238        }
2239
2240        return $line . "<br/>\n";
2241}
2242
2243# format from-file/to-file diff header
2244sub format_diff_from_to_header {
2245        my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2246        my $line;
2247        my $result = '';
2248
2249        $line = $from_line;
2250        #assert($line =~ m/^---/) if DEBUG;
2251        # no extra formatting for "^--- /dev/null"
2252        if (! $diffinfo->{'nparents'}) {
2253                # ordinary (single parent) diff
2254                if ($line =~ m!^--- "?a/!) {
2255                        if ($from->{'href'}) {
2256                                $line = '--- a/' .
2257                                        $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2258                                                esc_path($from->{'file'}));
2259                        } else {
2260                                $line = '--- a/' .
2261                                        esc_path($from->{'file'});
2262                        }
2263                }
2264                $result .= qq!<div class="diff from_file">$line</div>\n!;
2265
2266        } else {
2267                # combined diff (merge commit)
2268                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2269                        if ($from->{'href'}[$i]) {
2270                                $line = '--- ' .
2271                                        $cgi->a({-href=>href(action=>"blobdiff",
2272                                                             hash_parent=>$diffinfo->{'from_id'}[$i],
2273                                                             hash_parent_base=>$parents[$i],
2274                                                             file_parent=>$from->{'file'}[$i],
2275                                                             hash=>$diffinfo->{'to_id'},
2276                                                             hash_base=>$hash,
2277                                                             file_name=>$to->{'file'}),
2278                                                 -class=>"path",
2279                                                 -title=>"diff" . ($i+1)},
2280                                                $i+1) .
2281                                        '/' .
2282                                        $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2283                                                esc_path($from->{'file'}[$i]));
2284                        } else {
2285                                $line = '--- /dev/null';
2286                        }
2287                        $result .= qq!<div class="diff from_file">$line</div>\n!;
2288                }
2289        }
2290
2291        $line = $to_line;
2292        #assert($line =~ m/^\+\+\+/) if DEBUG;
2293        # no extra formatting for "^+++ /dev/null"
2294        if ($line =~ m!^\+\+\+ "?b/!) {
2295                if ($to->{'href'}) {
2296                        $line = '+++ b/' .
2297                                $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2298                                        esc_path($to->{'file'}));
2299                } else {
2300                        $line = '+++ b/' .
2301                                esc_path($to->{'file'});
2302                }
2303        }
2304        $result .= qq!<div class="diff to_file">$line</div>\n!;
2305
2306        return $result;
2307}
2308
2309# create note for patch simplified by combined diff
2310sub format_diff_cc_simplified {
2311        my ($diffinfo, @parents) = @_;
2312        my $result = '';
2313
2314        $result .= "<div class=\"diff header\">" .
2315                   "diff --cc ";
2316        if (!is_deleted($diffinfo)) {
2317                $result .= $cgi->a({-href => href(action=>"blob",
2318                                                  hash_base=>$hash,
2319                                                  hash=>$diffinfo->{'to_id'},
2320                                                  file_name=>$diffinfo->{'to_file'}),
2321                                    -class => "path"},
2322                                   esc_path($diffinfo->{'to_file'}));
2323        } else {
2324                $result .= esc_path($diffinfo->{'to_file'});
2325        }
2326        $result .= "</div>\n" . # class="diff header"
2327                   "<div class=\"diff nodifferences\">" .
2328                   "Simple merge" .
2329                   "</div>\n"; # class="diff nodifferences"
2330
2331        return $result;
2332}
2333
2334sub diff_line_class {
2335        my ($line, $from, $to) = @_;
2336
2337        # ordinary diff
2338        my $num_sign = 1;
2339        # combined diff
2340        if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2341                $num_sign = scalar @{$from->{'href'}};
2342        }
2343
2344        my @diff_line_classifier = (
2345                { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2346                { regexp => qr/^\\/,               class => "incomplete"  },
2347                { regexp => qr/^ {$num_sign}/,     class => "ctx" },
2348                # classifier for context must come before classifier add/rem,
2349                # or we would have to use more complicated regexp, for example
2350                # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2351                { regexp => qr/^[+ ]{$num_sign}/,   class => "add" },
2352                { regexp => qr/^[- ]{$num_sign}/,   class => "rem" },
2353        );
2354        for my $clsfy (@diff_line_classifier) {
2355                return $clsfy->{'class'}
2356                        if ($line =~ $clsfy->{'regexp'});
2357        }
2358
2359        # fallback
2360        return "";
2361}
2362
2363# assumes that $from and $to are defined and correctly filled,
2364# and that $line holds a line of chunk header for unified diff
2365sub format_unidiff_chunk_header {
2366        my ($line, $from, $to) = @_;
2367
2368        my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2369                $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2370
2371        $from_lines = 0 unless defined $from_lines;
2372        $to_lines   = 0 unless defined $to_lines;
2373
2374        if ($from->{'href'}) {
2375                $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2376                                     -class=>"list"}, $from_text);
2377        }
2378        if ($to->{'href'}) {
2379                $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2380                                     -class=>"list"}, $to_text);
2381        }
2382        $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2383                "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2384        return $line;
2385}
2386
2387# assumes that $from and $to are defined and correctly filled,
2388# and that $line holds a line of chunk header for combined diff
2389sub format_cc_diff_chunk_header {
2390        my ($line, $from, $to) = @_;
2391
2392        my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2393        my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2394
2395        @from_text = split(' ', $ranges);
2396        for (my $i = 0; $i < @from_text; ++$i) {
2397                ($from_start[$i], $from_nlines[$i]) =
2398                        (split(',', substr($from_text[$i], 1)), 0);
2399        }
2400
2401        $to_text   = pop @from_text;
2402        $to_start  = pop @from_start;
2403        $to_nlines = pop @from_nlines;
2404
2405        $line = "<span class=\"chunk_info\">$prefix ";
2406        for (my $i = 0; $i < @from_text; ++$i) {
2407                if ($from->{'href'}[$i]) {
2408                        $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2409                                          -class=>"list"}, $from_text[$i]);
2410                } else {
2411                        $line .= $from_text[$i];
2412                }
2413                $line .= " ";
2414        }
2415        if ($to->{'href'}) {
2416                $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2417                                  -class=>"list"}, $to_text);
2418        } else {
2419                $line .= $to_text;
2420        }
2421        $line .= " $prefix</span>" .
2422                 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2423        return $line;
2424}
2425
2426# process patch (diff) line (not to be used for diff headers),
2427# returning class and HTML-formatted (but not wrapped) line
2428sub process_diff_line {
2429        my $line = shift;
2430        my ($from, $to) = @_;
2431
2432        my $diff_class = diff_line_class($line, $from, $to);
2433
2434        chomp $line;
2435        $line = untabify($line);
2436
2437        if ($from && $to && $line =~ m/^\@{2} /) {
2438                $line = format_unidiff_chunk_header($line, $from, $to);
2439                return $diff_class, $line;
2440
2441        } elsif ($from && $to && $line =~ m/^\@{3}/) {
2442                $line = format_cc_diff_chunk_header($line, $from, $to);
2443                return $diff_class, $line;
2444
2445        }
2446        return $diff_class, esc_html($line, -nbsp=>1);
2447}
2448
2449# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2450# linked.  Pass the hash of the tree/commit to snapshot.
2451sub format_snapshot_links {
2452        my ($hash) = @_;
2453        my $num_fmts = @snapshot_fmts;
2454        if ($num_fmts > 1) {
2455                # A parenthesized list of links bearing format names.
2456                # e.g. "snapshot (_tar.gz_ _zip_)"
2457                return "snapshot (" . join(' ', map
2458                        $cgi->a({
2459                                -href => href(
2460                                        action=>"snapshot",
2461                                        hash=>$hash,
2462                                        snapshot_format=>$_
2463                                )
2464                        }, $known_snapshot_formats{$_}{'display'})
2465                , @snapshot_fmts) . ")";
2466        } elsif ($num_fmts == 1) {
2467                # A single "snapshot" link whose tooltip bears the format name.
2468                # i.e. "_snapshot_"
2469                my ($fmt) = @snapshot_fmts;
2470                return
2471                        $cgi->a({
2472                                -href => href(
2473                                        action=>"snapshot",
2474                                        hash=>$hash,
2475                                        snapshot_format=>$fmt
2476                                ),
2477                                -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2478                        }, "snapshot");
2479        } else { # $num_fmts == 0
2480                return undef;
2481        }
2482}
2483
2484## ......................................................................
2485## functions returning values to be passed, perhaps after some
2486## transformation, to other functions; e.g. returning arguments to href()
2487
2488# returns hash to be passed to href to generate gitweb URL
2489# in -title key it returns description of link
2490sub get_feed_info {
2491        my $format = shift || 'Atom';
2492        my %res = (action => lc($format));
2493
2494        # feed links are possible only for project views
2495        return unless (defined $project);
2496        # some views should link to OPML, or to generic project feed,
2497        # or don't have specific feed yet (so they should use generic)
2498        return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2499
2500        my $branch;
2501        # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2502        # from tag links; this also makes possible to detect branch links
2503        if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2504            (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
2505                $branch = $1;
2506        }
2507        # find log type for feed description (title)
2508        my $type = 'log';
2509        if (defined $file_name) {
2510                $type  = "history of $file_name";
2511                $type .= "/" if ($action eq 'tree');
2512                $type .= " on '$branch'" if (defined $branch);
2513        } else {
2514                $type = "log of $branch" if (defined $branch);
2515        }
2516
2517        $res{-title} = $type;
2518        $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2519        $res{'file_name'} = $file_name;
2520
2521        return %res;
2522}
2523
2524## ----------------------------------------------------------------------
2525## git utility subroutines, invoking git commands
2526
2527# returns path to the core git executable and the --git-dir parameter as list
2528sub git_cmd {
2529        $number_of_git_cmds++;
2530        return $GIT, '--git-dir='.$git_dir;
2531}
2532
2533# quote the given arguments for passing them to the shell
2534# quote_command("command", "arg 1", "arg with ' and ! characters")
2535# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2536# Try to avoid using this function wherever possible.
2537sub quote_command {
2538        return join(' ',
2539                map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2540}
2541
2542# get HEAD ref of given project as hash
2543sub git_get_head_hash {
2544        return git_get_full_hash(shift, 'HEAD');
2545}
2546
2547sub git_get_full_hash {
2548        return git_get_hash(@_);
2549}
2550
2551sub git_get_short_hash {
2552        return git_get_hash(@_, '--short=7');
2553}
2554
2555sub git_get_hash {
2556        my ($project, $hash, @options) = @_;
2557        my $o_git_dir = $git_dir;
2558        my $retval = undef;
2559        $git_dir = "$projectroot/$project";
2560        if (open my $fd, '-|', git_cmd(), 'rev-parse',
2561            '--verify', '-q', @options, $hash) {
2562                $retval = <$fd>;
2563                chomp $retval if defined $retval;
2564                close $fd;
2565        }
2566        if (defined $o_git_dir) {
2567                $git_dir = $o_git_dir;
2568        }
2569        return $retval;
2570}
2571
2572# get type of given object
2573sub git_get_type {
2574        my $hash = shift;
2575
2576        open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2577        my $type = <$fd>;
2578        close $fd or return;
2579        chomp $type;
2580        return $type;
2581}
2582
2583# repository configuration
2584our $config_file = '';
2585our %config;
2586
2587# store multiple values for single key as anonymous array reference
2588# single values stored directly in the hash, not as [ <value> ]
2589sub hash_set_multi {
2590        my ($hash, $key, $value) = @_;
2591
2592        if (!exists $hash->{$key}) {
2593                $hash->{$key} = $value;
2594        } elsif (!ref $hash->{$key}) {
2595                $hash->{$key} = [ $hash->{$key}, $value ];
2596        } else {
2597                push @{$hash->{$key}}, $value;
2598        }
2599}
2600
2601# return hash of git project configuration
2602# optionally limited to some section, e.g. 'gitweb'
2603sub git_parse_project_config {
2604        my $section_regexp = shift;
2605        my %config;
2606
2607        local $/ = "\0";
2608
2609        open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2610                or return;
2611
2612        while (my $keyval = <$fh>) {
2613                chomp $keyval;
2614                my ($key, $value) = split(/\n/, $keyval, 2);
2615
2616                hash_set_multi(\%config, $key, $value)
2617                        if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2618        }
2619        close $fh;
2620
2621        return %config;
2622}
2623
2624# convert config value to boolean: 'true' or 'false'
2625# no value, number > 0, 'true' and 'yes' values are true
2626# rest of values are treated as false (never as error)
2627sub config_to_bool {
2628        my $val = shift;
2629
2630        return 1 if !defined $val;             # section.key
2631
2632        # strip leading and trailing whitespace
2633        $val =~ s/^\s+//;
2634        $val =~ s/\s+$//;
2635
2636        return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2637                ($val =~ /^(?:true|yes)$/i));  # section.key = true
2638}
2639
2640# convert config value to simple decimal number
2641# an optional value suffix of 'k', 'm', or 'g' will cause the value
2642# to be multiplied by 1024, 1048576, or 1073741824
2643sub config_to_int {
2644        my $val = shift;
2645
2646        # strip leading and trailing whitespace
2647        $val =~ s/^\s+//;
2648        $val =~ s/\s+$//;
2649
2650        if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2651                $unit = lc($unit);
2652                # unknown unit is treated as 1
2653                return $num * ($unit eq 'g' ? 1073741824 :
2654                               $unit eq 'm' ?    1048576 :
2655                               $unit eq 'k' ?       1024 : 1);
2656        }
2657        return $val;
2658}
2659
2660# convert config value to array reference, if needed
2661sub config_to_multi {
2662        my $val = shift;
2663
2664        return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2665}
2666
2667sub git_get_project_config {
2668        my ($key, $type) = @_;
2669
2670        return unless defined $git_dir;
2671
2672        # key sanity check
2673        return unless ($key);
2674        # only subsection, if exists, is case sensitive,
2675        # and not lowercased by 'git config -z -l'
2676        if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2677                $key = join(".", lc($hi), $mi, lc($lo));
2678        } else {
2679                $key = lc($key);
2680        }
2681        $key =~ s/^gitweb\.//;
2682        return if ($key =~ m/\W/);
2683
2684        # type sanity check
2685        if (defined $type) {
2686                $type =~ s/^--//;
2687                $type = undef
2688                        unless ($type eq 'bool' || $type eq 'int');
2689        }
2690
2691        # get config
2692        if (!defined $config_file ||
2693            $config_file ne "$git_dir/config") {
2694                %config = git_parse_project_config('gitweb');
2695                $config_file = "$git_dir/config";
2696        }
2697
2698        # check if config variable (key) exists
2699        return unless exists $config{"gitweb.$key"};
2700
2701        # ensure given type
2702        if (!defined $type) {
2703                return $config{"gitweb.$key"};
2704        } elsif ($type eq 'bool') {
2705                # backward compatibility: 'git config --bool' returns true/false
2706                return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2707        } elsif ($type eq 'int') {
2708                return config_to_int($config{"gitweb.$key"});
2709        }
2710        return $config{"gitweb.$key"};
2711}
2712
2713# get hash of given path at given ref
2714sub git_get_hash_by_path {
2715        my $base = shift;
2716        my $path = shift || return undef;
2717        my $type = shift;
2718
2719        $path =~ s,/+$,,;
2720
2721        open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2722                or die_error(500, "Open git-ls-tree failed");
2723        my $line = <$fd>;
2724        close $fd or return undef;
2725
2726        if (!defined $line) {
2727                # there is no tree or hash given by $path at $base
2728                return undef;
2729        }
2730
2731        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2732        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2733        if (defined $type && $type ne $2) {
2734                # type doesn't match
2735                return undef;
2736        }
2737        return $3;
2738}
2739
2740# get path of entry with given hash at given tree-ish (ref)
2741# used to get 'from' filename for combined diff (merge commit) for renames
2742sub git_get_path_by_hash {
2743        my $base = shift || return;
2744        my $hash = shift || return;
2745
2746        local $/ = "\0";
2747
2748        open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2749                or return undef;
2750        while (my $line = <$fd>) {
2751                chomp $line;
2752
2753                #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2754                #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2755                if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2756                        close $fd;
2757                        return $1;
2758                }
2759        }
2760        close $fd;
2761        return undef;
2762}
2763
2764## ......................................................................
2765## git utility functions, directly accessing git repository
2766
2767# get the value of config variable either from file named as the variable
2768# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2769# configuration variable in the repository config file.
2770sub git_get_file_or_project_config {
2771        my ($path, $name) = @_;
2772
2773        $git_dir = "$projectroot/$path";
2774        open my $fd, '<', "$git_dir/$name"
2775                or return git_get_project_config($name);
2776        my $conf = <$fd>;
2777        close $fd;
2778        if (defined $conf) {
2779                chomp $conf;
2780        }
2781        return $conf;
2782}
2783
2784sub git_get_project_description {
2785        my $path = shift;
2786        return git_get_file_or_project_config($path, 'description');
2787}
2788
2789sub git_get_project_category {
2790        my $path = shift;
2791        return git_get_file_or_project_config($path, 'category');
2792}
2793
2794
2795# supported formats:
2796# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2797#   - if its contents is a number, use it as tag weight,
2798#   - otherwise add a tag with weight 1
2799# * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2800#   the same value multiple times increases tag weight
2801# * `gitweb.ctag' multi-valued repo config variable
2802sub git_get_project_ctags {
2803        my $project = shift;
2804        my $ctags = {};
2805
2806        $git_dir = "$projectroot/$project";
2807        if (opendir my $dh, "$git_dir/ctags") {
2808                my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2809                foreach my $tagfile (@files) {
2810                        open my $ct, '<', $tagfile
2811                                or next;
2812                        my $val = <$ct>;
2813                        chomp $val if $val;
2814                        close $ct;
2815
2816                        (my $ctag = $tagfile) =~ s#.*/##;
2817                        if ($val =~ /^\d+$/) {
2818                                $ctags->{$ctag} = $val;
2819                        } else {
2820                                $ctags->{$ctag} = 1;
2821                        }
2822                }
2823                closedir $dh;
2824
2825        } elsif (open my $fh, '<', "$git_dir/ctags") {
2826                while (my $line = <$fh>) {
2827                        chomp $line;
2828                        $ctags->{$line}++ if $line;
2829                }
2830                close $fh;
2831
2832        } else {
2833                my $taglist = config_to_multi(git_get_project_config('ctag'));
2834                foreach my $tag (@$taglist) {
2835                        $ctags->{$tag}++;
2836                }
2837        }
2838
2839        return $ctags;
2840}
2841
2842# return hash, where keys are content tags ('ctags'),
2843# and values are sum of weights of given tag in every project
2844sub git_gather_all_ctags {
2845        my $projects = shift;
2846        my $ctags = {};
2847
2848        foreach my $p (@$projects) {
2849                foreach my $ct (keys %{$p->{'ctags'}}) {
2850                        $ctags->{$ct} += $p->{'ctags'}->{$ct};
2851                }
2852        }
2853
2854        return $ctags;
2855}
2856
2857sub git_populate_project_tagcloud {
2858        my $ctags = shift;
2859
2860        # First, merge different-cased tags; tags vote on casing
2861        my %ctags_lc;
2862        foreach (keys %$ctags) {
2863                $ctags_lc{lc $_}->{count} += $ctags->{$_};
2864                if (not $ctags_lc{lc $_}->{topcount}
2865                    or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2866                        $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2867                        $ctags_lc{lc $_}->{topname} = $_;
2868                }
2869        }
2870
2871        my $cloud;
2872        my $matched = $input_params{'ctag'};
2873        if (eval { require HTML::TagCloud; 1; }) {
2874                $cloud = HTML::TagCloud->new;
2875                foreach my $ctag (sort keys %ctags_lc) {
2876                        # Pad the title with spaces so that the cloud looks
2877                        # less crammed.
2878                        my $title = esc_html($ctags_lc{$ctag}->{topname});
2879                        $title =~ s/ /&nbsp;/g;
2880                        $title =~ s/^/&nbsp;/g;
2881                        $title =~ s/$/&nbsp;/g;
2882                        if (defined $matched && $matched eq $ctag) {
2883                                $title = qq(<span class="match">$title</span>);
2884                        }
2885                        $cloud->add($title, href(project=>undef, ctag=>$ctag),
2886                                    $ctags_lc{$ctag}->{count});
2887                }
2888        } else {
2889                $cloud = {};
2890                foreach my $ctag (keys %ctags_lc) {
2891                        my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2892                        if (defined $matched && $matched eq $ctag) {
2893                                $title = qq(<span class="match">$title</span>);
2894                        }
2895                        $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2896                        $cloud->{$ctag}{ctag} =
2897                                $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2898                }
2899        }
2900        return $cloud;
2901}
2902
2903sub git_show_project_tagcloud {
2904        my ($cloud, $count) = @_;
2905        if (ref $cloud eq 'HTML::TagCloud') {
2906                return $cloud->html_and_css($count);
2907        } else {
2908                my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2909                return
2910                        '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2911                        join (', ', map {
2912                                $cloud->{$_}->{'ctag'}
2913                        } splice(@tags, 0, $count)) .
2914                        '</div>';
2915        }
2916}
2917
2918sub git_get_project_url_list {
2919        my $path = shift;
2920
2921        $git_dir = "$projectroot/$path";
2922        open my $fd, '<', "$git_dir/cloneurl"
2923                or return wantarray ?
2924                @{ config_to_multi(git_get_project_config('url')) } :
2925                   config_to_multi(git_get_project_config('url'));
2926        my @git_project_url_list = map { chomp; $_ } <$fd>;
2927        close $fd;
2928
2929        return wantarray ? @git_project_url_list : \@git_project_url_list;
2930}
2931
2932sub git_get_projects_list {
2933        my $filter = shift || '';
2934        my $paranoid = shift;
2935        my @list;
2936
2937        if (-d $projects_list) {
2938                # search in directory
2939                my $dir = $projects_list;
2940                # remove the trailing "/"
2941                $dir =~ s!/+$!!;
2942                my $pfxlen = length("$dir");
2943                my $pfxdepth = ($dir =~ tr!/!!);
2944                # when filtering, search only given subdirectory
2945                if ($filter && !$paranoid) {
2946                        $dir .= "/$filter";
2947                        $dir =~ s!/+$!!;
2948                }
2949
2950                File::Find::find({
2951                        follow_fast => 1, # follow symbolic links
2952                        follow_skip => 2, # ignore duplicates
2953                        dangling_symlinks => 0, # ignore dangling symlinks, silently
2954                        wanted => sub {
2955                                # global variables
2956                                our $project_maxdepth;
2957                                our $projectroot;
2958                                # skip project-list toplevel, if we get it.
2959                                return if (m!^[/.]$!);
2960                                # only directories can be git repositories
2961                                return unless (-d $_);
2962                                # don't traverse too deep (Find is super slow on os x)
2963                                # $project_maxdepth excludes depth of $projectroot
2964                                if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2965                                        $File::Find::prune = 1;
2966                                        return;
2967                                }
2968
2969                                my $path = substr($File::Find::name, $pfxlen + 1);
2970                                # paranoidly only filter here
2971                                if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
2972                                        next;
2973                                }
2974                                # we check related file in $projectroot
2975                                if (check_export_ok("$projectroot/$path")) {
2976                                        push @list, { path => $path };
2977                                        $File::Find::prune = 1;
2978                                }
2979                        },
2980                }, "$dir");
2981
2982        } elsif (-f $projects_list) {
2983                # read from file(url-encoded):
2984                # 'git%2Fgit.git Linus+Torvalds'
2985                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2986                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2987                open my $fd, '<', $projects_list or return;
2988        PROJECT:
2989                while (my $line = <$fd>) {
2990                        chomp $line;
2991                        my ($path, $owner) = split ' ', $line;
2992                        $path = unescape($path);
2993                        $owner = unescape($owner);
2994                        if (!defined $path) {
2995                                next;
2996                        }
2997                        # if $filter is rpovided, check if $path begins with $filter
2998                        if ($filter && $path !~ m!^\Q$filter\E/!) {
2999                                next;
3000                        }
3001                        if (check_export_ok("$projectroot/$path")) {
3002                                my $pr = {
3003                                        path => $path
3004                                };
3005                                if ($owner) {
3006                                        $pr->{'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                unless ($omit_age_column) {
5471                        print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5472                            (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n";
5473                }
5474                print"<td class=\"link\">" .
5475                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
5476                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5477                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5478                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5479                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5480                      "</td>\n" .
5481                      "</tr>\n";
5482        }
5483}
5484
5485sub git_project_list_body {
5486        # actually uses global variable $project
5487        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5488        my @projects = @$projlist;
5489
5490        my $check_forks = gitweb_check_feature('forks');
5491        my $show_ctags  = gitweb_check_feature('ctags');
5492        my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5493        $check_forks = undef
5494                if ($tagfilter || $search_regexp);
5495
5496        # filtering out forks before filling info allows to do less work
5497        @projects = filter_forks_from_projects_list(\@projects)
5498                if ($check_forks);
5499        # search_projects_list pre-fills required info
5500        @projects = search_projects_list(\@projects,
5501                                         'search_regexp' => $search_regexp,
5502                                         'tagfilter'  => $tagfilter)
5503                if ($tagfilter || $search_regexp);
5504        # fill the rest
5505        my @all_fields = $omit_age_column ? ('descr', 'descr_long', 'owner', 'ctags', 'category') : ();
5506        @projects = fill_project_list_info(\@projects, @all_fields);
5507
5508        $order ||= $default_projects_order;
5509        $from = 0 unless defined $from;
5510        $to = $#projects if (!defined $to || $#projects < $to);
5511
5512        # short circuit
5513        if ($from > $to) {
5514                print "<center>\n".
5515                      "<b>No such projects found</b><br />\n".
5516                      "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5517                      "</center>\n<br />\n";
5518                return;
5519        }
5520
5521        @projects = sort_projects_list(\@projects, $order);
5522
5523        if ($show_ctags) {
5524                my $ctags = git_gather_all_ctags(\@projects);
5525                my $cloud = git_populate_project_tagcloud($ctags);
5526                print git_show_project_tagcloud($cloud, 64);
5527        }
5528
5529        print "<table class=\"project_list\">\n";
5530        unless ($no_header) {
5531                print "<tr>\n";
5532                if ($check_forks) {
5533                        print "<th></th>\n";
5534                }
5535                print_sort_th('project', $order, 'Project');
5536                print_sort_th('descr', $order, 'Description');
5537                print_sort_th('owner', $order, 'Owner');
5538                print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
5539                print "<th></th>\n" . # for links
5540                      "</tr>\n";
5541        }
5542
5543        if ($projects_list_group_categories) {
5544                # only display categories with projects in the $from-$to window
5545                @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5546                my %categories = build_projlist_by_category(\@projects, $from, $to);
5547                foreach my $cat (sort keys %categories) {
5548                        unless ($cat eq "") {
5549                                print "<tr>\n";
5550                                if ($check_forks) {
5551                                        print "<td></td>\n";
5552                                }
5553                                print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5554                                print "</tr>\n";
5555                        }
5556
5557                        git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
5558                }
5559        } else {
5560                git_project_list_rows(\@projects, $from, $to, $check_forks);
5561        }
5562
5563        if (defined $extra) {
5564                print "<tr>\n";
5565                if ($check_forks) {
5566                        print "<td></td>\n";
5567                }
5568                print "<td colspan=\"5\">$extra</td>\n" .
5569                      "</tr>\n";
5570        }
5571        print "</table>\n";
5572}
5573
5574sub git_log_body {
5575        # uses global variable $project
5576        my ($commitlist, $from, $to, $refs, $extra) = @_;
5577
5578        $from = 0 unless defined $from;
5579        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5580
5581        for (my $i = 0; $i <= $to; $i++) {
5582                my %co = %{$commitlist->[$i]};
5583                next if !%co;
5584                my $commit = $co{'id'};
5585                my $ref = format_ref_marker($refs, $commit);
5586                git_print_header_div('commit',
5587                               "<span class=\"age\">$co{'age_string'}</span>" .
5588                               esc_html($co{'title'}) . $ref,
5589                               $commit);
5590                print "<div class=\"title_text\">\n" .
5591                      "<div class=\"log_link\">\n" .
5592                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5593                      " | " .
5594                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5595                      " | " .
5596                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5597                      "<br/>\n" .
5598                      "</div>\n";
5599                      git_print_authorship(\%co, -tag => 'span');
5600                      print "<br/>\n</div>\n";
5601
5602                print "<div class=\"log_body\">\n";
5603                git_print_log($co{'comment'}, -final_empty_line=> 1);
5604                print "</div>\n";
5605        }
5606        if ($extra) {
5607                print "<div class=\"page_nav\">\n";
5608                print "$extra\n";
5609                print "</div>\n";
5610        }
5611}
5612
5613sub git_shortlog_body {
5614        # uses global variable $project
5615        my ($commitlist, $from, $to, $refs, $extra) = @_;
5616
5617        $from = 0 unless defined $from;
5618        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5619
5620        print "<table class=\"shortlog\">\n";
5621        my $alternate = 1;
5622        for (my $i = $from; $i <= $to; $i++) {
5623                my %co = %{$commitlist->[$i]};
5624                my $commit = $co{'id'};
5625                my $ref = format_ref_marker($refs, $commit);
5626                if ($alternate) {
5627                        print "<tr class=\"dark\">\n";
5628                } else {
5629                        print "<tr class=\"light\">\n";
5630                }
5631                $alternate ^= 1;
5632                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5633                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5634                      format_author_html('td', \%co, 10) . "<td>";
5635                print format_subject_html($co{'title'}, $co{'title_short'},
5636                                          href(action=>"commit", hash=>$commit), $ref);
5637                print "</td>\n" .
5638                      "<td class=\"link\">" .
5639                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5640                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5641                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5642                my $snapshot_links = format_snapshot_links($commit);
5643                if (defined $snapshot_links) {
5644                        print " | " . $snapshot_links;
5645                }
5646                print "</td>\n" .
5647                      "</tr>\n";
5648        }
5649        if (defined $extra) {
5650                print "<tr>\n" .
5651                      "<td colspan=\"4\">$extra</td>\n" .
5652                      "</tr>\n";
5653        }
5654        print "</table>\n";
5655}
5656
5657sub git_history_body {
5658        # Warning: assumes constant type (blob or tree) during history
5659        my ($commitlist, $from, $to, $refs, $extra,
5660            $file_name, $file_hash, $ftype) = @_;
5661
5662        $from = 0 unless defined $from;
5663        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5664
5665        print "<table class=\"history\">\n";
5666        my $alternate = 1;
5667        for (my $i = $from; $i <= $to; $i++) {
5668                my %co = %{$commitlist->[$i]};
5669                if (!%co) {
5670                        next;
5671                }
5672                my $commit = $co{'id'};
5673
5674                my $ref = format_ref_marker($refs, $commit);
5675
5676                if ($alternate) {
5677                        print "<tr class=\"dark\">\n";
5678                } else {
5679                        print "<tr class=\"light\">\n";
5680                }
5681                $alternate ^= 1;
5682                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5683        # shortlog:   format_author_html('td', \%co, 10)
5684                      format_author_html('td', \%co, 15, 3) . "<td>";
5685                # originally git_history used chop_str($co{'title'}, 50)
5686                print format_subject_html($co{'title'}, $co{'title_short'},
5687                                          href(action=>"commit", hash=>$commit), $ref);
5688                print "</td>\n" .
5689                      "<td class=\"link\">" .
5690                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5691                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5692
5693                if ($ftype eq 'blob') {
5694                        my $blob_current = $file_hash;
5695                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
5696                        if (defined $blob_current && defined $blob_parent &&
5697                                        $blob_current ne $blob_parent) {
5698                                print " | " .
5699                                        $cgi->a({-href => href(action=>"blobdiff",
5700                                                               hash=>$blob_current, hash_parent=>$blob_parent,
5701                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
5702                                                               file_name=>$file_name)},
5703                                                "diff to current");
5704                        }
5705                }
5706                print "</td>\n" .
5707                      "</tr>\n";
5708        }
5709        if (defined $extra) {
5710                print "<tr>\n" .
5711                      "<td colspan=\"4\">$extra</td>\n" .
5712                      "</tr>\n";
5713        }
5714        print "</table>\n";
5715}
5716
5717sub git_tags_body {
5718        # uses global variable $project
5719        my ($taglist, $from, $to, $extra) = @_;
5720        $from = 0 unless defined $from;
5721        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5722
5723        print "<table class=\"tags\">\n";
5724        my $alternate = 1;
5725        for (my $i = $from; $i <= $to; $i++) {
5726                my $entry = $taglist->[$i];
5727                my %tag = %$entry;
5728                my $comment = $tag{'subject'};
5729                my $comment_short;
5730                if (defined $comment) {
5731                        $comment_short = chop_str($comment, 30, 5);
5732                }
5733                if ($alternate) {
5734                        print "<tr class=\"dark\">\n";
5735                } else {
5736                        print "<tr class=\"light\">\n";
5737                }
5738                $alternate ^= 1;
5739                if (defined $tag{'age'}) {
5740                        print "<td><i>$tag{'age'}</i></td>\n";
5741                } else {
5742                        print "<td></td>\n";
5743                }
5744                print "<td>" .
5745                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5746                               -class => "list name"}, esc_html($tag{'name'})) .
5747                      "</td>\n" .
5748                      "<td>";
5749                if (defined $comment) {
5750                        print format_subject_html($comment, $comment_short,
5751                                                  href(action=>"tag", hash=>$tag{'id'}));
5752                }
5753                print "</td>\n" .
5754                      "<td class=\"selflink\">";
5755                if ($tag{'type'} eq "tag") {
5756                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5757                } else {
5758                        print "&nbsp;";
5759                }
5760                print "</td>\n" .
5761                      "<td class=\"link\">" . " | " .
5762                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5763                if ($tag{'reftype'} eq "commit") {
5764                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5765                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5766                } elsif ($tag{'reftype'} eq "blob") {
5767                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5768                }
5769                print "</td>\n" .
5770                      "</tr>";
5771        }
5772        if (defined $extra) {
5773                print "<tr>\n" .
5774                      "<td colspan=\"5\">$extra</td>\n" .
5775                      "</tr>\n";
5776        }
5777        print "</table>\n";
5778}
5779
5780sub git_heads_body {
5781        # uses global variable $project
5782        my ($headlist, $head_at, $from, $to, $extra) = @_;
5783        $from = 0 unless defined $from;
5784        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5785
5786        print "<table class=\"heads\">\n";
5787        my $alternate = 1;
5788        for (my $i = $from; $i <= $to; $i++) {
5789                my $entry = $headlist->[$i];
5790                my %ref = %$entry;
5791                my $curr = defined $head_at && $ref{'id'} eq $head_at;
5792                if ($alternate) {
5793                        print "<tr class=\"dark\">\n";
5794                } else {
5795                        print "<tr class=\"light\">\n";
5796                }
5797                $alternate ^= 1;
5798                print "<td><i>$ref{'age'}</i></td>\n" .
5799                      ($curr ? "<td class=\"current_head\">" : "<td>") .
5800                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5801                               -class => "list name"},esc_html($ref{'name'})) .
5802                      "</td>\n" .
5803                      "<td class=\"link\">" .
5804                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5805                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5806                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5807                      "</td>\n" .
5808                      "</tr>";
5809        }
5810        if (defined $extra) {
5811                print "<tr>\n" .
5812                      "<td colspan=\"3\">$extra</td>\n" .
5813                      "</tr>\n";
5814        }
5815        print "</table>\n";
5816}
5817
5818# Display a single remote block
5819sub git_remote_block {
5820        my ($remote, $rdata, $limit, $head) = @_;
5821
5822        my $heads = $rdata->{'heads'};
5823        my $fetch = $rdata->{'fetch'};
5824        my $push = $rdata->{'push'};
5825
5826        my $urls_table = "<table class=\"projects_list\">\n" ;
5827
5828        if (defined $fetch) {
5829                if ($fetch eq $push) {
5830                        $urls_table .= format_repo_url("URL", $fetch);
5831                } else {
5832                        $urls_table .= format_repo_url("Fetch URL", $fetch);
5833                        $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5834                }
5835        } elsif (defined $push) {
5836                $urls_table .= format_repo_url("Push URL", $push);
5837        } else {
5838                $urls_table .= format_repo_url("", "No remote URL");
5839        }
5840
5841        $urls_table .= "</table>\n";
5842
5843        my $dots;
5844        if (defined $limit && $limit < @$heads) {
5845                $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5846        }
5847
5848        print $urls_table;
5849        git_heads_body($heads, $head, 0, $limit, $dots);
5850}
5851
5852# Display a list of remote names with the respective fetch and push URLs
5853sub git_remotes_list {
5854        my ($remotedata, $limit) = @_;
5855        print "<table class=\"heads\">\n";
5856        my $alternate = 1;
5857        my @remotes = sort keys %$remotedata;
5858
5859        my $limited = $limit && $limit < @remotes;
5860
5861        $#remotes = $limit - 1 if $limited;
5862
5863        while (my $remote = shift @remotes) {
5864                my $rdata = $remotedata->{$remote};
5865                my $fetch = $rdata->{'fetch'};
5866                my $push = $rdata->{'push'};
5867                if ($alternate) {
5868                        print "<tr class=\"dark\">\n";
5869                } else {
5870                        print "<tr class=\"light\">\n";
5871                }
5872                $alternate ^= 1;
5873                print "<td>" .
5874                      $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5875                               -class=> "list name"},esc_html($remote)) .
5876                      "</td>";
5877                print "<td class=\"link\">" .
5878                      (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5879                      " | " .
5880                      (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5881                      "</td>";
5882
5883                print "</tr>\n";
5884        }
5885
5886        if ($limited) {
5887                print "<tr>\n" .
5888                      "<td colspan=\"3\">" .
5889                      $cgi->a({-href => href(action=>"remotes")}, "...") .
5890                      "</td>\n" . "</tr>\n";
5891        }
5892
5893        print "</table>";
5894}
5895
5896# Display remote heads grouped by remote, unless there are too many
5897# remotes, in which case we only display the remote names
5898sub git_remotes_body {
5899        my ($remotedata, $limit, $head) = @_;
5900        if ($limit and $limit < keys %$remotedata) {
5901                git_remotes_list($remotedata, $limit);
5902        } else {
5903                fill_remote_heads($remotedata);
5904                while (my ($remote, $rdata) = each %$remotedata) {
5905                        git_print_section({-class=>"remote", -id=>$remote},
5906                                ["remotes", $remote, $remote], sub {
5907                                        git_remote_block($remote, $rdata, $limit, $head);
5908                                });
5909                }
5910        }
5911}
5912
5913sub git_search_message {
5914        my %co = @_;
5915
5916        my $greptype;
5917        if ($searchtype eq 'commit') {
5918                $greptype = "--grep=";
5919        } elsif ($searchtype eq 'author') {
5920                $greptype = "--author=";
5921        } elsif ($searchtype eq 'committer') {
5922                $greptype = "--committer=";
5923        }
5924        $greptype .= $searchtext;
5925        my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5926                                       $greptype, '--regexp-ignore-case',
5927                                       $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5928
5929        my $paging_nav = '';
5930        if ($page > 0) {
5931                $paging_nav .=
5932                        $cgi->a({-href => href(-replay=>1, page=>undef)},
5933                                "first") .
5934                        " &sdot; " .
5935                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5936                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5937        } else {
5938                $paging_nav .= "first &sdot; prev";
5939        }
5940        my $next_link = '';
5941        if ($#commitlist >= 100) {
5942                $next_link =
5943                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5944                                 -accesskey => "n", -title => "Alt-n"}, "next");
5945                $paging_nav .= " &sdot; $next_link";
5946        } else {
5947                $paging_nav .= " &sdot; next";
5948        }
5949
5950        git_header_html();
5951
5952        git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5953        git_print_header_div('commit', esc_html($co{'title'}), $hash);
5954        if ($page == 0 && !@commitlist) {
5955                print "<p>No match.</p>\n";
5956        } else {
5957                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5958        }
5959
5960        git_footer_html();
5961}
5962
5963sub git_search_changes {
5964        my %co = @_;
5965
5966        local $/ = "\n";
5967        open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5968                '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5969                ($search_use_regexp ? '--pickaxe-regex' : ())
5970                        or die_error(500, "Open git-log failed");
5971
5972        git_header_html();
5973
5974        git_print_page_nav('','', $hash,$co{'tree'},$hash);
5975        git_print_header_div('commit', esc_html($co{'title'}), $hash);
5976
5977        print "<table class=\"pickaxe search\">\n";
5978        my $alternate = 1;
5979        undef %co;
5980        my @files;
5981        while (my $line = <$fd>) {
5982                chomp $line;
5983                next unless $line;
5984
5985                my %set = parse_difftree_raw_line($line);
5986                if (defined $set{'commit'}) {
5987                        # finish previous commit
5988                        if (%co) {
5989                                print "</td>\n" .
5990                                      "<td class=\"link\">" .
5991                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
5992                                              "commit") .
5993                                      " | " .
5994                                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
5995                                                             hash_base=>$co{'id'})},
5996                                              "tree") .
5997                                      "</td>\n" .
5998                                      "</tr>\n";
5999                        }
6000
6001                        if ($alternate) {
6002                                print "<tr class=\"dark\">\n";
6003                        } else {
6004                                print "<tr class=\"light\">\n";
6005                        }
6006                        $alternate ^= 1;
6007                        %co = parse_commit($set{'commit'});
6008                        my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6009                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6010                              "<td><i>$author</i></td>\n" .
6011                              "<td>" .
6012                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6013                                      -class => "list subject"},
6014                                      chop_and_escape_str($co{'title'}, 50) . "<br/>");
6015                } elsif (defined $set{'to_id'}) {
6016                        next if ($set{'to_id'} =~ m/^0{40}$/);
6017
6018                        print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6019                                                     hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6020                                      -class => "list"},
6021                                      "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6022                              "<br/>\n";
6023                }
6024        }
6025        close $fd;
6026
6027        # finish last commit (warning: repetition!)
6028        if (%co) {
6029                print "</td>\n" .
6030                      "<td class=\"link\">" .
6031                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6032                              "commit") .
6033                      " | " .
6034                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6035                                             hash_base=>$co{'id'})},
6036                              "tree") .
6037                      "</td>\n" .
6038                      "</tr>\n";
6039        }
6040
6041        print "</table>\n";
6042
6043        git_footer_html();
6044}
6045
6046sub git_search_files {
6047        my %co = @_;
6048
6049        local $/ = "\n";
6050        open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
6051                $search_use_regexp ? ('-E', '-i') : '-F',
6052                $searchtext, $co{'tree'}
6053                        or die_error(500, "Open git-grep failed");
6054
6055        git_header_html();
6056
6057        git_print_page_nav('','', $hash,$co{'tree'},$hash);
6058        git_print_header_div('commit', esc_html($co{'title'}), $hash);
6059
6060        print "<table class=\"grep_search\">\n";
6061        my $alternate = 1;
6062        my $matches = 0;
6063        my $lastfile = '';
6064        my $file_href;
6065        while (my $line = <$fd>) {
6066                chomp $line;
6067                my ($file, $lno, $ltext, $binary);
6068                last if ($matches++ > 1000);
6069                if ($line =~ /^Binary file (.+) matches$/) {
6070                        $file = $1;
6071                        $binary = 1;
6072                } else {
6073                        ($file, $lno, $ltext) = split(/\0/, $line, 3);
6074                        $file =~ s/^$co{'tree'}://;
6075                }
6076                if ($file ne $lastfile) {
6077                        $lastfile and print "</td></tr>\n";
6078                        if ($alternate++) {
6079                                print "<tr class=\"dark\">\n";
6080                        } else {
6081                                print "<tr class=\"light\">\n";
6082                        }
6083                        $file_href = href(action=>"blob", hash_base=>$co{'id'},
6084                                          file_name=>$file);
6085                        print "<td class=\"list\">".
6086                                $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6087                        print "</td><td>\n";
6088                        $lastfile = $file;
6089                }
6090                if ($binary) {
6091                        print "<div class=\"binary\">Binary file</div>\n";
6092                } else {
6093                        $ltext = untabify($ltext);
6094                        if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6095                                $ltext = esc_html($1, -nbsp=>1);
6096                                $ltext .= '<span class="match">';
6097                                $ltext .= esc_html($2, -nbsp=>1);
6098                                $ltext .= '</span>';
6099                                $ltext .= esc_html($3, -nbsp=>1);
6100                        } else {
6101                                $ltext = esc_html($ltext, -nbsp=>1);
6102                        }
6103                        print "<div class=\"pre\">" .
6104                                $cgi->a({-href => $file_href.'#l'.$lno,
6105                                        -class => "linenr"}, sprintf('%4i', $lno)) .
6106                                ' ' .  $ltext . "</div>\n";
6107                }
6108        }
6109        if ($lastfile) {
6110                print "</td></tr>\n";
6111                if ($matches > 1000) {
6112                        print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6113                }
6114        } else {
6115                print "<div class=\"diff nodifferences\">No matches found</div>\n";
6116        }
6117        close $fd;
6118
6119        print "</table>\n";
6120
6121        git_footer_html();
6122}
6123
6124sub git_search_grep_body {
6125        my ($commitlist, $from, $to, $extra) = @_;
6126        $from = 0 unless defined $from;
6127        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6128
6129        print "<table class=\"commit_search\">\n";
6130        my $alternate = 1;
6131        for (my $i = $from; $i <= $to; $i++) {
6132                my %co = %{$commitlist->[$i]};
6133                if (!%co) {
6134                        next;
6135                }
6136                my $commit = $co{'id'};
6137                if ($alternate) {
6138                        print "<tr class=\"dark\">\n";
6139                } else {
6140                        print "<tr class=\"light\">\n";
6141                }
6142                $alternate ^= 1;
6143                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6144                      format_author_html('td', \%co, 15, 5) .
6145                      "<td>" .
6146                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6147                               -class => "list subject"},
6148                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6149                my $comment = $co{'comment'};
6150                foreach my $line (@$comment) {
6151                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6152                                my ($lead, $match, $trail) = ($1, $2, $3);
6153                                $match = chop_str($match, 70, 5, 'center');
6154                                my $contextlen = int((80 - length($match))/2);
6155                                $contextlen = 30 if ($contextlen > 30);
6156                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
6157                                $trail = chop_str($trail, $contextlen, 10, 'right');
6158
6159                                $lead  = esc_html($lead);
6160                                $match = esc_html($match);
6161                                $trail = esc_html($trail);
6162
6163                                print "$lead<span class=\"match\">$match</span>$trail<br />";
6164                        }
6165                }
6166                print "</td>\n" .
6167                      "<td class=\"link\">" .
6168                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6169                      " | " .
6170                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6171                      " | " .
6172                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6173                print "</td>\n" .
6174                      "</tr>\n";
6175        }
6176        if (defined $extra) {
6177                print "<tr>\n" .
6178                      "<td colspan=\"3\">$extra</td>\n" .
6179                      "</tr>\n";
6180        }
6181        print "</table>\n";
6182}
6183
6184## ======================================================================
6185## ======================================================================
6186## actions
6187
6188sub git_project_list {
6189        my $order = $input_params{'order'};
6190        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6191                die_error(400, "Unknown order parameter");
6192        }
6193
6194        my @list = git_get_projects_list($project_filter, $strict_export);
6195        if (!@list) {
6196                die_error(404, "No projects found");
6197        }
6198
6199        git_header_html();
6200        if (defined $home_text && -f $home_text) {
6201                print "<div class=\"index_include\">\n";
6202                insert_file($home_text);
6203                print "</div>\n";
6204        }
6205
6206        git_project_search_form($searchtext, $search_use_regexp);
6207        git_project_list_body(\@list, $order);
6208        git_footer_html();
6209}
6210
6211sub git_forks {
6212        my $order = $input_params{'order'};
6213        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6214                die_error(400, "Unknown order parameter");
6215        }
6216
6217        my $filter = $project;
6218        $filter =~ s/\.git$//;
6219        my @list = git_get_projects_list($filter);
6220        if (!@list) {
6221                die_error(404, "No forks found");
6222        }
6223
6224        git_header_html();
6225        git_print_page_nav('','');
6226        git_print_header_div('summary', "$project forks");
6227        git_project_list_body(\@list, $order);
6228        git_footer_html();
6229}
6230
6231sub git_project_index {
6232        my @projects = git_get_projects_list($project_filter, $strict_export);
6233        if (!@projects) {
6234                die_error(404, "No projects found");
6235        }
6236
6237        print $cgi->header(
6238                -type => 'text/plain',
6239                -charset => 'utf-8',
6240                -content_disposition => 'inline; filename="index.aux"');
6241
6242        foreach my $pr (@projects) {
6243                if (!exists $pr->{'owner'}) {
6244                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6245                }
6246
6247                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6248                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6249                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6250                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6251                $path  =~ s/ /\+/g;
6252                $owner =~ s/ /\+/g;
6253
6254                print "$path $owner\n";
6255        }
6256}
6257
6258sub git_summary {
6259        my $descr = git_get_project_description($project) || "none";
6260        my %co = parse_commit("HEAD");
6261        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6262        my $head = $co{'id'};
6263        my $remote_heads = gitweb_check_feature('remote_heads');
6264
6265        my $owner = git_get_project_owner($project);
6266
6267        my $refs = git_get_references();
6268        # These get_*_list functions return one more to allow us to see if
6269        # there are more ...
6270        my @taglist  = git_get_tags_list(16);
6271        my @headlist = git_get_heads_list(16);
6272        my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6273        my @forklist;
6274        my $check_forks = gitweb_check_feature('forks');
6275
6276        if ($check_forks) {
6277                # find forks of a project
6278                my $filter = $project;
6279                $filter =~ s/\.git$//;
6280                @forklist = git_get_projects_list($filter);
6281                # filter out forks of forks
6282                @forklist = filter_forks_from_projects_list(\@forklist)
6283                        if (@forklist);
6284        }
6285
6286        git_header_html();
6287        git_print_page_nav('summary','', $head);
6288
6289        print "<div class=\"title\">&nbsp;</div>\n";
6290        print "<table class=\"projects_list\">\n" .
6291              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
6292              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6293        if (defined $cd{'rfc2822'}) {
6294                print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6295                      "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6296        }
6297
6298        # use per project git URL list in $projectroot/$project/cloneurl
6299        # or make project git URL from git base URL and project name
6300        my $url_tag = "URL";
6301        my @url_list = git_get_project_url_list($project);
6302        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6303        foreach my $git_url (@url_list) {
6304                next unless $git_url;
6305                print format_repo_url($url_tag, $git_url);
6306                $url_tag = "";
6307        }
6308
6309        # Tag cloud
6310        my $show_ctags = gitweb_check_feature('ctags');
6311        if ($show_ctags) {
6312                my $ctags = git_get_project_ctags($project);
6313                if (%$ctags) {
6314                        # without ability to add tags, don't show if there are none
6315                        my $cloud = git_populate_project_tagcloud($ctags);
6316                        print "<tr id=\"metadata_ctags\">" .
6317                              "<td>content tags</td>" .
6318                              "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6319                              "</tr>\n";
6320                }
6321        }
6322
6323        print "</table>\n";
6324
6325        # If XSS prevention is on, we don't include README.html.
6326        # TODO: Allow a readme in some safe format.
6327        if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6328                print "<div class=\"title\">readme</div>\n" .
6329                      "<div class=\"readme\">\n";
6330                insert_file("$projectroot/$project/README.html");
6331                print "\n</div>\n"; # class="readme"
6332        }
6333
6334        # we need to request one more than 16 (0..15) to check if
6335        # those 16 are all
6336        my @commitlist = $head ? parse_commits($head, 17) : ();
6337        if (@commitlist) {
6338                git_print_header_div('shortlog');
6339                git_shortlog_body(\@commitlist, 0, 15, $refs,
6340                                  $#commitlist <=  15 ? undef :
6341                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
6342        }
6343
6344        if (@taglist) {
6345                git_print_header_div('tags');
6346                git_tags_body(\@taglist, 0, 15,
6347                              $#taglist <=  15 ? undef :
6348                              $cgi->a({-href => href(action=>"tags")}, "..."));
6349        }
6350
6351        if (@headlist) {
6352                git_print_header_div('heads');
6353                git_heads_body(\@headlist, $head, 0, 15,
6354                               $#headlist <= 15 ? undef :
6355                               $cgi->a({-href => href(action=>"heads")}, "..."));
6356        }
6357
6358        if (%remotedata) {
6359                git_print_header_div('remotes');
6360                git_remotes_body(\%remotedata, 15, $head);
6361        }
6362
6363        if (@forklist) {
6364                git_print_header_div('forks');
6365                git_project_list_body(\@forklist, 'age', 0, 15,
6366                                      $#forklist <= 15 ? undef :
6367                                      $cgi->a({-href => href(action=>"forks")}, "..."),
6368                                      'no_header');
6369        }
6370
6371        git_footer_html();
6372}
6373
6374sub git_tag {
6375        my %tag = parse_tag($hash);
6376
6377        if (! %tag) {
6378                die_error(404, "Unknown tag object");
6379        }
6380
6381        my $head = git_get_head_hash($project);
6382        git_header_html();
6383        git_print_page_nav('','', $head,undef,$head);
6384        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6385        print "<div class=\"title_text\">\n" .
6386              "<table class=\"object_header\">\n" .
6387              "<tr>\n" .
6388              "<td>object</td>\n" .
6389              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6390                               $tag{'object'}) . "</td>\n" .
6391              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6392                                              $tag{'type'}) . "</td>\n" .
6393              "</tr>\n";
6394        if (defined($tag{'author'})) {
6395                git_print_authorship_rows(\%tag, 'author');
6396        }
6397        print "</table>\n\n" .
6398              "</div>\n";
6399        print "<div class=\"page_body\">";
6400        my $comment = $tag{'comment'};
6401        foreach my $line (@$comment) {
6402                chomp $line;
6403                print esc_html($line, -nbsp=>1) . "<br/>\n";
6404        }
6405        print "</div>\n";
6406        git_footer_html();
6407}
6408
6409sub git_blame_common {
6410        my $format = shift || 'porcelain';
6411        if ($format eq 'porcelain' && $input_params{'javascript'}) {
6412                $format = 'incremental';
6413                $action = 'blame_incremental'; # for page title etc
6414        }
6415
6416        # permissions
6417        gitweb_check_feature('blame')
6418                or die_error(403, "Blame view not allowed");
6419
6420        # error checking
6421        die_error(400, "No file name given") unless $file_name;
6422        $hash_base ||= git_get_head_hash($project);
6423        die_error(404, "Couldn't find base commit") unless $hash_base;
6424        my %co = parse_commit($hash_base)
6425                or die_error(404, "Commit not found");
6426        my $ftype = "blob";
6427        if (!defined $hash) {
6428                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6429                        or die_error(404, "Error looking up file");
6430        } else {
6431                $ftype = git_get_type($hash);
6432                if ($ftype !~ "blob") {
6433                        die_error(400, "Object is not a blob");
6434                }
6435        }
6436
6437        my $fd;
6438        if ($format eq 'incremental') {
6439                # get file contents (as base)
6440                open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6441                        or die_error(500, "Open git-cat-file failed");
6442        } elsif ($format eq 'data') {
6443                # run git-blame --incremental
6444                open $fd, "-|", git_cmd(), "blame", "--incremental",
6445                        $hash_base, "--", $file_name
6446                        or die_error(500, "Open git-blame --incremental failed");
6447        } else {
6448                # run git-blame --porcelain
6449                open $fd, "-|", git_cmd(), "blame", '-p',
6450                        $hash_base, '--', $file_name
6451                        or die_error(500, "Open git-blame --porcelain failed");
6452        }
6453
6454        # incremental blame data returns early
6455        if ($format eq 'data') {
6456                print $cgi->header(
6457                        -type=>"text/plain", -charset => "utf-8",
6458                        -status=> "200 OK");
6459                local $| = 1; # output autoflush
6460                while (my $line = <$fd>) {
6461                        print to_utf8($line);
6462                }
6463                close $fd
6464                        or print "ERROR $!\n";
6465
6466                print 'END';
6467                if (defined $t0 && gitweb_check_feature('timed')) {
6468                        print ' '.
6469                              tv_interval($t0, [ gettimeofday() ]).
6470                              ' '.$number_of_git_cmds;
6471                }
6472                print "\n";
6473
6474                return;
6475        }
6476
6477        # page header
6478        git_header_html();
6479        my $formats_nav =
6480                $cgi->a({-href => href(action=>"blob", -replay=>1)},
6481                        "blob") .
6482                " | ";
6483        if ($format eq 'incremental') {
6484                $formats_nav .=
6485                        $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6486                                "blame") . " (non-incremental)";
6487        } else {
6488                $formats_nav .=
6489                        $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6490                                "blame") . " (incremental)";
6491        }
6492        $formats_nav .=
6493                " | " .
6494                $cgi->a({-href => href(action=>"history", -replay=>1)},
6495                        "history") .
6496                " | " .
6497                $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6498                        "HEAD");
6499        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6500        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6501        git_print_page_path($file_name, $ftype, $hash_base);
6502
6503        # page body
6504        if ($format eq 'incremental') {
6505                print "<noscript>\n<div class=\"error\"><center><b>\n".
6506                      "This page requires JavaScript to run.\n Use ".
6507                      $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6508                              'this page').
6509                      " instead.\n".
6510                      "</b></center></div>\n</noscript>\n";
6511
6512                print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6513        }
6514
6515        print qq!<div class="page_body">\n!;
6516        print qq!<div id="progress_info">... / ...</div>\n!
6517                if ($format eq 'incremental');
6518        print qq!<table id="blame_table" class="blame" width="100%">\n!.
6519              #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6520              qq!<thead>\n!.
6521              qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6522              qq!</thead>\n!.
6523              qq!<tbody>\n!;
6524
6525        my @rev_color = qw(light dark);
6526        my $num_colors = scalar(@rev_color);
6527        my $current_color = 0;
6528
6529        if ($format eq 'incremental') {
6530                my $color_class = $rev_color[$current_color];
6531
6532                #contents of a file
6533                my $linenr = 0;
6534        LINE:
6535                while (my $line = <$fd>) {
6536                        chomp $line;
6537                        $linenr++;
6538
6539                        print qq!<tr id="l$linenr" class="$color_class">!.
6540                              qq!<td class="sha1"><a href=""> </a></td>!.
6541                              qq!<td class="linenr">!.
6542                              qq!<a class="linenr" href="">$linenr</a></td>!;
6543                        print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6544                        print qq!</tr>\n!;
6545                }
6546
6547        } else { # porcelain, i.e. ordinary blame
6548                my %metainfo = (); # saves information about commits
6549
6550                # blame data
6551        LINE:
6552                while (my $line = <$fd>) {
6553                        chomp $line;
6554                        # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6555                        # no <lines in group> for subsequent lines in group of lines
6556                        my ($full_rev, $orig_lineno, $lineno, $group_size) =
6557                           ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
6558                        if (!exists $metainfo{$full_rev}) {
6559                                $metainfo{$full_rev} = { 'nprevious' => 0 };
6560                        }
6561                        my $meta = $metainfo{$full_rev};
6562                        my $data;
6563                        while ($data = <$fd>) {
6564                                chomp $data;
6565                                last if ($data =~ s/^\t//); # contents of line
6566                                if ($data =~ /^(\S+)(?: (.*))?$/) {
6567                                        $meta->{$1} = $2 unless exists $meta->{$1};
6568                                }
6569                                if ($data =~ /^previous /) {
6570                                        $meta->{'nprevious'}++;
6571                                }
6572                        }
6573                        my $short_rev = substr($full_rev, 0, 8);
6574                        my $author = $meta->{'author'};
6575                        my %date =
6576                                parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6577                        my $date = $date{'iso-tz'};
6578                        if ($group_size) {
6579                                $current_color = ($current_color + 1) % $num_colors;
6580                        }
6581                        my $tr_class = $rev_color[$current_color];
6582                        $tr_class .= ' boundary' if (exists $meta->{'boundary'});
6583                        $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6584                        $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6585                        print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6586                        if ($group_size) {
6587                                print "<td class=\"sha1\"";
6588                                print " title=\"". esc_html($author) . ", $date\"";
6589                                print " rowspan=\"$group_size\"" if ($group_size > 1);
6590                                print ">";
6591                                print $cgi->a({-href => href(action=>"commit",
6592                                                             hash=>$full_rev,
6593                                                             file_name=>$file_name)},
6594                                              esc_html($short_rev));
6595                                if ($group_size >= 2) {
6596                                        my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6597                                        if (@author_initials) {
6598                                                print "<br />" .
6599                                                      esc_html(join('', @author_initials));
6600                                                #           or join('.', ...)
6601                                        }
6602                                }
6603                                print "</td>\n";
6604                        }
6605                        # 'previous' <sha1 of parent commit> <filename at commit>
6606                        if (exists $meta->{'previous'} &&
6607                            $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
6608                                $meta->{'parent'} = $1;
6609                                $meta->{'file_parent'} = unquote($2);
6610                        }
6611                        my $linenr_commit =
6612                                exists($meta->{'parent'}) ?
6613                                $meta->{'parent'} : $full_rev;
6614                        my $linenr_filename =
6615                                exists($meta->{'file_parent'}) ?
6616                                $meta->{'file_parent'} : unquote($meta->{'filename'});
6617                        my $blamed = href(action => 'blame',
6618                                          file_name => $linenr_filename,
6619                                          hash_base => $linenr_commit);
6620                        print "<td class=\"linenr\">";
6621                        print $cgi->a({ -href => "$blamed#l$orig_lineno",
6622                                        -class => "linenr" },
6623                                      esc_html($lineno));
6624                        print "</td>";
6625                        print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6626                        print "</tr>\n";
6627                } # end while
6628
6629        }
6630
6631        # footer
6632        print "</tbody>\n".
6633              "</table>\n"; # class="blame"
6634        print "</div>\n";   # class="blame_body"
6635        close $fd
6636                or print "Reading blob failed\n";
6637
6638        git_footer_html();
6639}
6640
6641sub git_blame {
6642        git_blame_common();
6643}
6644
6645sub git_blame_incremental {
6646        git_blame_common('incremental');
6647}
6648
6649sub git_blame_data {
6650        git_blame_common('data');
6651}
6652
6653sub git_tags {
6654        my $head = git_get_head_hash($project);
6655        git_header_html();
6656        git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
6657        git_print_header_div('summary', $project);
6658
6659        my @tagslist = git_get_tags_list();
6660        if (@tagslist) {
6661                git_tags_body(\@tagslist);
6662        }
6663        git_footer_html();
6664}
6665
6666sub git_heads {
6667        my $head = git_get_head_hash($project);
6668        git_header_html();
6669        git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
6670        git_print_header_div('summary', $project);
6671
6672        my @headslist = git_get_heads_list();
6673        if (@headslist) {
6674                git_heads_body(\@headslist, $head);
6675        }
6676        git_footer_html();
6677}
6678
6679# used both for single remote view and for list of all the remotes
6680sub git_remotes {
6681        gitweb_check_feature('remote_heads')
6682                or die_error(403, "Remote heads view is disabled");
6683
6684        my $head = git_get_head_hash($project);
6685        my $remote = $input_params{'hash'};
6686
6687        my $remotedata = git_get_remotes_list($remote);
6688        die_error(500, "Unable to get remote information") unless defined $remotedata;
6689
6690        unless (%$remotedata) {
6691                die_error(404, defined $remote ?
6692                        "Remote $remote not found" :
6693                        "No remotes found");
6694        }
6695
6696        git_header_html(undef, undef, -action_extra => $remote);
6697        git_print_page_nav('', '',  $head, undef, $head,
6698                format_ref_views($remote ? '' : 'remotes'));
6699
6700        fill_remote_heads($remotedata);
6701        if (defined $remote) {
6702                git_print_header_div('remotes', "$remote remote for $project");
6703                git_remote_block($remote, $remotedata->{$remote}, undef, $head);
6704        } else {
6705                git_print_header_div('summary', "$project remotes");
6706                git_remotes_body($remotedata, undef, $head);
6707        }
6708
6709        git_footer_html();
6710}
6711
6712sub git_blob_plain {
6713        my $type = shift;
6714        my $expires;
6715
6716        if (!defined $hash) {
6717                if (defined $file_name) {
6718                        my $base = $hash_base || git_get_head_hash($project);
6719                        $hash = git_get_hash_by_path($base, $file_name, "blob")
6720                                or die_error(404, "Cannot find file");
6721                } else {
6722                        die_error(400, "No file name defined");
6723                }
6724        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6725                # blobs defined by non-textual hash id's can be cached
6726                $expires = "+1d";
6727        }
6728
6729        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6730                or die_error(500, "Open git-cat-file blob '$hash' failed");
6731
6732        # content-type (can include charset)
6733        $type = blob_contenttype($fd, $file_name, $type);
6734
6735        # "save as" filename, even when no $file_name is given
6736        my $save_as = "$hash";
6737        if (defined $file_name) {
6738                $save_as = $file_name;
6739        } elsif ($type =~ m/^text\//) {
6740                $save_as .= '.txt';
6741        }
6742
6743        # With XSS prevention on, blobs of all types except a few known safe
6744        # ones are served with "Content-Disposition: attachment" to make sure
6745        # they don't run in our security domain.  For certain image types,
6746        # blob view writes an <img> tag referring to blob_plain view, and we
6747        # want to be sure not to break that by serving the image as an
6748        # attachment (though Firefox 3 doesn't seem to care).
6749        my $sandbox = $prevent_xss &&
6750                $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
6751
6752        # serve text/* as text/plain
6753        if ($prevent_xss &&
6754            ($type =~ m!^text/[a-z]+\b(.*)$! ||
6755             ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
6756                my $rest = $1;
6757                $rest = defined $rest ? $rest : '';
6758                $type = "text/plain$rest";
6759        }
6760
6761        print $cgi->header(
6762                -type => $type,
6763                -expires => $expires,
6764                -content_disposition =>
6765                        ($sandbox ? 'attachment' : 'inline')
6766                        . '; filename="' . $save_as . '"');
6767        local $/ = undef;
6768        binmode STDOUT, ':raw';
6769        print <$fd>;
6770        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6771        close $fd;
6772}
6773
6774sub git_blob {
6775        my $expires;
6776
6777        if (!defined $hash) {
6778                if (defined $file_name) {
6779                        my $base = $hash_base || git_get_head_hash($project);
6780                        $hash = git_get_hash_by_path($base, $file_name, "blob")
6781                                or die_error(404, "Cannot find file");
6782                } else {
6783                        die_error(400, "No file name defined");
6784                }
6785        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6786                # blobs defined by non-textual hash id's can be cached
6787                $expires = "+1d";
6788        }
6789
6790        my $have_blame = gitweb_check_feature('blame');
6791        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6792                or die_error(500, "Couldn't cat $file_name, $hash");
6793        my $mimetype = blob_mimetype($fd, $file_name);
6794        # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6795        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6796                close $fd;
6797                return git_blob_plain($mimetype);
6798        }
6799        # we can have blame only for text/* mimetype
6800        $have_blame &&= ($mimetype =~ m!^text/!);
6801
6802        my $highlight = gitweb_check_feature('highlight');
6803        my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6804        $fd = run_highlighter($fd, $highlight, $syntax)
6805                if $syntax;
6806
6807        git_header_html(undef, $expires);
6808        my $formats_nav = '';
6809        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6810                if (defined $file_name) {
6811                        if ($have_blame) {
6812                                $formats_nav .=
6813                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
6814                                                "blame") .
6815                                        " | ";
6816                        }
6817                        $formats_nav .=
6818                                $cgi->a({-href => href(action=>"history", -replay=>1)},
6819                                        "history") .
6820                                " | " .
6821                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6822                                        "raw") .
6823                                " | " .
6824                                $cgi->a({-href => href(action=>"blob",
6825                                                       hash_base=>"HEAD", file_name=>$file_name)},
6826                                        "HEAD");
6827                } else {
6828                        $formats_nav .=
6829                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6830                                        "raw");
6831                }
6832                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6833                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6834        } else {
6835                print "<div class=\"page_nav\">\n" .
6836                      "<br/><br/></div>\n" .
6837                      "<div class=\"title\">".esc_html($hash)."</div>\n";
6838        }
6839        git_print_page_path($file_name, "blob", $hash_base);
6840        print "<div class=\"page_body\">\n";
6841        if ($mimetype =~ m!^image/!) {
6842                print qq!<img type="!.esc_attr($mimetype).qq!"!;
6843                if ($file_name) {
6844                        print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6845                }
6846                print qq! src="! .
6847                      href(action=>"blob_plain", hash=>$hash,
6848                           hash_base=>$hash_base, file_name=>$file_name) .
6849                      qq!" />\n!;
6850        } else {
6851                my $nr;
6852                while (my $line = <$fd>) {
6853                        chomp $line;
6854                        $nr++;
6855                        $line = untabify($line);
6856                        printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6857                               $nr, esc_attr(href(-replay => 1)), $nr, $nr,
6858                               $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
6859                }
6860        }
6861        close $fd
6862                or print "Reading blob failed.\n";
6863        print "</div>";
6864        git_footer_html();
6865}
6866
6867sub git_tree {
6868        if (!defined $hash_base) {
6869                $hash_base = "HEAD";
6870        }
6871        if (!defined $hash) {
6872                if (defined $file_name) {
6873                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6874                } else {
6875                        $hash = $hash_base;
6876                }
6877        }
6878        die_error(404, "No such tree") unless defined($hash);
6879
6880        my $show_sizes = gitweb_check_feature('show-sizes');
6881        my $have_blame = gitweb_check_feature('blame');
6882
6883        my @entries = ();
6884        {
6885                local $/ = "\0";
6886                open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6887                        ($show_sizes ? '-l' : ()), @extra_options, $hash
6888                        or die_error(500, "Open git-ls-tree failed");
6889                @entries = map { chomp; $_ } <$fd>;
6890                close $fd
6891                        or die_error(404, "Reading tree failed");
6892        }
6893
6894        my $refs = git_get_references();
6895        my $ref = format_ref_marker($refs, $hash_base);
6896        git_header_html();
6897        my $basedir = '';
6898        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6899                my @views_nav = ();
6900                if (defined $file_name) {
6901                        push @views_nav,
6902                                $cgi->a({-href => href(action=>"history", -replay=>1)},
6903                                        "history"),
6904                                $cgi->a({-href => href(action=>"tree",
6905                                                       hash_base=>"HEAD", file_name=>$file_name)},
6906                                        "HEAD"),
6907                }
6908                my $snapshot_links = format_snapshot_links($hash);
6909                if (defined $snapshot_links) {
6910                        # FIXME: Should be available when we have no hash base as well.
6911                        push @views_nav, $snapshot_links;
6912                }
6913                git_print_page_nav('tree','', $hash_base, undef, undef,
6914                                   join(' | ', @views_nav));
6915                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6916        } else {
6917                undef $hash_base;
6918                print "<div class=\"page_nav\">\n";
6919                print "<br/><br/></div>\n";
6920                print "<div class=\"title\">".esc_html($hash)."</div>\n";
6921        }
6922        if (defined $file_name) {
6923                $basedir = $file_name;
6924                if ($basedir ne '' && substr($basedir, -1) ne '/') {
6925                        $basedir .= '/';
6926                }
6927                git_print_page_path($file_name, 'tree', $hash_base);
6928        }
6929        print "<div class=\"page_body\">\n";
6930        print "<table class=\"tree\">\n";
6931        my $alternate = 1;
6932        # '..' (top directory) link if possible
6933        if (defined $hash_base &&
6934            defined $file_name && $file_name =~ m![^/]+$!) {
6935                if ($alternate) {
6936                        print "<tr class=\"dark\">\n";
6937                } else {
6938                        print "<tr class=\"light\">\n";
6939                }
6940                $alternate ^= 1;
6941
6942                my $up = $file_name;
6943                $up =~ s!/?[^/]+$!!;
6944                undef $up unless $up;
6945                # based on git_print_tree_entry
6946                print '<td class="mode">' . mode_str('040000') . "</td>\n";
6947                print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6948                print '<td class="list">';
6949                print $cgi->a({-href => href(action=>"tree",
6950                                             hash_base=>$hash_base,
6951                                             file_name=>$up)},
6952                              "..");
6953                print "</td>\n";
6954                print "<td class=\"link\"></td>\n";
6955
6956                print "</tr>\n";
6957        }
6958        foreach my $line (@entries) {
6959                my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6960
6961                if ($alternate) {
6962                        print "<tr class=\"dark\">\n";
6963                } else {
6964                        print "<tr class=\"light\">\n";
6965                }
6966                $alternate ^= 1;
6967
6968                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6969
6970                print "</tr>\n";
6971        }
6972        print "</table>\n" .
6973              "</div>";
6974        git_footer_html();
6975}
6976
6977sub snapshot_name {
6978        my ($project, $hash) = @_;
6979
6980        # path/to/project.git  -> project
6981        # path/to/project/.git -> project
6982        my $name = to_utf8($project);
6983        $name =~ s,([^/])/*\.git$,$1,;
6984        $name = basename($name);
6985        # sanitize name
6986        $name =~ s/[[:cntrl:]]/?/g;
6987
6988        my $ver = $hash;
6989        if ($hash =~ /^[0-9a-fA-F]+$/) {
6990                # shorten SHA-1 hash
6991                my $full_hash = git_get_full_hash($project, $hash);
6992                if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6993                        $ver = git_get_short_hash($project, $hash);
6994                }
6995        } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6996                # tags don't need shortened SHA-1 hash
6997                $ver = $1;
6998        } else {
6999                # branches and other need shortened SHA-1 hash
7000                if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
7001                        $ver = $1;
7002                }
7003                $ver .= '-' . git_get_short_hash($project, $hash);
7004        }
7005        # in case of hierarchical branch names
7006        $ver =~ s!/!.!g;
7007
7008        # name = project-version_string
7009        $name = "$name-$ver";
7010
7011        return wantarray ? ($name, $name) : $name;
7012}
7013
7014sub git_snapshot {
7015        my $format = $input_params{'snapshot_format'};
7016        if (!@snapshot_fmts) {
7017                die_error(403, "Snapshots not allowed");
7018        }
7019        # default to first supported snapshot format
7020        $format ||= $snapshot_fmts[0];
7021        if ($format !~ m/^[a-z0-9]+$/) {
7022                die_error(400, "Invalid snapshot format parameter");
7023        } elsif (!exists($known_snapshot_formats{$format})) {
7024                die_error(400, "Unknown snapshot format");
7025        } elsif ($known_snapshot_formats{$format}{'disabled'}) {
7026                die_error(403, "Snapshot format not allowed");
7027        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
7028                die_error(403, "Unsupported snapshot format");
7029        }
7030
7031        my $type = git_get_type("$hash^{}");
7032        if (!$type) {
7033                die_error(404, 'Object does not exist');
7034        }  elsif ($type eq 'blob') {
7035                die_error(400, 'Object is not a tree-ish');
7036        }
7037
7038        my ($name, $prefix) = snapshot_name($project, $hash);
7039        my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7040        my $cmd = quote_command(
7041                git_cmd(), 'archive',
7042                "--format=$known_snapshot_formats{$format}{'format'}",
7043                "--prefix=$prefix/", $hash);
7044        if (exists $known_snapshot_formats{$format}{'compressor'}) {
7045                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7046        }
7047
7048        $filename =~ s/(["\\])/\\$1/g;
7049        print $cgi->header(
7050                -type => $known_snapshot_formats{$format}{'type'},
7051                -content_disposition => 'inline; filename="' . $filename . '"',
7052                -status => '200 OK');
7053
7054        open my $fd, "-|", $cmd
7055                or die_error(500, "Execute git-archive failed");
7056        binmode STDOUT, ':raw';
7057        print <$fd>;
7058        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7059        close $fd;
7060}
7061
7062sub git_log_generic {
7063        my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7064
7065        my $head = git_get_head_hash($project);
7066        if (!defined $base) {
7067                $base = $head;
7068        }
7069        if (!defined $page) {
7070                $page = 0;
7071        }
7072        my $refs = git_get_references();
7073
7074        my $commit_hash = $base;
7075        if (defined $parent) {
7076                $commit_hash = "$parent..$base";
7077        }
7078        my @commitlist =
7079                parse_commits($commit_hash, 101, (100 * $page),
7080                              defined $file_name ? ($file_name, "--full-history") : ());
7081
7082        my $ftype;
7083        if (!defined $file_hash && defined $file_name) {
7084                # some commits could have deleted file in question,
7085                # and not have it in tree, but one of them has to have it
7086                for (my $i = 0; $i < @commitlist; $i++) {
7087                        $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7088                        last if defined $file_hash;
7089                }
7090        }
7091        if (defined $file_hash) {
7092                $ftype = git_get_type($file_hash);
7093        }
7094        if (defined $file_name && !defined $ftype) {
7095                die_error(500, "Unknown type of object");
7096        }
7097        my %co;
7098        if (defined $file_name) {
7099                %co = parse_commit($base)
7100                        or die_error(404, "Unknown commit object");
7101        }
7102
7103
7104        my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7105        my $next_link = '';
7106        if ($#commitlist >= 100) {
7107                $next_link =
7108                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
7109                                 -accesskey => "n", -title => "Alt-n"}, "next");
7110        }
7111        my $patch_max = gitweb_get_feature('patches');
7112        if ($patch_max && !defined $file_name) {
7113                if ($patch_max < 0 || @commitlist <= $patch_max) {
7114                        $paging_nav .= " &sdot; " .
7115                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
7116                                        "patches");
7117                }
7118        }
7119
7120        git_header_html();
7121        git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7122        if (defined $file_name) {
7123                git_print_header_div('commit', esc_html($co{'title'}), $base);
7124        } else {
7125                git_print_header_div('summary', $project)
7126        }
7127        git_print_page_path($file_name, $ftype, $hash_base)
7128                if (defined $file_name);
7129
7130        $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7131                     $file_name, $file_hash, $ftype);
7132
7133        git_footer_html();
7134}
7135
7136sub git_log {
7137        git_log_generic('log', \&git_log_body,
7138                        $hash, $hash_parent);
7139}
7140
7141sub git_commit {
7142        $hash ||= $hash_base || "HEAD";
7143        my %co = parse_commit($hash)
7144            or die_error(404, "Unknown commit object");
7145
7146        my $parent  = $co{'parent'};
7147        my $parents = $co{'parents'}; # listref
7148
7149        # we need to prepare $formats_nav before any parameter munging
7150        my $formats_nav;
7151        if (!defined $parent) {
7152                # --root commitdiff
7153                $formats_nav .= '(initial)';
7154        } elsif (@$parents == 1) {
7155                # single parent commit
7156                $formats_nav .=
7157                        '(parent: ' .
7158                        $cgi->a({-href => href(action=>"commit",
7159                                               hash=>$parent)},
7160                                esc_html(substr($parent, 0, 7))) .
7161                        ')';
7162        } else {
7163                # merge commit
7164                $formats_nav .=
7165                        '(merge: ' .
7166                        join(' ', map {
7167                                $cgi->a({-href => href(action=>"commit",
7168                                                       hash=>$_)},
7169                                        esc_html(substr($_, 0, 7)));
7170                        } @$parents ) .
7171                        ')';
7172        }
7173        if (gitweb_check_feature('patches') && @$parents <= 1) {
7174                $formats_nav .= " | " .
7175                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
7176                                "patch");
7177        }
7178
7179        if (!defined $parent) {
7180                $parent = "--root";
7181        }
7182        my @difftree;
7183        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
7184                @diff_opts,
7185                (@$parents <= 1 ? $parent : '-c'),
7186                $hash, "--"
7187                or die_error(500, "Open git-diff-tree failed");
7188        @difftree = map { chomp; $_ } <$fd>;
7189        close $fd or die_error(404, "Reading git-diff-tree failed");
7190
7191        # non-textual hash id's can be cached
7192        my $expires;
7193        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7194                $expires = "+1d";
7195        }
7196        my $refs = git_get_references();
7197        my $ref = format_ref_marker($refs, $co{'id'});
7198
7199        git_header_html(undef, $expires);
7200        git_print_page_nav('commit', '',
7201                           $hash, $co{'tree'}, $hash,
7202                           $formats_nav);
7203
7204        if (defined $co{'parent'}) {
7205                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7206        } else {
7207                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7208        }
7209        print "<div class=\"title_text\">\n" .
7210              "<table class=\"object_header\">\n";
7211        git_print_authorship_rows(\%co);
7212        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7213        print "<tr>" .
7214              "<td>tree</td>" .
7215              "<td class=\"sha1\">" .
7216              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7217                       class => "list"}, $co{'tree'}) .
7218              "</td>" .
7219              "<td class=\"link\">" .
7220              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7221                      "tree");
7222        my $snapshot_links = format_snapshot_links($hash);
7223        if (defined $snapshot_links) {
7224                print " | " . $snapshot_links;
7225        }
7226        print "</td>" .
7227              "</tr>\n";
7228
7229        foreach my $par (@$parents) {
7230                print "<tr>" .
7231                      "<td>parent</td>" .
7232                      "<td class=\"sha1\">" .
7233                      $cgi->a({-href => href(action=>"commit", hash=>$par),
7234                               class => "list"}, $par) .
7235                      "</td>" .
7236                      "<td class=\"link\">" .
7237                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7238                      " | " .
7239                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7240                      "</td>" .
7241                      "</tr>\n";
7242        }
7243        print "</table>".
7244              "</div>\n";
7245
7246        print "<div class=\"page_body\">\n";
7247        git_print_log($co{'comment'});
7248        print "</div>\n";
7249
7250        git_difftree_body(\@difftree, $hash, @$parents);
7251
7252        git_footer_html();
7253}
7254
7255sub git_object {
7256        # object is defined by:
7257        # - hash or hash_base alone
7258        # - hash_base and file_name
7259        my $type;
7260
7261        # - hash or hash_base alone
7262        if ($hash || ($hash_base && !defined $file_name)) {
7263                my $object_id = $hash || $hash_base;
7264
7265                open my $fd, "-|", quote_command(
7266                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
7267                        or die_error(404, "Object does not exist");
7268                $type = <$fd>;
7269                chomp $type;
7270                close $fd
7271                        or die_error(404, "Object does not exist");
7272
7273        # - hash_base and file_name
7274        } elsif ($hash_base && defined $file_name) {
7275                $file_name =~ s,/+$,,;
7276
7277                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7278                        or die_error(404, "Base object does not exist");
7279
7280                # here errors should not hapen
7281                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
7282                        or die_error(500, "Open git-ls-tree failed");
7283                my $line = <$fd>;
7284                close $fd;
7285
7286                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
7287                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
7288                        die_error(404, "File or directory for given base does not exist");
7289                }
7290                $type = $2;
7291                $hash = $3;
7292        } else {
7293                die_error(400, "Not enough information to find object");
7294        }
7295
7296        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7297                                          hash=>$hash, hash_base=>$hash_base,
7298                                          file_name=>$file_name),
7299                             -status => '302 Found');
7300}
7301
7302sub git_blobdiff {
7303        my $format = shift || 'html';
7304        my $diff_style = $input_params{'diff_style'} || 'inline';
7305
7306        my $fd;
7307        my @difftree;
7308        my %diffinfo;
7309        my $expires;
7310
7311        # preparing $fd and %diffinfo for git_patchset_body
7312        # new style URI
7313        if (defined $hash_base && defined $hash_parent_base) {
7314                if (defined $file_name) {
7315                        # read raw output
7316                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7317                                $hash_parent_base, $hash_base,
7318                                "--", (defined $file_parent ? $file_parent : ()), $file_name
7319                                or die_error(500, "Open git-diff-tree failed");
7320                        @difftree = map { chomp; $_ } <$fd>;
7321                        close $fd
7322                                or die_error(404, "Reading git-diff-tree failed");
7323                        @difftree
7324                                or die_error(404, "Blob diff not found");
7325
7326                } elsif (defined $hash &&
7327                         $hash =~ /[0-9a-fA-F]{40}/) {
7328                        # try to find filename from $hash
7329
7330                        # read filtered raw output
7331                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7332                                $hash_parent_base, $hash_base, "--"
7333                                or die_error(500, "Open git-diff-tree failed");
7334                        @difftree =
7335                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
7336                                # $hash == to_id
7337                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7338                                map { chomp; $_ } <$fd>;
7339                        close $fd
7340                                or die_error(404, "Reading git-diff-tree failed");
7341                        @difftree
7342                                or die_error(404, "Blob diff not found");
7343
7344                } else {
7345                        die_error(400, "Missing one of the blob diff parameters");
7346                }
7347
7348                if (@difftree > 1) {
7349                        die_error(400, "Ambiguous blob diff specification");
7350                }
7351
7352                %diffinfo = parse_difftree_raw_line($difftree[0]);
7353                $file_parent ||= $diffinfo{'from_file'} || $file_name;
7354                $file_name   ||= $diffinfo{'to_file'};
7355
7356                $hash_parent ||= $diffinfo{'from_id'};
7357                $hash        ||= $diffinfo{'to_id'};
7358
7359                # non-textual hash id's can be cached
7360                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7361                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7362                        $expires = '+1d';
7363                }
7364
7365                # open patch output
7366                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7367                        '-p', ($format eq 'html' ? "--full-index" : ()),
7368                        $hash_parent_base, $hash_base,
7369                        "--", (defined $file_parent ? $file_parent : ()), $file_name
7370                        or die_error(500, "Open git-diff-tree failed");
7371        }
7372
7373        # old/legacy style URI -- not generated anymore since 1.4.3.
7374        if (!%diffinfo) {
7375                die_error('404 Not Found', "Missing one of the blob diff parameters")
7376        }
7377
7378        # header
7379        if ($format eq 'html') {
7380                my $formats_nav =
7381                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7382                                "raw");
7383                $formats_nav .= diff_style_nav($diff_style);
7384                git_header_html(undef, $expires);
7385                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7386                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7387                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7388                } else {
7389                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7390                        print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7391                }
7392                if (defined $file_name) {
7393                        git_print_page_path($file_name, "blob", $hash_base);
7394                } else {
7395                        print "<div class=\"page_path\"></div>\n";
7396                }
7397
7398        } elsif ($format eq 'plain') {
7399                print $cgi->header(
7400                        -type => 'text/plain',
7401                        -charset => 'utf-8',
7402                        -expires => $expires,
7403                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7404
7405                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7406
7407        } else {
7408                die_error(400, "Unknown blobdiff format");
7409        }
7410
7411        # patch
7412        if ($format eq 'html') {
7413                print "<div class=\"page_body\">\n";
7414
7415                git_patchset_body($fd, $diff_style,
7416                                  [ \%diffinfo ], $hash_base, $hash_parent_base);
7417                close $fd;
7418
7419                print "</div>\n"; # class="page_body"
7420                git_footer_html();
7421
7422        } else {
7423                while (my $line = <$fd>) {
7424                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7425                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7426
7427                        print $line;
7428
7429                        last if $line =~ m!^\+\+\+!;
7430                }
7431                local $/ = undef;
7432                print <$fd>;
7433                close $fd;
7434        }
7435}
7436
7437sub git_blobdiff_plain {
7438        git_blobdiff('plain');
7439}
7440
7441# assumes that it is added as later part of already existing navigation,
7442# so it returns "| foo | bar" rather than just "foo | bar"
7443sub diff_style_nav {
7444        my ($diff_style, $is_combined) = @_;
7445        $diff_style ||= 'inline';
7446
7447        return "" if ($is_combined);
7448
7449        my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7450        my %styles = @styles;
7451        @styles =
7452                @styles[ map { $_ * 2 } 0..$#styles/2 ];
7453
7454        return join '',
7455                map { " | ".$_ }
7456                map {
7457                        $_ eq $diff_style ? $styles{$_} :
7458                        $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7459                } @styles;
7460}
7461
7462sub git_commitdiff {
7463        my %params = @_;
7464        my $format = $params{-format} || 'html';
7465        my $diff_style = $input_params{'diff_style'} || 'inline';
7466
7467        my ($patch_max) = gitweb_get_feature('patches');
7468        if ($format eq 'patch') {
7469                die_error(403, "Patch view not allowed") unless $patch_max;
7470        }
7471
7472        $hash ||= $hash_base || "HEAD";
7473        my %co = parse_commit($hash)
7474            or die_error(404, "Unknown commit object");
7475
7476        # choose format for commitdiff for merge
7477        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7478                $hash_parent = '--cc';
7479        }
7480        # we need to prepare $formats_nav before almost any parameter munging
7481        my $formats_nav;
7482        if ($format eq 'html') {
7483                $formats_nav =
7484                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
7485                                "raw");
7486                if ($patch_max && @{$co{'parents'}} <= 1) {
7487                        $formats_nav .= " | " .
7488                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
7489                                        "patch");
7490                }
7491                $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
7492
7493                if (defined $hash_parent &&
7494                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
7495                        # commitdiff with two commits given
7496                        my $hash_parent_short = $hash_parent;
7497                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
7498                                $hash_parent_short = substr($hash_parent, 0, 7);
7499                        }
7500                        $formats_nav .=
7501                                ' (from';
7502                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7503                                if ($co{'parents'}[$i] eq $hash_parent) {
7504                                        $formats_nav .= ' parent ' . ($i+1);
7505                                        last;
7506                                }
7507                        }
7508                        $formats_nav .= ': ' .
7509                                $cgi->a({-href => href(-replay=>1,
7510                                                       hash=>$hash_parent, hash_base=>undef)},
7511                                        esc_html($hash_parent_short)) .
7512                                ')';
7513                } elsif (!$co{'parent'}) {
7514                        # --root commitdiff
7515                        $formats_nav .= ' (initial)';
7516                } elsif (scalar @{$co{'parents'}} == 1) {
7517                        # single parent commit
7518                        $formats_nav .=
7519                                ' (parent: ' .
7520                                $cgi->a({-href => href(-replay=>1,
7521                                                       hash=>$co{'parent'}, hash_base=>undef)},
7522                                        esc_html(substr($co{'parent'}, 0, 7))) .
7523                                ')';
7524                } else {
7525                        # merge commit
7526                        if ($hash_parent eq '--cc') {
7527                                $formats_nav .= ' | ' .
7528                                        $cgi->a({-href => href(-replay=>1,
7529                                                               hash=>$hash, hash_parent=>'-c')},
7530                                                'combined');
7531                        } else { # $hash_parent eq '-c'
7532                                $formats_nav .= ' | ' .
7533                                        $cgi->a({-href => href(-replay=>1,
7534                                                               hash=>$hash, hash_parent=>'--cc')},
7535                                                'compact');
7536                        }
7537                        $formats_nav .=
7538                                ' (merge: ' .
7539                                join(' ', map {
7540                                        $cgi->a({-href => href(-replay=>1,
7541                                                               hash=>$_, hash_base=>undef)},
7542                                                esc_html(substr($_, 0, 7)));
7543                                } @{$co{'parents'}} ) .
7544                                ')';
7545                }
7546        }
7547
7548        my $hash_parent_param = $hash_parent;
7549        if (!defined $hash_parent_param) {
7550                # --cc for multiple parents, --root for parentless
7551                $hash_parent_param =
7552                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
7553        }
7554
7555        # read commitdiff
7556        my $fd;
7557        my @difftree;
7558        if ($format eq 'html') {
7559                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7560                        "--no-commit-id", "--patch-with-raw", "--full-index",
7561                        $hash_parent_param, $hash, "--"
7562                        or die_error(500, "Open git-diff-tree failed");
7563
7564                while (my $line = <$fd>) {
7565                        chomp $line;
7566                        # empty line ends raw part of diff-tree output
7567                        last unless $line;
7568                        push @difftree, scalar parse_difftree_raw_line($line);
7569                }
7570
7571        } elsif ($format eq 'plain') {
7572                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7573                        '-p', $hash_parent_param, $hash, "--"
7574                        or die_error(500, "Open git-diff-tree failed");
7575        } elsif ($format eq 'patch') {
7576                # For commit ranges, we limit the output to the number of
7577                # patches specified in the 'patches' feature.
7578                # For single commits, we limit the output to a single patch,
7579                # diverging from the git-format-patch default.
7580                my @commit_spec = ();
7581                if ($hash_parent) {
7582                        if ($patch_max > 0) {
7583                                push @commit_spec, "-$patch_max";
7584                        }
7585                        push @commit_spec, '-n', "$hash_parent..$hash";
7586                } else {
7587                        if ($params{-single}) {
7588                                push @commit_spec, '-1';
7589                        } else {
7590                                if ($patch_max > 0) {
7591                                        push @commit_spec, "-$patch_max";
7592                                }
7593                                push @commit_spec, "-n";
7594                        }
7595                        push @commit_spec, '--root', $hash;
7596                }
7597                open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7598                        '--encoding=utf8', '--stdout', @commit_spec
7599                        or die_error(500, "Open git-format-patch failed");
7600        } else {
7601                die_error(400, "Unknown commitdiff format");
7602        }
7603
7604        # non-textual hash id's can be cached
7605        my $expires;
7606        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7607                $expires = "+1d";
7608        }
7609
7610        # write commit message
7611        if ($format eq 'html') {
7612                my $refs = git_get_references();
7613                my $ref = format_ref_marker($refs, $co{'id'});
7614
7615                git_header_html(undef, $expires);
7616                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7617                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
7618                print "<div class=\"title_text\">\n" .
7619                      "<table class=\"object_header\">\n";
7620                git_print_authorship_rows(\%co);
7621                print "</table>".
7622                      "</div>\n";
7623                print "<div class=\"page_body\">\n";
7624                if (@{$co{'comment'}} > 1) {
7625                        print "<div class=\"log\">\n";
7626                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7627                        print "</div>\n"; # class="log"
7628                }
7629
7630        } elsif ($format eq 'plain') {
7631                my $refs = git_get_references("tags");
7632                my $tagname = git_get_rev_name_tags($hash);
7633                my $filename = basename($project) . "-$hash.patch";
7634
7635                print $cgi->header(
7636                        -type => 'text/plain',
7637                        -charset => 'utf-8',
7638                        -expires => $expires,
7639                        -content_disposition => 'inline; filename="' . "$filename" . '"');
7640                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7641                print "From: " . to_utf8($co{'author'}) . "\n";
7642                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
7643                print "Subject: " . to_utf8($co{'title'}) . "\n";
7644
7645                print "X-Git-Tag: $tagname\n" if $tagname;
7646                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7647
7648                foreach my $line (@{$co{'comment'}}) {
7649                        print to_utf8($line) . "\n";
7650                }
7651                print "---\n\n";
7652        } elsif ($format eq 'patch') {
7653                my $filename = basename($project) . "-$hash.patch";
7654
7655                print $cgi->header(
7656                        -type => 'text/plain',
7657                        -charset => 'utf-8',
7658                        -expires => $expires,
7659                        -content_disposition => 'inline; filename="' . "$filename" . '"');
7660        }
7661
7662        # write patch
7663        if ($format eq 'html') {
7664                my $use_parents = !defined $hash_parent ||
7665                        $hash_parent eq '-c' || $hash_parent eq '--cc';
7666                git_difftree_body(\@difftree, $hash,
7667                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
7668                print "<br/>\n";
7669
7670                git_patchset_body($fd, $diff_style,
7671                                  \@difftree, $hash,
7672                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
7673                close $fd;
7674                print "</div>\n"; # class="page_body"
7675                git_footer_html();
7676
7677        } elsif ($format eq 'plain') {
7678                local $/ = undef;
7679                print <$fd>;
7680                close $fd
7681                        or print "Reading git-diff-tree failed\n";
7682        } elsif ($format eq 'patch') {
7683                local $/ = undef;
7684                print <$fd>;
7685                close $fd
7686                        or print "Reading git-format-patch failed\n";
7687        }
7688}
7689
7690sub git_commitdiff_plain {
7691        git_commitdiff(-format => 'plain');
7692}
7693
7694# format-patch-style patches
7695sub git_patch {
7696        git_commitdiff(-format => 'patch', -single => 1);
7697}
7698
7699sub git_patches {
7700        git_commitdiff(-format => 'patch');
7701}
7702
7703sub git_history {
7704        git_log_generic('history', \&git_history_body,
7705                        $hash_base, $hash_parent_base,
7706                        $file_name, $hash);
7707}
7708
7709sub git_search {
7710        $searchtype ||= 'commit';
7711
7712        # check if appropriate features are enabled
7713        gitweb_check_feature('search')
7714                or die_error(403, "Search is disabled");
7715        if ($searchtype eq 'pickaxe') {
7716                # pickaxe may take all resources of your box and run for several minutes
7717                # with every query - so decide by yourself how public you make this feature
7718                gitweb_check_feature('pickaxe')
7719                        or die_error(403, "Pickaxe search is disabled");
7720        }
7721        if ($searchtype eq 'grep') {
7722                # grep search might be potentially CPU-intensive, too
7723                gitweb_check_feature('grep')
7724                        or die_error(403, "Grep search is disabled");
7725        }
7726
7727        if (!defined $searchtext) {
7728                die_error(400, "Text field is empty");
7729        }
7730        if (!defined $hash) {
7731                $hash = git_get_head_hash($project);
7732        }
7733        my %co = parse_commit($hash);
7734        if (!%co) {
7735                die_error(404, "Unknown commit object");
7736        }
7737        if (!defined $page) {
7738                $page = 0;
7739        }
7740
7741        if ($searchtype eq 'commit' ||
7742            $searchtype eq 'author' ||
7743            $searchtype eq 'committer') {
7744                git_search_message(%co);
7745        } elsif ($searchtype eq 'pickaxe') {
7746                git_search_changes(%co);
7747        } elsif ($searchtype eq 'grep') {
7748                git_search_files(%co);
7749        } else {
7750                die_error(400, "Unknown search type");
7751        }
7752}
7753
7754sub git_search_help {
7755        git_header_html();
7756        git_print_page_nav('','', $hash,$hash,$hash);
7757        print <<EOT;
7758<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7759regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7760the pattern entered is recognized as the POSIX extended
7761<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7762insensitive).</p>
7763<dl>
7764<dt><b>commit</b></dt>
7765<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7766EOT
7767        my $have_grep = gitweb_check_feature('grep');
7768        if ($have_grep) {
7769                print <<EOT;
7770<dt><b>grep</b></dt>
7771<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7772    a different one) are searched for the given pattern. On large trees, this search can take
7773a while and put some strain on the server, so please use it with some consideration. Note that
7774due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7775case-sensitive.</dd>
7776EOT
7777        }
7778        print <<EOT;
7779<dt><b>author</b></dt>
7780<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7781<dt><b>committer</b></dt>
7782<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7783EOT
7784        my $have_pickaxe = gitweb_check_feature('pickaxe');
7785        if ($have_pickaxe) {
7786                print <<EOT;
7787<dt><b>pickaxe</b></dt>
7788<dd>All commits that caused the string to appear or disappear from any file (changes that
7789added, removed or "modified" the string) will be listed. This search can take a while and
7790takes a lot of strain on the server, so please use it wisely. Note that since you may be
7791interested even in changes just changing the case as well, this search is case sensitive.</dd>
7792EOT
7793        }
7794        print "</dl>\n";
7795        git_footer_html();
7796}
7797
7798sub git_shortlog {
7799        git_log_generic('shortlog', \&git_shortlog_body,
7800                        $hash, $hash_parent);
7801}
7802
7803## ......................................................................
7804## feeds (RSS, Atom; OPML)
7805
7806sub git_feed {
7807        my $format = shift || 'atom';
7808        my $have_blame = gitweb_check_feature('blame');
7809
7810        # Atom: http://www.atomenabled.org/developers/syndication/
7811        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7812        if ($format ne 'rss' && $format ne 'atom') {
7813                die_error(400, "Unknown web feed format");
7814        }
7815
7816        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7817        my $head = $hash || 'HEAD';
7818        my @commitlist = parse_commits($head, 150, 0, $file_name);
7819
7820        my %latest_commit;
7821        my %latest_date;
7822        my $content_type = "application/$format+xml";
7823        if (defined $cgi->http('HTTP_ACCEPT') &&
7824                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7825                # browser (feed reader) prefers text/xml
7826                $content_type = 'text/xml';
7827        }
7828        if (defined($commitlist[0])) {
7829                %latest_commit = %{$commitlist[0]};
7830                my $latest_epoch = $latest_commit{'committer_epoch'};
7831                %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7832                my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7833                if (defined $if_modified) {
7834                        my $since;
7835                        if (eval { require HTTP::Date; 1; }) {
7836                                $since = HTTP::Date::str2time($if_modified);
7837                        } elsif (eval { require Time::ParseDate; 1; }) {
7838                                $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7839                        }
7840                        if (defined $since && $latest_epoch <= $since) {
7841                                print $cgi->header(
7842                                        -type => $content_type,
7843                                        -charset => 'utf-8',
7844                                        -last_modified => $latest_date{'rfc2822'},
7845                                        -status => '304 Not Modified');
7846                                return;
7847                        }
7848                }
7849                print $cgi->header(
7850                        -type => $content_type,
7851                        -charset => 'utf-8',
7852                        -last_modified => $latest_date{'rfc2822'});
7853        } else {
7854                print $cgi->header(
7855                        -type => $content_type,
7856                        -charset => 'utf-8');
7857        }
7858
7859        # Optimization: skip generating the body if client asks only
7860        # for Last-Modified date.
7861        return if ($cgi->request_method() eq 'HEAD');
7862
7863        # header variables
7864        my $title = "$site_name - $project/$action";
7865        my $feed_type = 'log';
7866        if (defined $hash) {
7867                $title .= " - '$hash'";
7868                $feed_type = 'branch log';
7869                if (defined $file_name) {
7870                        $title .= " :: $file_name";
7871                        $feed_type = 'history';
7872                }
7873        } elsif (defined $file_name) {
7874                $title .= " - $file_name";
7875                $feed_type = 'history';
7876        }
7877        $title .= " $feed_type";
7878        my $descr = git_get_project_description($project);
7879        if (defined $descr) {
7880                $descr = esc_html($descr);
7881        } else {
7882                $descr = "$project " .
7883                         ($format eq 'rss' ? 'RSS' : 'Atom') .
7884                         " feed";
7885        }
7886        my $owner = git_get_project_owner($project);
7887        $owner = esc_html($owner);
7888
7889        #header
7890        my $alt_url;
7891        if (defined $file_name) {
7892                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7893        } elsif (defined $hash) {
7894                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7895        } else {
7896                $alt_url = href(-full=>1, action=>"summary");
7897        }
7898        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7899        if ($format eq 'rss') {
7900                print <<XML;
7901<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7902<channel>
7903XML
7904                print "<title>$title</title>\n" .
7905                      "<link>$alt_url</link>\n" .
7906                      "<description>$descr</description>\n" .
7907                      "<language>en</language>\n" .
7908                      # project owner is responsible for 'editorial' content
7909                      "<managingEditor>$owner</managingEditor>\n";
7910                if (defined $logo || defined $favicon) {
7911                        # prefer the logo to the favicon, since RSS
7912                        # doesn't allow both
7913                        my $img = esc_url($logo || $favicon);
7914                        print "<image>\n" .
7915                              "<url>$img</url>\n" .
7916                              "<title>$title</title>\n" .
7917                              "<link>$alt_url</link>\n" .
7918                              "</image>\n";
7919                }
7920                if (%latest_date) {
7921                        print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7922                        print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7923                }
7924                print "<generator>gitweb v.$version/$git_version</generator>\n";
7925        } elsif ($format eq 'atom') {
7926                print <<XML;
7927<feed xmlns="http://www.w3.org/2005/Atom">
7928XML
7929                print "<title>$title</title>\n" .
7930                      "<subtitle>$descr</subtitle>\n" .
7931                      '<link rel="alternate" type="text/html" href="' .
7932                      $alt_url . '" />' . "\n" .
7933                      '<link rel="self" type="' . $content_type . '" href="' .
7934                      $cgi->self_url() . '" />' . "\n" .
7935                      "<id>" . href(-full=>1) . "</id>\n" .
7936                      # use project owner for feed author
7937                      "<author><name>$owner</name></author>\n";
7938                if (defined $favicon) {
7939                        print "<icon>" . esc_url($favicon) . "</icon>\n";
7940                }
7941                if (defined $logo) {
7942                        # not twice as wide as tall: 72 x 27 pixels
7943                        print "<logo>" . esc_url($logo) . "</logo>\n";
7944                }
7945                if (! %latest_date) {
7946                        # dummy date to keep the feed valid until commits trickle in:
7947                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
7948                } else {
7949                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
7950                }
7951                print "<generator version='$version/$git_version'>gitweb</generator>\n";
7952        }
7953
7954        # contents
7955        for (my $i = 0; $i <= $#commitlist; $i++) {
7956                my %co = %{$commitlist[$i]};
7957                my $commit = $co{'id'};
7958                # we read 150, we always show 30 and the ones more recent than 48 hours
7959                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7960                        last;
7961                }
7962                my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7963
7964                # get list of changed files
7965                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7966                        $co{'parent'} || "--root",
7967                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
7968                        or next;
7969                my @difftree = map { chomp; $_ } <$fd>;
7970                close $fd
7971                        or next;
7972
7973                # print element (entry, item)
7974                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7975                if ($format eq 'rss') {
7976                        print "<item>\n" .
7977                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
7978                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
7979                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7980                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7981                              "<link>$co_url</link>\n" .
7982                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
7983                              "<content:encoded>" .
7984                              "<![CDATA[\n";
7985                } elsif ($format eq 'atom') {
7986                        print "<entry>\n" .
7987                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7988                              "<updated>$cd{'iso-8601'}</updated>\n" .
7989                              "<author>\n" .
7990                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
7991                        if ($co{'author_email'}) {
7992                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
7993                        }
7994                        print "</author>\n" .
7995                              # use committer for contributor
7996                              "<contributor>\n" .
7997                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7998                        if ($co{'committer_email'}) {
7999                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
8000                        }
8001                        print "</contributor>\n" .
8002                              "<published>$cd{'iso-8601'}</published>\n" .
8003                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
8004                              "<id>$co_url</id>\n" .
8005                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8006                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8007                }
8008                my $comment = $co{'comment'};
8009                print "<pre>\n";
8010                foreach my $line (@$comment) {
8011                        $line = esc_html($line);
8012                        print "$line\n";
8013                }
8014                print "</pre><ul>\n";
8015                foreach my $difftree_line (@difftree) {
8016                        my %difftree = parse_difftree_raw_line($difftree_line);
8017                        next if !$difftree{'from_id'};
8018
8019                        my $file = $difftree{'file'} || $difftree{'to_file'};
8020
8021                        print "<li>" .
8022                              "[" .
8023                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8024                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8025                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8026                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
8027                                      -title => "diff"}, 'D');
8028                        if ($have_blame) {
8029                                print $cgi->a({-href => href(-full=>1, action=>"blame",
8030                                                             file_name=>$file, hash_base=>$commit),
8031                                              -title => "blame"}, 'B');
8032                        }
8033                        # if this is not a feed of a file history
8034                        if (!defined $file_name || $file_name ne $file) {
8035                                print $cgi->a({-href => href(-full=>1, action=>"history",
8036                                                             file_name=>$file, hash=>$commit),
8037                                              -title => "history"}, 'H');
8038                        }
8039                        $file = esc_path($file);
8040                        print "] ".
8041                              "$file</li>\n";
8042                }
8043                if ($format eq 'rss') {
8044                        print "</ul>]]>\n" .
8045                              "</content:encoded>\n" .
8046                              "</item>\n";
8047                } elsif ($format eq 'atom') {
8048                        print "</ul>\n</div>\n" .
8049                              "</content>\n" .
8050                              "</entry>\n";
8051                }
8052        }
8053
8054        # end of feed
8055        if ($format eq 'rss') {
8056                print "</channel>\n</rss>\n";
8057        } elsif ($format eq 'atom') {
8058                print "</feed>\n";
8059        }
8060}
8061
8062sub git_rss {
8063        git_feed('rss');
8064}
8065
8066sub git_atom {
8067        git_feed('atom');
8068}
8069
8070sub git_opml {
8071        my @list = git_get_projects_list($project_filter, $strict_export);
8072        if (!@list) {
8073                die_error(404, "No projects found");
8074        }
8075
8076        print $cgi->header(
8077                -type => 'text/xml',
8078                -charset => 'utf-8',
8079                -content_disposition => 'inline; filename="opml.xml"');
8080
8081        my $title = esc_html($site_name);
8082        my $filter = " within subdirectory ";
8083        if (defined $project_filter) {
8084                $filter .= esc_html($project_filter);
8085        } else {
8086                $filter = "";
8087        }
8088        print <<XML;
8089<?xml version="1.0" encoding="utf-8"?>
8090<opml version="1.0">
8091<head>
8092  <title>$title OPML Export$filter</title>
8093</head>
8094<body>
8095<outline text="git RSS feeds">
8096XML
8097
8098        foreach my $pr (@list) {
8099                my %proj = %$pr;
8100                my $head = git_get_head_hash($proj{'path'});
8101                if (!defined $head) {
8102                        next;
8103                }
8104                $git_dir = "$projectroot/$proj{'path'}";
8105                my %co = parse_commit($head);
8106                if (!%co) {
8107                        next;
8108                }
8109
8110                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8111                my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
8112                my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
8113                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8114        }
8115        print <<XML;
8116</outline>
8117</body>
8118</opml>
8119XML
8120}