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