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