951bb0d6f020edf4c5ef6a04ccac9c662d2b8dbd
   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, @classes) = @_;
3173        unless (@classes) {
3174                my $remote_heads = gitweb_check_feature('remote_heads');
3175                @classes = ('heads', $remote_heads ? 'remotes' : ());
3176        }
3177        my @patterns = map { "refs/$_" } @classes;
3178        my @headslist;
3179
3180        open my $fd, '-|', git_cmd(), 'for-each-ref',
3181                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3182                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3183                @patterns
3184                or return;
3185        while (my $line = <$fd>) {
3186                my %ref_item;
3187
3188                chomp $line;
3189                my ($refinfo, $committerinfo) = split(/\0/, $line);
3190                my ($hash, $name, $title) = split(' ', $refinfo, 3);
3191                my ($committer, $epoch, $tz) =
3192                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3193                $ref_item{'fullname'}  = $name;
3194                $name =~ s!^refs/(?:head|remote)s/!!;
3195
3196                $ref_item{'name'}  = $name;
3197                $ref_item{'id'}    = $hash;
3198                $ref_item{'title'} = $title || '(no commit message)';
3199                $ref_item{'epoch'} = $epoch;
3200                if ($epoch) {
3201                        $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3202                } else {
3203                        $ref_item{'age'} = "unknown";
3204                }
3205
3206                push @headslist, \%ref_item;
3207        }
3208        close $fd;
3209
3210        return wantarray ? @headslist : \@headslist;
3211}
3212
3213sub git_get_tags_list {
3214        my $limit = shift;
3215        my @tagslist;
3216
3217        open my $fd, '-|', git_cmd(), 'for-each-ref',
3218                ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3219                '--format=%(objectname) %(objecttype) %(refname) '.
3220                '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3221                'refs/tags'
3222                or return;
3223        while (my $line = <$fd>) {
3224                my %ref_item;
3225
3226                chomp $line;
3227                my ($refinfo, $creatorinfo) = split(/\0/, $line);
3228                my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3229                my ($creator, $epoch, $tz) =
3230                        ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3231                $ref_item{'fullname'} = $name;
3232                $name =~ s!^refs/tags/!!;
3233
3234                $ref_item{'type'} = $type;
3235                $ref_item{'id'} = $id;
3236                $ref_item{'name'} = $name;
3237                if ($type eq "tag") {
3238                        $ref_item{'subject'} = $title;
3239                        $ref_item{'reftype'} = $reftype;
3240                        $ref_item{'refid'}   = $refid;
3241                } else {
3242                        $ref_item{'reftype'} = $type;
3243                        $ref_item{'refid'}   = $id;
3244                }
3245
3246                if ($type eq "tag" || $type eq "commit") {
3247                        $ref_item{'epoch'} = $epoch;
3248                        if ($epoch) {
3249                                $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3250                        } else {
3251                                $ref_item{'age'} = "unknown";
3252                        }
3253                }
3254
3255                push @tagslist, \%ref_item;
3256        }
3257        close $fd;
3258
3259        return wantarray ? @tagslist : \@tagslist;
3260}
3261
3262## ----------------------------------------------------------------------
3263## filesystem-related functions
3264
3265sub get_file_owner {
3266        my $path = shift;
3267
3268        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3269        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3270        if (!defined $gcos) {
3271                return undef;
3272        }
3273        my $owner = $gcos;
3274        $owner =~ s/[,;].*$//;
3275        return to_utf8($owner);
3276}
3277
3278# assume that file exists
3279sub insert_file {
3280        my $filename = shift;
3281
3282        open my $fd, '<', $filename;
3283        print map { to_utf8($_) } <$fd>;
3284        close $fd;
3285}
3286
3287## ......................................................................
3288## mimetype related functions
3289
3290sub mimetype_guess_file {
3291        my $filename = shift;
3292        my $mimemap = shift;
3293        -r $mimemap or return undef;
3294
3295        my %mimemap;
3296        open(my $mh, '<', $mimemap) or return undef;
3297        while (<$mh>) {
3298                next if m/^#/; # skip comments
3299                my ($mimetype, $exts) = split(/\t+/);
3300                if (defined $exts) {
3301                        my @exts = split(/\s+/, $exts);
3302                        foreach my $ext (@exts) {
3303                                $mimemap{$ext} = $mimetype;
3304                        }
3305                }
3306        }
3307        close($mh);
3308
3309        $filename =~ /\.([^.]*)$/;
3310        return $mimemap{$1};
3311}
3312
3313sub mimetype_guess {
3314        my $filename = shift;
3315        my $mime;
3316        $filename =~ /\./ or return undef;
3317
3318        if ($mimetypes_file) {
3319                my $file = $mimetypes_file;
3320                if ($file !~ m!^/!) { # if it is relative path
3321                        # it is relative to project
3322                        $file = "$projectroot/$project/$file";
3323                }
3324                $mime = mimetype_guess_file($filename, $file);
3325        }
3326        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3327        return $mime;
3328}
3329
3330sub blob_mimetype {
3331        my $fd = shift;
3332        my $filename = shift;
3333
3334        if ($filename) {
3335                my $mime = mimetype_guess($filename);
3336                $mime and return $mime;
3337        }
3338
3339        # just in case
3340        return $default_blob_plain_mimetype unless $fd;
3341
3342        if (-T $fd) {
3343                return 'text/plain';
3344        } elsif (! $filename) {
3345                return 'application/octet-stream';
3346        } elsif ($filename =~ m/\.png$/i) {
3347                return 'image/png';
3348        } elsif ($filename =~ m/\.gif$/i) {
3349                return 'image/gif';
3350        } elsif ($filename =~ m/\.jpe?g$/i) {
3351                return 'image/jpeg';
3352        } else {
3353                return 'application/octet-stream';
3354        }
3355}
3356
3357sub blob_contenttype {
3358        my ($fd, $file_name, $type) = @_;
3359
3360        $type ||= blob_mimetype($fd, $file_name);
3361        if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3362                $type .= "; charset=$default_text_plain_charset";
3363        }
3364
3365        return $type;
3366}
3367
3368# guess file syntax for syntax highlighting; return undef if no highlighting
3369# the name of syntax can (in the future) depend on syntax highlighter used
3370sub guess_file_syntax {
3371        my ($highlight, $mimetype, $file_name) = @_;
3372        return undef unless ($highlight && defined $file_name);
3373        my $basename = basename($file_name, '.in');
3374        return $highlight_basename{$basename}
3375                if exists $highlight_basename{$basename};
3376
3377        $basename =~ /\.([^.]*)$/;
3378        my $ext = $1 or return undef;
3379        return $highlight_ext{$ext}
3380                if exists $highlight_ext{$ext};
3381
3382        return undef;
3383}
3384
3385# run highlighter and return FD of its output,
3386# or return original FD if no highlighting
3387sub run_highlighter {
3388        my ($fd, $highlight, $syntax) = @_;
3389        return $fd unless ($highlight && defined $syntax);
3390
3391        close $fd
3392                or die_error(404, "Reading blob failed");
3393        open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3394                  quote_command($highlight_bin).
3395                  " --xhtml --fragment --syntax $syntax |"
3396                or die_error(500, "Couldn't open file or run syntax highlighter");
3397        return $fd;
3398}
3399
3400## ======================================================================
3401## functions printing HTML: header, footer, error page
3402
3403sub get_page_title {
3404        my $title = to_utf8($site_name);
3405
3406        return $title unless (defined $project);
3407        $title .= " - " . to_utf8($project);
3408
3409        return $title unless (defined $action);
3410        $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3411
3412        return $title unless (defined $file_name);
3413        $title .= " - " . esc_path($file_name);
3414        if ($action eq "tree" && $file_name !~ m|/$|) {
3415                $title .= "/";
3416        }
3417
3418        return $title;
3419}
3420
3421sub git_header_html {
3422        my $status = shift || "200 OK";
3423        my $expires = shift;
3424        my %opts = @_;
3425
3426        my $title = get_page_title();
3427        my $content_type;
3428        # require explicit support from the UA if we are to send the page as
3429        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3430        # we have to do this because MSIE sometimes globs '*/*', pretending to
3431        # support xhtml+xml but choking when it gets what it asked for.
3432        if (defined $cgi->http('HTTP_ACCEPT') &&
3433            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3434            $cgi->Accept('application/xhtml+xml') != 0) {
3435                $content_type = 'application/xhtml+xml';
3436        } else {
3437                $content_type = 'text/html';
3438        }
3439        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3440                           -status=> $status, -expires => $expires)
3441                unless ($opts{'-no_http_header'});
3442        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3443        print <<EOF;
3444<?xml version="1.0" encoding="utf-8"?>
3445<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3446<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3447<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3448<!-- git core binaries version $git_version -->
3449<head>
3450<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3451<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3452<meta name="robots" content="index, nofollow"/>
3453<title>$title</title>
3454EOF
3455        # the stylesheet, favicon etc urls won't work correctly with path_info
3456        # unless we set the appropriate base URL
3457        if ($ENV{'PATH_INFO'}) {
3458                print "<base href=\"".esc_url($base_url)."\" />\n";
3459        }
3460        # print out each stylesheet that exist, providing backwards capability
3461        # for those people who defined $stylesheet in a config file
3462        if (defined $stylesheet) {
3463                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3464        } else {
3465                foreach my $stylesheet (@stylesheets) {
3466                        next unless $stylesheet;
3467                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3468                }
3469        }
3470        if (defined $project) {
3471                my %href_params = get_feed_info();
3472                if (!exists $href_params{'-title'}) {
3473                        $href_params{'-title'} = 'log';
3474                }
3475
3476                foreach my $format qw(RSS Atom) {
3477                        my $type = lc($format);
3478                        my %link_attr = (
3479                                '-rel' => 'alternate',
3480                                '-title' => "$project - $href_params{'-title'} - $format feed",
3481                                '-type' => "application/$type+xml"
3482                        );
3483
3484                        $href_params{'action'} = $type;
3485                        $link_attr{'-href'} = href(%href_params);
3486                        print "<link ".
3487                              "rel=\"$link_attr{'-rel'}\" ".
3488                              "title=\"$link_attr{'-title'}\" ".
3489                              "href=\"$link_attr{'-href'}\" ".
3490                              "type=\"$link_attr{'-type'}\" ".
3491                              "/>\n";
3492
3493                        $href_params{'extra_options'} = '--no-merges';
3494                        $link_attr{'-href'} = href(%href_params);
3495                        $link_attr{'-title'} .= ' (no merges)';
3496                        print "<link ".
3497                              "rel=\"$link_attr{'-rel'}\" ".
3498                              "title=\"$link_attr{'-title'}\" ".
3499                              "href=\"$link_attr{'-href'}\" ".
3500                              "type=\"$link_attr{'-type'}\" ".
3501                              "/>\n";
3502                }
3503
3504        } else {
3505                printf('<link rel="alternate" title="%s projects list" '.
3506                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
3507                       $site_name, href(project=>undef, action=>"project_index"));
3508                printf('<link rel="alternate" title="%s projects feeds" '.
3509                       'href="%s" type="text/x-opml" />'."\n",
3510                       $site_name, href(project=>undef, action=>"opml"));
3511        }
3512        if (defined $favicon) {
3513                print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3514        }
3515
3516        print "</head>\n" .
3517              "<body>\n";
3518
3519        if (defined $site_header && -f $site_header) {
3520                insert_file($site_header);
3521        }
3522
3523        print "<div class=\"page_header\">\n" .
3524              $cgi->a({-href => esc_url($logo_url),
3525                       -title => $logo_label},
3526                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3527        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3528        if (defined $project) {
3529                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3530                if (defined $action) {
3531                        print " / $action";
3532                }
3533                print "\n";
3534        }
3535        print "</div>\n";
3536
3537        my $have_search = gitweb_check_feature('search');
3538        if (defined $project && $have_search) {
3539                if (!defined $searchtext) {
3540                        $searchtext = "";
3541                }
3542                my $search_hash;
3543                if (defined $hash_base) {
3544                        $search_hash = $hash_base;
3545                } elsif (defined $hash) {
3546                        $search_hash = $hash;
3547                } else {
3548                        $search_hash = "HEAD";
3549                }
3550                my $action = $my_uri;
3551                my $use_pathinfo = gitweb_check_feature('pathinfo');
3552                if ($use_pathinfo) {
3553                        $action .= "/".esc_url($project);
3554                }
3555                print $cgi->startform(-method => "get", -action => $action) .
3556                      "<div class=\"search\">\n" .
3557                      (!$use_pathinfo &&
3558                      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3559                      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3560                      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3561                      $cgi->popup_menu(-name => 'st', -default => 'commit',
3562                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3563                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3564                      " search:\n",
3565                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3566                      "<span title=\"Extended regular expression\">" .
3567                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3568                                     -checked => $search_use_regexp) .
3569                      "</span>" .
3570                      "</div>" .
3571                      $cgi->end_form() . "\n";
3572        }
3573}
3574
3575sub git_footer_html {
3576        my $feed_class = 'rss_logo';
3577
3578        print "<div class=\"page_footer\">\n";
3579        if (defined $project) {
3580                my $descr = git_get_project_description($project);
3581                if (defined $descr) {
3582                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3583                }
3584
3585                my %href_params = get_feed_info();
3586                if (!%href_params) {
3587                        $feed_class .= ' generic';
3588                }
3589                $href_params{'-title'} ||= 'log';
3590
3591                foreach my $format qw(RSS Atom) {
3592                        $href_params{'action'} = lc($format);
3593                        print $cgi->a({-href => href(%href_params),
3594                                      -title => "$href_params{'-title'} $format feed",
3595                                      -class => $feed_class}, $format)."\n";
3596                }
3597
3598        } else {
3599                print $cgi->a({-href => href(project=>undef, action=>"opml"),
3600                              -class => $feed_class}, "OPML") . " ";
3601                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3602                              -class => $feed_class}, "TXT") . "\n";
3603        }
3604        print "</div>\n"; # class="page_footer"
3605
3606        if (defined $t0 && gitweb_check_feature('timed')) {
3607                print "<div id=\"generating_info\">\n";
3608                print 'This page took '.
3609                      '<span id="generating_time" class="time_span">'.
3610                      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3611                      ' seconds </span>'.
3612                      ' and '.
3613                      '<span id="generating_cmd">'.
3614                      $number_of_git_cmds.
3615                      '</span> git commands '.
3616                      " to generate.\n";
3617                print "</div>\n"; # class="page_footer"
3618        }
3619
3620        if (defined $site_footer && -f $site_footer) {
3621                insert_file($site_footer);
3622        }
3623
3624        print qq!<script type="text/javascript" src="$javascript"></script>\n!;
3625        if (defined $action &&
3626            $action eq 'blame_incremental') {
3627                print qq!<script type="text/javascript">\n!.
3628                      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3629                      qq!           "!. href() .qq!");\n!.
3630                      qq!</script>\n!;
3631        } elsif (gitweb_check_feature('javascript-actions')) {
3632                print qq!<script type="text/javascript">\n!.
3633                      qq!window.onload = fixLinks;\n!.
3634                      qq!</script>\n!;
3635        }
3636
3637        print "</body>\n" .
3638              "</html>";
3639}
3640
3641# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3642# Example: die_error(404, 'Hash not found')
3643# By convention, use the following status codes (as defined in RFC 2616):
3644# 400: Invalid or missing CGI parameters, or
3645#      requested object exists but has wrong type.
3646# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3647#      this server or project.
3648# 404: Requested object/revision/project doesn't exist.
3649# 500: The server isn't configured properly, or
3650#      an internal error occurred (e.g. failed assertions caused by bugs), or
3651#      an unknown error occurred (e.g. the git binary died unexpectedly).
3652# 503: The server is currently unavailable (because it is overloaded,
3653#      or down for maintenance).  Generally, this is a temporary state.
3654sub die_error {
3655        my $status = shift || 500;
3656        my $error = esc_html(shift) || "Internal Server Error";
3657        my $extra = shift;
3658        my %opts = @_;
3659
3660        my %http_responses = (
3661                400 => '400 Bad Request',
3662                403 => '403 Forbidden',
3663                404 => '404 Not Found',
3664                500 => '500 Internal Server Error',
3665                503 => '503 Service Unavailable',
3666        );
3667        git_header_html($http_responses{$status}, undef, %opts);
3668        print <<EOF;
3669<div class="page_body">
3670<br /><br />
3671$status - $error
3672<br />
3673EOF
3674        if (defined $extra) {
3675                print "<hr />\n" .
3676                      "$extra\n";
3677        }
3678        print "</div>\n";
3679
3680        git_footer_html();
3681        goto DONE_GITWEB
3682                unless ($opts{'-error_handler'});
3683}
3684
3685## ----------------------------------------------------------------------
3686## functions printing or outputting HTML: navigation
3687
3688sub git_print_page_nav {
3689        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3690        $extra = '' if !defined $extra; # pager or formats
3691
3692        my @navs = qw(summary shortlog log commit commitdiff tree);
3693        if ($suppress) {
3694                @navs = grep { $_ ne $suppress } @navs;
3695        }
3696
3697        my %arg = map { $_ => {action=>$_} } @navs;
3698        if (defined $head) {
3699                for (qw(commit commitdiff)) {
3700                        $arg{$_}{'hash'} = $head;
3701                }
3702                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3703                        for (qw(shortlog log)) {
3704                                $arg{$_}{'hash'} = $head;
3705                        }
3706                }
3707        }
3708
3709        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3710        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3711
3712        my @actions = gitweb_get_feature('actions');
3713        my %repl = (
3714                '%' => '%',
3715                'n' => $project,         # project name
3716                'f' => $git_dir,         # project path within filesystem
3717                'h' => $treehead || '',  # current hash ('h' parameter)
3718                'b' => $treebase || '',  # hash base ('hb' parameter)
3719        );
3720        while (@actions) {
3721                my ($label, $link, $pos) = splice(@actions,0,3);
3722                # insert
3723                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3724                # munch munch
3725                $link =~ s/%([%nfhb])/$repl{$1}/g;
3726                $arg{$label}{'_href'} = $link;
3727        }
3728
3729        print "<div class=\"page_nav\">\n" .
3730                (join " | ",
3731                 map { $_ eq $current ?
3732                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3733                 } @navs);
3734        print "<br/>\n$extra<br/>\n" .
3735              "</div>\n";
3736}
3737
3738sub format_paging_nav {
3739        my ($action, $page, $has_next_link) = @_;
3740        my $paging_nav;
3741
3742
3743        if ($page > 0) {
3744                $paging_nav .=
3745                        $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
3746                        " &sdot; " .
3747                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
3748                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3749        } else {
3750                $paging_nav .= "first &sdot; prev";
3751        }
3752
3753        if ($has_next_link) {
3754                $paging_nav .= " &sdot; " .
3755                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
3756                                 -accesskey => "n", -title => "Alt-n"}, "next");
3757        } else {
3758                $paging_nav .= " &sdot; next";
3759        }
3760
3761        return $paging_nav;
3762}
3763
3764## ......................................................................
3765## functions printing or outputting HTML: div
3766
3767sub git_print_header_div {
3768        my ($action, $title, $hash, $hash_base) = @_;
3769        my %args = ();
3770
3771        $args{'action'} = $action;
3772        $args{'hash'} = $hash if $hash;
3773        $args{'hash_base'} = $hash_base if $hash_base;
3774
3775        print "<div class=\"header\">\n" .
3776              $cgi->a({-href => href(%args), -class => "title"},
3777              $title ? $title : $action) .
3778              "\n</div>\n";
3779}
3780
3781sub print_local_time {
3782        print format_local_time(@_);
3783}
3784
3785sub format_local_time {
3786        my $localtime = '';
3787        my %date = @_;
3788        if ($date{'hour_local'} < 6) {
3789                $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3790                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3791        } else {
3792                $localtime .= sprintf(" (%02d:%02d %s)",
3793                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3794        }
3795
3796        return $localtime;
3797}
3798
3799# Outputs the author name and date in long form
3800sub git_print_authorship {
3801        my $co = shift;
3802        my %opts = @_;
3803        my $tag = $opts{-tag} || 'div';
3804        my $author = $co->{'author_name'};
3805
3806        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3807        print "<$tag class=\"author_date\">" .
3808              format_search_author($author, "author", esc_html($author)) .
3809              " [$ad{'rfc2822'}";
3810        print_local_time(%ad) if ($opts{-localtime});
3811        print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3812                  . "</$tag>\n";
3813}
3814
3815# Outputs table rows containing the full author or committer information,
3816# in the format expected for 'commit' view (& similar).
3817# Parameters are a commit hash reference, followed by the list of people
3818# to output information for. If the list is empty it defaults to both
3819# author and committer.
3820sub git_print_authorship_rows {
3821        my $co = shift;
3822        # too bad we can't use @people = @_ || ('author', 'committer')
3823        my @people = @_;
3824        @people = ('author', 'committer') unless @people;
3825        foreach my $who (@people) {
3826                my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3827                print "<tr><td>$who</td><td>" .
3828                      format_search_author($co->{"${who}_name"}, $who,
3829                               esc_html($co->{"${who}_name"})) . " " .
3830                      format_search_author($co->{"${who}_email"}, $who,
3831                               esc_html("<" . $co->{"${who}_email"} . ">")) .
3832                      "</td><td rowspan=\"2\">" .
3833                      git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3834                      "</td></tr>\n" .
3835                      "<tr>" .
3836                      "<td></td><td> $wd{'rfc2822'}";
3837                print_local_time(%wd);
3838                print "</td>" .
3839                      "</tr>\n";
3840        }
3841}
3842
3843sub git_print_page_path {
3844        my $name = shift;
3845        my $type = shift;
3846        my $hb = shift;
3847
3848
3849        print "<div class=\"page_path\">";
3850        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3851                      -title => 'tree root'}, to_utf8("[$project]"));
3852        print " / ";
3853        if (defined $name) {
3854                my @dirname = split '/', $name;
3855                my $basename = pop @dirname;
3856                my $fullname = '';
3857
3858                foreach my $dir (@dirname) {
3859                        $fullname .= ($fullname ? '/' : '') . $dir;
3860                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3861                                                     hash_base=>$hb),
3862                                      -title => $fullname}, esc_path($dir));
3863                        print " / ";
3864                }
3865                if (defined $type && $type eq 'blob') {
3866                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3867                                                     hash_base=>$hb),
3868                                      -title => $name}, esc_path($basename));
3869                } elsif (defined $type && $type eq 'tree') {
3870                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3871                                                     hash_base=>$hb),
3872                                      -title => $name}, esc_path($basename));
3873                        print " / ";
3874                } else {
3875                        print esc_path($basename);
3876                }
3877        }
3878        print "<br/></div>\n";
3879}
3880
3881sub git_print_log {
3882        my $log = shift;
3883        my %opts = @_;
3884
3885        if ($opts{'-remove_title'}) {
3886                # remove title, i.e. first line of log
3887                shift @$log;
3888        }
3889        # remove leading empty lines
3890        while (defined $log->[0] && $log->[0] eq "") {
3891                shift @$log;
3892        }
3893
3894        # print log
3895        my $signoff = 0;
3896        my $empty = 0;
3897        foreach my $line (@$log) {
3898                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3899                        $signoff = 1;
3900                        $empty = 0;
3901                        if (! $opts{'-remove_signoff'}) {
3902                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3903                                next;
3904                        } else {
3905                                # remove signoff lines
3906                                next;
3907                        }
3908                } else {
3909                        $signoff = 0;
3910                }
3911
3912                # print only one empty line
3913                # do not print empty line after signoff
3914                if ($line eq "") {
3915                        next if ($empty || $signoff);
3916                        $empty = 1;
3917                } else {
3918                        $empty = 0;
3919                }
3920
3921                print format_log_line_html($line) . "<br/>\n";
3922        }
3923
3924        if ($opts{'-final_empty_line'}) {
3925                # end with single empty line
3926                print "<br/>\n" unless $empty;
3927        }
3928}
3929
3930# return link target (what link points to)
3931sub git_get_link_target {
3932        my $hash = shift;
3933        my $link_target;
3934
3935        # read link
3936        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3937                or return;
3938        {
3939                local $/ = undef;
3940                $link_target = <$fd>;
3941        }
3942        close $fd
3943                or return;
3944
3945        return $link_target;
3946}
3947
3948# given link target, and the directory (basedir) the link is in,
3949# return target of link relative to top directory (top tree);
3950# return undef if it is not possible (including absolute links).
3951sub normalize_link_target {
3952        my ($link_target, $basedir) = @_;
3953
3954        # absolute symlinks (beginning with '/') cannot be normalized
3955        return if (substr($link_target, 0, 1) eq '/');
3956
3957        # normalize link target to path from top (root) tree (dir)
3958        my $path;
3959        if ($basedir) {
3960                $path = $basedir . '/' . $link_target;
3961        } else {
3962                # we are in top (root) tree (dir)
3963                $path = $link_target;
3964        }
3965
3966        # remove //, /./, and /../
3967        my @path_parts;
3968        foreach my $part (split('/', $path)) {
3969                # discard '.' and ''
3970                next if (!$part || $part eq '.');
3971                # handle '..'
3972                if ($part eq '..') {
3973                        if (@path_parts) {
3974                                pop @path_parts;
3975                        } else {
3976                                # link leads outside repository (outside top dir)
3977                                return;
3978                        }
3979                } else {
3980                        push @path_parts, $part;
3981                }
3982        }
3983        $path = join('/', @path_parts);
3984
3985        return $path;
3986}
3987
3988# print tree entry (row of git_tree), but without encompassing <tr> element
3989sub git_print_tree_entry {
3990        my ($t, $basedir, $hash_base, $have_blame) = @_;
3991
3992        my %base_key = ();
3993        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3994
3995        # The format of a table row is: mode list link.  Where mode is
3996        # the mode of the entry, list is the name of the entry, an href,
3997        # and link is the action links of the entry.
3998
3999        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4000        if (exists $t->{'size'}) {
4001                print "<td class=\"size\">$t->{'size'}</td>\n";
4002        }
4003        if ($t->{'type'} eq "blob") {
4004                print "<td class=\"list\">" .
4005                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4006                                               file_name=>"$basedir$t->{'name'}", %base_key),
4007                                -class => "list"}, esc_path($t->{'name'}));
4008                if (S_ISLNK(oct $t->{'mode'})) {
4009                        my $link_target = git_get_link_target($t->{'hash'});
4010                        if ($link_target) {
4011                                my $norm_target = normalize_link_target($link_target, $basedir);
4012                                if (defined $norm_target) {
4013                                        print " -> " .
4014                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4015                                                                     file_name=>$norm_target),
4016                                                       -title => $norm_target}, esc_path($link_target));
4017                                } else {
4018                                        print " -> " . esc_path($link_target);
4019                                }
4020                        }
4021                }
4022                print "</td>\n";
4023                print "<td class=\"link\">";
4024                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4025                                             file_name=>"$basedir$t->{'name'}", %base_key)},
4026                              "blob");
4027                if ($have_blame) {
4028                        print " | " .
4029                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4030                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
4031                                      "blame");
4032                }
4033                if (defined $hash_base) {
4034                        print " | " .
4035                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4036                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4037                                      "history");
4038                }
4039                print " | " .
4040                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4041                                               file_name=>"$basedir$t->{'name'}")},
4042                                "raw");
4043                print "</td>\n";
4044
4045        } elsif ($t->{'type'} eq "tree") {
4046                print "<td class=\"list\">";
4047                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4048                                             file_name=>"$basedir$t->{'name'}",
4049                                             %base_key)},
4050                              esc_path($t->{'name'}));
4051                print "</td>\n";
4052                print "<td class=\"link\">";
4053                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4054                                             file_name=>"$basedir$t->{'name'}",
4055                                             %base_key)},
4056                              "tree");
4057                if (defined $hash_base) {
4058                        print " | " .
4059                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4060                                                     file_name=>"$basedir$t->{'name'}")},
4061                                      "history");
4062                }
4063                print "</td>\n";
4064        } else {
4065                # unknown object: we can only present history for it
4066                # (this includes 'commit' object, i.e. submodule support)
4067                print "<td class=\"list\">" .
4068                      esc_path($t->{'name'}) .
4069                      "</td>\n";
4070                print "<td class=\"link\">";
4071                if (defined $hash_base) {
4072                        print $cgi->a({-href => href(action=>"history",
4073                                                     hash_base=>$hash_base,
4074                                                     file_name=>"$basedir$t->{'name'}")},
4075                                      "history");
4076                }
4077                print "</td>\n";
4078        }
4079}
4080
4081## ......................................................................
4082## functions printing large fragments of HTML
4083
4084# get pre-image filenames for merge (combined) diff
4085sub fill_from_file_info {
4086        my ($diff, @parents) = @_;
4087
4088        $diff->{'from_file'} = [ ];
4089        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4090        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4091                if ($diff->{'status'}[$i] eq 'R' ||
4092                    $diff->{'status'}[$i] eq 'C') {
4093                        $diff->{'from_file'}[$i] =
4094                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4095                }
4096        }
4097
4098        return $diff;
4099}
4100
4101# is current raw difftree line of file deletion
4102sub is_deleted {
4103        my $diffinfo = shift;
4104
4105        return $diffinfo->{'to_id'} eq ('0' x 40);
4106}
4107
4108# does patch correspond to [previous] difftree raw line
4109# $diffinfo  - hashref of parsed raw diff format
4110# $patchinfo - hashref of parsed patch diff format
4111#              (the same keys as in $diffinfo)
4112sub is_patch_split {
4113        my ($diffinfo, $patchinfo) = @_;
4114
4115        return defined $diffinfo && defined $patchinfo
4116                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4117}
4118
4119
4120sub git_difftree_body {
4121        my ($difftree, $hash, @parents) = @_;
4122        my ($parent) = $parents[0];
4123        my $have_blame = gitweb_check_feature('blame');
4124        print "<div class=\"list_head\">\n";
4125        if ($#{$difftree} > 10) {
4126                print(($#{$difftree} + 1) . " files changed:\n");
4127        }
4128        print "</div>\n";
4129
4130        print "<table class=\"" .
4131              (@parents > 1 ? "combined " : "") .
4132              "diff_tree\">\n";
4133
4134        # header only for combined diff in 'commitdiff' view
4135        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4136        if ($has_header) {
4137                # table header
4138                print "<thead><tr>\n" .
4139                       "<th></th><th></th>\n"; # filename, patchN link
4140                for (my $i = 0; $i < @parents; $i++) {
4141                        my $par = $parents[$i];
4142                        print "<th>" .
4143                              $cgi->a({-href => href(action=>"commitdiff",
4144                                                     hash=>$hash, hash_parent=>$par),
4145                                       -title => 'commitdiff to parent number ' .
4146                                                  ($i+1) . ': ' . substr($par,0,7)},
4147                                      $i+1) .
4148                              "&nbsp;</th>\n";
4149                }
4150                print "</tr></thead>\n<tbody>\n";
4151        }
4152
4153        my $alternate = 1;
4154        my $patchno = 0;
4155        foreach my $line (@{$difftree}) {
4156                my $diff = parsed_difftree_line($line);
4157
4158                if ($alternate) {
4159                        print "<tr class=\"dark\">\n";
4160                } else {
4161                        print "<tr class=\"light\">\n";
4162                }
4163                $alternate ^= 1;
4164
4165                if (exists $diff->{'nparents'}) { # combined diff
4166
4167                        fill_from_file_info($diff, @parents)
4168                                unless exists $diff->{'from_file'};
4169
4170                        if (!is_deleted($diff)) {
4171                                # file exists in the result (child) commit
4172                                print "<td>" .
4173                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4174                                                             file_name=>$diff->{'to_file'},
4175                                                             hash_base=>$hash),
4176                                              -class => "list"}, esc_path($diff->{'to_file'})) .
4177                                      "</td>\n";
4178                        } else {
4179                                print "<td>" .
4180                                      esc_path($diff->{'to_file'}) .
4181                                      "</td>\n";
4182                        }
4183
4184                        if ($action eq 'commitdiff') {
4185                                # link to patch
4186                                $patchno++;
4187                                print "<td class=\"link\">" .
4188                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
4189                                      " | " .
4190                                      "</td>\n";
4191                        }
4192
4193                        my $has_history = 0;
4194                        my $not_deleted = 0;
4195                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4196                                my $hash_parent = $parents[$i];
4197                                my $from_hash = $diff->{'from_id'}[$i];
4198                                my $from_path = $diff->{'from_file'}[$i];
4199                                my $status = $diff->{'status'}[$i];
4200
4201                                $has_history ||= ($status ne 'A');
4202                                $not_deleted ||= ($status ne 'D');
4203
4204                                if ($status eq 'A') {
4205                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
4206                                } elsif ($status eq 'D') {
4207                                        print "<td class=\"link\">" .
4208                                              $cgi->a({-href => href(action=>"blob",
4209                                                                     hash_base=>$hash,
4210                                                                     hash=>$from_hash,
4211                                                                     file_name=>$from_path)},
4212                                                      "blob" . ($i+1)) .
4213                                              " | </td>\n";
4214                                } else {
4215                                        if ($diff->{'to_id'} eq $from_hash) {
4216                                                print "<td class=\"link nochange\">";
4217                                        } else {
4218                                                print "<td class=\"link\">";
4219                                        }
4220                                        print $cgi->a({-href => href(action=>"blobdiff",
4221                                                                     hash=>$diff->{'to_id'},
4222                                                                     hash_parent=>$from_hash,
4223                                                                     hash_base=>$hash,
4224                                                                     hash_parent_base=>$hash_parent,
4225                                                                     file_name=>$diff->{'to_file'},
4226                                                                     file_parent=>$from_path)},
4227                                                      "diff" . ($i+1)) .
4228                                              " | </td>\n";
4229                                }
4230                        }
4231
4232                        print "<td class=\"link\">";
4233                        if ($not_deleted) {
4234                                print $cgi->a({-href => href(action=>"blob",
4235                                                             hash=>$diff->{'to_id'},
4236                                                             file_name=>$diff->{'to_file'},
4237                                                             hash_base=>$hash)},
4238                                              "blob");
4239                                print " | " if ($has_history);
4240                        }
4241                        if ($has_history) {
4242                                print $cgi->a({-href => href(action=>"history",
4243                                                             file_name=>$diff->{'to_file'},
4244                                                             hash_base=>$hash)},
4245                                              "history");
4246                        }
4247                        print "</td>\n";
4248
4249                        print "</tr>\n";
4250                        next; # instead of 'else' clause, to avoid extra indent
4251                }
4252                # else ordinary diff
4253
4254                my ($to_mode_oct, $to_mode_str, $to_file_type);
4255                my ($from_mode_oct, $from_mode_str, $from_file_type);
4256                if ($diff->{'to_mode'} ne ('0' x 6)) {
4257                        $to_mode_oct = oct $diff->{'to_mode'};
4258                        if (S_ISREG($to_mode_oct)) { # only for regular file
4259                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4260                        }
4261                        $to_file_type = file_type($diff->{'to_mode'});
4262                }
4263                if ($diff->{'from_mode'} ne ('0' x 6)) {
4264                        $from_mode_oct = oct $diff->{'from_mode'};
4265                        if (S_ISREG($to_mode_oct)) { # only for regular file
4266                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4267                        }
4268                        $from_file_type = file_type($diff->{'from_mode'});
4269                }
4270
4271                if ($diff->{'status'} eq "A") { # created
4272                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4273                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
4274                        $mode_chng   .= "]</span>";
4275                        print "<td>";
4276                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4277                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
4278                                      -class => "list"}, esc_path($diff->{'file'}));
4279                        print "</td>\n";
4280                        print "<td>$mode_chng</td>\n";
4281                        print "<td class=\"link\">";
4282                        if ($action eq 'commitdiff') {
4283                                # link to patch
4284                                $patchno++;
4285                                print $cgi->a({-href => "#patch$patchno"}, "patch");
4286                                print " | ";
4287                        }
4288                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4289                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
4290                                      "blob");
4291                        print "</td>\n";
4292
4293                } elsif ($diff->{'status'} eq "D") { # deleted
4294                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4295                        print "<td>";
4296                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4297                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
4298                                       -class => "list"}, esc_path($diff->{'file'}));
4299                        print "</td>\n";
4300                        print "<td>$mode_chng</td>\n";
4301                        print "<td class=\"link\">";
4302                        if ($action eq 'commitdiff') {
4303                                # link to patch
4304                                $patchno++;
4305                                print $cgi->a({-href => "#patch$patchno"}, "patch");
4306                                print " | ";
4307                        }
4308                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4309                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
4310                                      "blob") . " | ";
4311                        if ($have_blame) {
4312                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4313                                                             file_name=>$diff->{'file'})},
4314                                              "blame") . " | ";
4315                        }
4316                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4317                                                     file_name=>$diff->{'file'})},
4318                                      "history");
4319                        print "</td>\n";
4320
4321                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4322                        my $mode_chnge = "";
4323                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4324                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4325                                if ($from_file_type ne $to_file_type) {
4326                                        $mode_chnge .= " from $from_file_type to $to_file_type";
4327                                }
4328                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4329                                        if ($from_mode_str && $to_mode_str) {
4330                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4331                                        } elsif ($to_mode_str) {
4332                                                $mode_chnge .= " mode: $to_mode_str";
4333                                        }
4334                                }
4335                                $mode_chnge .= "]</span>\n";
4336                        }
4337                        print "<td>";
4338                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4339                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
4340                                      -class => "list"}, esc_path($diff->{'file'}));
4341                        print "</td>\n";
4342                        print "<td>$mode_chnge</td>\n";
4343                        print "<td class=\"link\">";
4344                        if ($action eq 'commitdiff') {
4345                                # link to patch
4346                                $patchno++;
4347                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
4348                                      " | ";
4349                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4350                                # "commit" view and modified file (not onlu mode changed)
4351                                print $cgi->a({-href => href(action=>"blobdiff",
4352                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4353                                                             hash_base=>$hash, hash_parent_base=>$parent,
4354                                                             file_name=>$diff->{'file'})},
4355                                              "diff") .
4356                                      " | ";
4357                        }
4358                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4359                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
4360                                       "blob") . " | ";
4361                        if ($have_blame) {
4362                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4363                                                             file_name=>$diff->{'file'})},
4364                                              "blame") . " | ";
4365                        }
4366                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4367                                                     file_name=>$diff->{'file'})},
4368                                      "history");
4369                        print "</td>\n";
4370
4371                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4372                        my %status_name = ('R' => 'moved', 'C' => 'copied');
4373                        my $nstatus = $status_name{$diff->{'status'}};
4374                        my $mode_chng = "";
4375                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4376                                # mode also for directories, so we cannot use $to_mode_str
4377                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4378                        }
4379                        print "<td>" .
4380                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4381                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4382                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4383                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4384                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4385                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4386                                      -class => "list"}, esc_path($diff->{'from_file'})) .
4387                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4388                              "<td class=\"link\">";
4389                        if ($action eq 'commitdiff') {
4390                                # link to patch
4391                                $patchno++;
4392                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
4393                                      " | ";
4394                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4395                                # "commit" view and modified file (not only pure rename or copy)
4396                                print $cgi->a({-href => href(action=>"blobdiff",
4397                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4398                                                             hash_base=>$hash, hash_parent_base=>$parent,
4399                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4400                                              "diff") .
4401                                      " | ";
4402                        }
4403                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4404                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
4405                                      "blob") . " | ";
4406                        if ($have_blame) {
4407                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4408                                                             file_name=>$diff->{'to_file'})},
4409                                              "blame") . " | ";
4410                        }
4411                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4412                                                    file_name=>$diff->{'to_file'})},
4413                                      "history");
4414                        print "</td>\n";
4415
4416                } # we should not encounter Unmerged (U) or Unknown (X) status
4417                print "</tr>\n";
4418        }
4419        print "</tbody>" if $has_header;
4420        print "</table>\n";
4421}
4422
4423sub git_patchset_body {
4424        my ($fd, $difftree, $hash, @hash_parents) = @_;
4425        my ($hash_parent) = $hash_parents[0];
4426
4427        my $is_combined = (@hash_parents > 1);
4428        my $patch_idx = 0;
4429        my $patch_number = 0;
4430        my $patch_line;
4431        my $diffinfo;
4432        my $to_name;
4433        my (%from, %to);
4434
4435        print "<div class=\"patchset\">\n";
4436
4437        # skip to first patch
4438        while ($patch_line = <$fd>) {
4439                chomp $patch_line;
4440
4441                last if ($patch_line =~ m/^diff /);
4442        }
4443
4444 PATCH:
4445        while ($patch_line) {
4446
4447                # parse "git diff" header line
4448                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4449                        # $1 is from_name, which we do not use
4450                        $to_name = unquote($2);
4451                        $to_name =~ s!^b/!!;
4452                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4453                        # $1 is 'cc' or 'combined', which we do not use
4454                        $to_name = unquote($2);
4455                } else {
4456                        $to_name = undef;
4457                }
4458
4459                # check if current patch belong to current raw line
4460                # and parse raw git-diff line if needed
4461                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4462                        # this is continuation of a split patch
4463                        print "<div class=\"patch cont\">\n";
4464                } else {
4465                        # advance raw git-diff output if needed
4466                        $patch_idx++ if defined $diffinfo;
4467
4468                        # read and prepare patch information
4469                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4470
4471                        # compact combined diff output can have some patches skipped
4472                        # find which patch (using pathname of result) we are at now;
4473                        if ($is_combined) {
4474                                while ($to_name ne $diffinfo->{'to_file'}) {
4475                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4476                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
4477                                              "</div>\n";  # class="patch"
4478
4479                                        $patch_idx++;
4480                                        $patch_number++;
4481
4482                                        last if $patch_idx > $#$difftree;
4483                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4484                                }
4485                        }
4486
4487                        # modifies %from, %to hashes
4488                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4489
4490                        # this is first patch for raw difftree line with $patch_idx index
4491                        # we index @$difftree array from 0, but number patches from 1
4492                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4493                }
4494
4495                # git diff header
4496                #assert($patch_line =~ m/^diff /) if DEBUG;
4497                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4498                $patch_number++;
4499                # print "git diff" header
4500                print format_git_diff_header_line($patch_line, $diffinfo,
4501                                                  \%from, \%to);
4502
4503                # print extended diff header
4504                print "<div class=\"diff extended_header\">\n";
4505        EXTENDED_HEADER:
4506                while ($patch_line = <$fd>) {
4507                        chomp $patch_line;
4508
4509                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4510
4511                        print format_extended_diff_header_line($patch_line, $diffinfo,
4512                                                               \%from, \%to);
4513                }
4514                print "</div>\n"; # class="diff extended_header"
4515
4516                # from-file/to-file diff header
4517                if (! $patch_line) {
4518                        print "</div>\n"; # class="patch"
4519                        last PATCH;
4520                }
4521                next PATCH if ($patch_line =~ m/^diff /);
4522                #assert($patch_line =~ m/^---/) if DEBUG;
4523
4524                my $last_patch_line = $patch_line;
4525                $patch_line = <$fd>;
4526                chomp $patch_line;
4527                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4528
4529                print format_diff_from_to_header($last_patch_line, $patch_line,
4530                                                 $diffinfo, \%from, \%to,
4531                                                 @hash_parents);
4532
4533                # the patch itself
4534        LINE:
4535                while ($patch_line = <$fd>) {
4536                        chomp $patch_line;
4537
4538                        next PATCH if ($patch_line =~ m/^diff /);
4539
4540                        print format_diff_line($patch_line, \%from, \%to);
4541                }
4542
4543        } continue {
4544                print "</div>\n"; # class="patch"
4545        }
4546
4547        # for compact combined (--cc) format, with chunk and patch simplification
4548        # the patchset might be empty, but there might be unprocessed raw lines
4549        for (++$patch_idx if $patch_number > 0;
4550             $patch_idx < @$difftree;
4551             ++$patch_idx) {
4552                # read and prepare patch information
4553                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4554
4555                # generate anchor for "patch" links in difftree / whatchanged part
4556                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4557                      format_diff_cc_simplified($diffinfo, @hash_parents) .
4558                      "</div>\n";  # class="patch"
4559
4560                $patch_number++;
4561        }
4562
4563        if ($patch_number == 0) {
4564                if (@hash_parents > 1) {
4565                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4566                } else {
4567                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
4568                }
4569        }
4570
4571        print "</div>\n"; # class="patchset"
4572}
4573
4574# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4575
4576# fills project list info (age, description, owner, forks) for each
4577# project in the list, removing invalid projects from returned list
4578# NOTE: modifies $projlist, but does not remove entries from it
4579sub fill_project_list_info {
4580        my ($projlist, $check_forks) = @_;
4581        my @projects;
4582
4583        my $show_ctags = gitweb_check_feature('ctags');
4584 PROJECT:
4585        foreach my $pr (@$projlist) {
4586                my (@activity) = git_get_last_activity($pr->{'path'});
4587                unless (@activity) {
4588                        next PROJECT;
4589                }
4590                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4591                if (!defined $pr->{'descr'}) {
4592                        my $descr = git_get_project_description($pr->{'path'}) || "";
4593                        $descr = to_utf8($descr);
4594                        $pr->{'descr_long'} = $descr;
4595                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4596                }
4597                if (!defined $pr->{'owner'}) {
4598                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4599                }
4600                if ($check_forks) {
4601                        my $pname = $pr->{'path'};
4602                        if (($pname =~ s/\.git$//) &&
4603                            ($pname !~ /\/$/) &&
4604                            (-d "$projectroot/$pname")) {
4605                                $pr->{'forks'} = "-d $projectroot/$pname";
4606                        } else {
4607                                $pr->{'forks'} = 0;
4608                        }
4609                }
4610                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4611                push @projects, $pr;
4612        }
4613
4614        return @projects;
4615}
4616
4617# print 'sort by' <th> element, generating 'sort by $name' replay link
4618# if that order is not selected
4619sub print_sort_th {
4620        print format_sort_th(@_);
4621}
4622
4623sub format_sort_th {
4624        my ($name, $order, $header) = @_;
4625        my $sort_th = "";
4626        $header ||= ucfirst($name);
4627
4628        if ($order eq $name) {
4629                $sort_th .= "<th>$header</th>\n";
4630        } else {
4631                $sort_th .= "<th>" .
4632                            $cgi->a({-href => href(-replay=>1, order=>$name),
4633                                     -class => "header"}, $header) .
4634                            "</th>\n";
4635        }
4636
4637        return $sort_th;
4638}
4639
4640sub git_project_list_body {
4641        # actually uses global variable $project
4642        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4643
4644        my $check_forks = gitweb_check_feature('forks');
4645        my @projects = fill_project_list_info($projlist, $check_forks);
4646
4647        $order ||= $default_projects_order;
4648        $from = 0 unless defined $from;
4649        $to = $#projects if (!defined $to || $#projects < $to);
4650
4651        my %order_info = (
4652                project => { key => 'path', type => 'str' },
4653                descr => { key => 'descr_long', type => 'str' },
4654                owner => { key => 'owner', type => 'str' },
4655                age => { key => 'age', type => 'num' }
4656        );
4657        my $oi = $order_info{$order};
4658        if ($oi->{'type'} eq 'str') {
4659                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4660        } else {
4661                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4662        }
4663
4664        my $show_ctags = gitweb_check_feature('ctags');
4665        if ($show_ctags) {
4666                my %ctags;
4667                foreach my $p (@projects) {
4668                        foreach my $ct (keys %{$p->{'ctags'}}) {
4669                                $ctags{$ct} += $p->{'ctags'}->{$ct};
4670                        }
4671                }
4672                my $cloud = git_populate_project_tagcloud(\%ctags);
4673                print git_show_project_tagcloud($cloud, 64);
4674        }
4675
4676        print "<table class=\"project_list\">\n";
4677        unless ($no_header) {
4678                print "<tr>\n";
4679                if ($check_forks) {
4680                        print "<th></th>\n";
4681                }
4682                print_sort_th('project', $order, 'Project');
4683                print_sort_th('descr', $order, 'Description');
4684                print_sort_th('owner', $order, 'Owner');
4685                print_sort_th('age', $order, 'Last Change');
4686                print "<th></th>\n" . # for links
4687                      "</tr>\n";
4688        }
4689        my $alternate = 1;
4690        my $tagfilter = $cgi->param('by_tag');
4691        for (my $i = $from; $i <= $to; $i++) {
4692                my $pr = $projects[$i];
4693
4694                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4695                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4696                        and not $pr->{'descr_long'} =~ /$searchtext/;
4697                # Weed out forks or non-matching entries of search
4698                if ($check_forks) {
4699                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4700                        $forkbase="^$forkbase" if $forkbase;
4701                        next if not $searchtext and not $tagfilter and $show_ctags
4702                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4703                }
4704
4705                if ($alternate) {
4706                        print "<tr class=\"dark\">\n";
4707                } else {
4708                        print "<tr class=\"light\">\n";
4709                }
4710                $alternate ^= 1;
4711                if ($check_forks) {
4712                        print "<td>";
4713                        if ($pr->{'forks'}) {
4714                                print "<!-- $pr->{'forks'} -->\n";
4715                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4716                        }
4717                        print "</td>\n";
4718                }
4719                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4720                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4721                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4722                                        -class => "list", -title => $pr->{'descr_long'}},
4723                                        esc_html($pr->{'descr'})) . "</td>\n" .
4724                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4725                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4726                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4727                      "<td class=\"link\">" .
4728                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4729                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4730                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4731                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4732                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4733                      "</td>\n" .
4734                      "</tr>\n";
4735        }
4736        if (defined $extra) {
4737                print "<tr>\n";
4738                if ($check_forks) {
4739                        print "<td></td>\n";
4740                }
4741                print "<td colspan=\"5\">$extra</td>\n" .
4742                      "</tr>\n";
4743        }
4744        print "</table>\n";
4745}
4746
4747sub git_log_body {
4748        # uses global variable $project
4749        my ($commitlist, $from, $to, $refs, $extra) = @_;
4750
4751        $from = 0 unless defined $from;
4752        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4753
4754        for (my $i = 0; $i <= $to; $i++) {
4755                my %co = %{$commitlist->[$i]};
4756                next if !%co;
4757                my $commit = $co{'id'};
4758                my $ref = format_ref_marker($refs, $commit);
4759                my %ad = parse_date($co{'author_epoch'});
4760                git_print_header_div('commit',
4761                               "<span class=\"age\">$co{'age_string'}</span>" .
4762                               esc_html($co{'title'}) . $ref,
4763                               $commit);
4764                print "<div class=\"title_text\">\n" .
4765                      "<div class=\"log_link\">\n" .
4766                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4767                      " | " .
4768                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4769                      " | " .
4770                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4771                      "<br/>\n" .
4772                      "</div>\n";
4773                      git_print_authorship(\%co, -tag => 'span');
4774                      print "<br/>\n</div>\n";
4775
4776                print "<div class=\"log_body\">\n";
4777                git_print_log($co{'comment'}, -final_empty_line=> 1);
4778                print "</div>\n";
4779        }
4780        if ($extra) {
4781                print "<div class=\"page_nav\">\n";
4782                print "$extra\n";
4783                print "</div>\n";
4784        }
4785}
4786
4787sub git_shortlog_body {
4788        # uses global variable $project
4789        my ($commitlist, $from, $to, $refs, $extra) = @_;
4790
4791        $from = 0 unless defined $from;
4792        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4793
4794        print "<table class=\"shortlog\">\n";
4795        my $alternate = 1;
4796        for (my $i = $from; $i <= $to; $i++) {
4797                my %co = %{$commitlist->[$i]};
4798                my $commit = $co{'id'};
4799                my $ref = format_ref_marker($refs, $commit);
4800                if ($alternate) {
4801                        print "<tr class=\"dark\">\n";
4802                } else {
4803                        print "<tr class=\"light\">\n";
4804                }
4805                $alternate ^= 1;
4806                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4807                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4808                      format_author_html('td', \%co, 10) . "<td>";
4809                print format_subject_html($co{'title'}, $co{'title_short'},
4810                                          href(action=>"commit", hash=>$commit), $ref);
4811                print "</td>\n" .
4812                      "<td class=\"link\">" .
4813                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4814                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4815                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4816                my $snapshot_links = format_snapshot_links($commit);
4817                if (defined $snapshot_links) {
4818                        print " | " . $snapshot_links;
4819                }
4820                print "</td>\n" .
4821                      "</tr>\n";
4822        }
4823        if (defined $extra) {
4824                print "<tr>\n" .
4825                      "<td colspan=\"4\">$extra</td>\n" .
4826                      "</tr>\n";
4827        }
4828        print "</table>\n";
4829}
4830
4831sub git_history_body {
4832        # Warning: assumes constant type (blob or tree) during history
4833        my ($commitlist, $from, $to, $refs, $extra,
4834            $file_name, $file_hash, $ftype) = @_;
4835
4836        $from = 0 unless defined $from;
4837        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4838
4839        print "<table class=\"history\">\n";
4840        my $alternate = 1;
4841        for (my $i = $from; $i <= $to; $i++) {
4842                my %co = %{$commitlist->[$i]};
4843                if (!%co) {
4844                        next;
4845                }
4846                my $commit = $co{'id'};
4847
4848                my $ref = format_ref_marker($refs, $commit);
4849
4850                if ($alternate) {
4851                        print "<tr class=\"dark\">\n";
4852                } else {
4853                        print "<tr class=\"light\">\n";
4854                }
4855                $alternate ^= 1;
4856                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4857        # shortlog:   format_author_html('td', \%co, 10)
4858                      format_author_html('td', \%co, 15, 3) . "<td>";
4859                # originally git_history used chop_str($co{'title'}, 50)
4860                print format_subject_html($co{'title'}, $co{'title_short'},
4861                                          href(action=>"commit", hash=>$commit), $ref);
4862                print "</td>\n" .
4863                      "<td class=\"link\">" .
4864                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4865                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4866
4867                if ($ftype eq 'blob') {
4868                        my $blob_current = $file_hash;
4869                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4870                        if (defined $blob_current && defined $blob_parent &&
4871                                        $blob_current ne $blob_parent) {
4872                                print " | " .
4873                                        $cgi->a({-href => href(action=>"blobdiff",
4874                                                               hash=>$blob_current, hash_parent=>$blob_parent,
4875                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
4876                                                               file_name=>$file_name)},
4877                                                "diff to current");
4878                        }
4879                }
4880                print "</td>\n" .
4881                      "</tr>\n";
4882        }
4883        if (defined $extra) {
4884                print "<tr>\n" .
4885                      "<td colspan=\"4\">$extra</td>\n" .
4886                      "</tr>\n";
4887        }
4888        print "</table>\n";
4889}
4890
4891sub git_tags_body {
4892        # uses global variable $project
4893        my ($taglist, $from, $to, $extra) = @_;
4894        $from = 0 unless defined $from;
4895        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4896
4897        print "<table class=\"tags\">\n";
4898        my $alternate = 1;
4899        for (my $i = $from; $i <= $to; $i++) {
4900                my $entry = $taglist->[$i];
4901                my %tag = %$entry;
4902                my $comment = $tag{'subject'};
4903                my $comment_short;
4904                if (defined $comment) {
4905                        $comment_short = chop_str($comment, 30, 5);
4906                }
4907                if ($alternate) {
4908                        print "<tr class=\"dark\">\n";
4909                } else {
4910                        print "<tr class=\"light\">\n";
4911                }
4912                $alternate ^= 1;
4913                if (defined $tag{'age'}) {
4914                        print "<td><i>$tag{'age'}</i></td>\n";
4915                } else {
4916                        print "<td></td>\n";
4917                }
4918                print "<td>" .
4919                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4920                               -class => "list name"}, esc_html($tag{'name'})) .
4921                      "</td>\n" .
4922                      "<td>";
4923                if (defined $comment) {
4924                        print format_subject_html($comment, $comment_short,
4925                                                  href(action=>"tag", hash=>$tag{'id'}));
4926                }
4927                print "</td>\n" .
4928                      "<td class=\"selflink\">";
4929                if ($tag{'type'} eq "tag") {
4930                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4931                } else {
4932                        print "&nbsp;";
4933                }
4934                print "</td>\n" .
4935                      "<td class=\"link\">" . " | " .
4936                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4937                if ($tag{'reftype'} eq "commit") {
4938                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4939                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4940                } elsif ($tag{'reftype'} eq "blob") {
4941                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4942                }
4943                print "</td>\n" .
4944                      "</tr>";
4945        }
4946        if (defined $extra) {
4947                print "<tr>\n" .
4948                      "<td colspan=\"5\">$extra</td>\n" .
4949                      "</tr>\n";
4950        }
4951        print "</table>\n";
4952}
4953
4954sub git_heads_body {
4955        # uses global variable $project
4956        my ($headlist, $head, $from, $to, $extra) = @_;
4957        $from = 0 unless defined $from;
4958        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4959
4960        print "<table class=\"heads\">\n";
4961        my $alternate = 1;
4962        for (my $i = $from; $i <= $to; $i++) {
4963                my $entry = $headlist->[$i];
4964                my %ref = %$entry;
4965                my $curr = $ref{'id'} eq $head;
4966                if ($alternate) {
4967                        print "<tr class=\"dark\">\n";
4968                } else {
4969                        print "<tr class=\"light\">\n";
4970                }
4971                $alternate ^= 1;
4972                print "<td><i>$ref{'age'}</i></td>\n" .
4973                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4974                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4975                               -class => "list name"},esc_html($ref{'name'})) .
4976                      "</td>\n" .
4977                      "<td class=\"link\">" .
4978                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4979                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4980                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
4981                      "</td>\n" .
4982                      "</tr>";
4983        }
4984        if (defined $extra) {
4985                print "<tr>\n" .
4986                      "<td colspan=\"3\">$extra</td>\n" .
4987                      "</tr>\n";
4988        }
4989        print "</table>\n";
4990}
4991
4992sub git_search_grep_body {
4993        my ($commitlist, $from, $to, $extra) = @_;
4994        $from = 0 unless defined $from;
4995        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4996
4997        print "<table class=\"commit_search\">\n";
4998        my $alternate = 1;
4999        for (my $i = $from; $i <= $to; $i++) {
5000                my %co = %{$commitlist->[$i]};
5001                if (!%co) {
5002                        next;
5003                }
5004                my $commit = $co{'id'};
5005                if ($alternate) {
5006                        print "<tr class=\"dark\">\n";
5007                } else {
5008                        print "<tr class=\"light\">\n";
5009                }
5010                $alternate ^= 1;
5011                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5012                      format_author_html('td', \%co, 15, 5) .
5013                      "<td>" .
5014                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5015                               -class => "list subject"},
5016                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5017                my $comment = $co{'comment'};
5018                foreach my $line (@$comment) {
5019                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
5020                                my ($lead, $match, $trail) = ($1, $2, $3);
5021                                $match = chop_str($match, 70, 5, 'center');
5022                                my $contextlen = int((80 - length($match))/2);
5023                                $contextlen = 30 if ($contextlen > 30);
5024                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
5025                                $trail = chop_str($trail, $contextlen, 10, 'right');
5026
5027                                $lead  = esc_html($lead);
5028                                $match = esc_html($match);
5029                                $trail = esc_html($trail);
5030
5031                                print "$lead<span class=\"match\">$match</span>$trail<br />";
5032                        }
5033                }
5034                print "</td>\n" .
5035                      "<td class=\"link\">" .
5036                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5037                      " | " .
5038                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5039                      " | " .
5040                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5041                print "</td>\n" .
5042                      "</tr>\n";
5043        }
5044        if (defined $extra) {
5045                print "<tr>\n" .
5046                      "<td colspan=\"3\">$extra</td>\n" .
5047                      "</tr>\n";
5048        }
5049        print "</table>\n";
5050}
5051
5052## ======================================================================
5053## ======================================================================
5054## actions
5055
5056sub git_project_list {
5057        my $order = $input_params{'order'};
5058        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5059                die_error(400, "Unknown order parameter");
5060        }
5061
5062        my @list = git_get_projects_list();
5063        if (!@list) {
5064                die_error(404, "No projects found");
5065        }
5066
5067        git_header_html();
5068        if (defined $home_text && -f $home_text) {
5069                print "<div class=\"index_include\">\n";
5070                insert_file($home_text);
5071                print "</div>\n";
5072        }
5073        print $cgi->startform(-method => "get") .
5074              "<p class=\"projsearch\">Search:\n" .
5075              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5076              "</p>" .
5077              $cgi->end_form() . "\n";
5078        git_project_list_body(\@list, $order);
5079        git_footer_html();
5080}
5081
5082sub git_forks {
5083        my $order = $input_params{'order'};
5084        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5085                die_error(400, "Unknown order parameter");
5086        }
5087
5088        my @list = git_get_projects_list($project);
5089        if (!@list) {
5090                die_error(404, "No forks found");
5091        }
5092
5093        git_header_html();
5094        git_print_page_nav('','');
5095        git_print_header_div('summary', "$project forks");
5096        git_project_list_body(\@list, $order);
5097        git_footer_html();
5098}
5099
5100sub git_project_index {
5101        my @projects = git_get_projects_list($project);
5102
5103        print $cgi->header(
5104                -type => 'text/plain',
5105                -charset => 'utf-8',
5106                -content_disposition => 'inline; filename="index.aux"');
5107
5108        foreach my $pr (@projects) {
5109                if (!exists $pr->{'owner'}) {
5110                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5111                }
5112
5113                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5114                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5115                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5116                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5117                $path  =~ s/ /\+/g;
5118                $owner =~ s/ /\+/g;
5119
5120                print "$path $owner\n";
5121        }
5122}
5123
5124sub git_summary {
5125        my $descr = git_get_project_description($project) || "none";
5126        my %co = parse_commit("HEAD");
5127        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5128        my $head = $co{'id'};
5129
5130        my $owner = git_get_project_owner($project);
5131
5132        my $refs = git_get_references();
5133        # These get_*_list functions return one more to allow us to see if
5134        # there are more ...
5135        my @taglist  = git_get_tags_list(16);
5136        my @headlist = git_get_heads_list(16);
5137        my @forklist;
5138        my $check_forks = gitweb_check_feature('forks');
5139
5140        if ($check_forks) {
5141                @forklist = git_get_projects_list($project);
5142        }
5143
5144        git_header_html();
5145        git_print_page_nav('summary','', $head);
5146
5147        print "<div class=\"title\">&nbsp;</div>\n";
5148        print "<table class=\"projects_list\">\n" .
5149              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5150              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5151        if (defined $cd{'rfc2822'}) {
5152                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5153        }
5154
5155        # use per project git URL list in $projectroot/$project/cloneurl
5156        # or make project git URL from git base URL and project name
5157        my $url_tag = "URL";
5158        my @url_list = git_get_project_url_list($project);
5159        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5160        foreach my $git_url (@url_list) {
5161                next unless $git_url;
5162                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
5163                $url_tag = "";
5164        }
5165
5166        # Tag cloud
5167        my $show_ctags = gitweb_check_feature('ctags');
5168        if ($show_ctags) {
5169                my $ctags = git_get_project_ctags($project);
5170                my $cloud = git_populate_project_tagcloud($ctags);
5171                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
5172                print "</td>\n<td>" unless %$ctags;
5173                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
5174                print "</td>\n<td>" if %$ctags;
5175                print git_show_project_tagcloud($cloud, 48);
5176                print "</td></tr>";
5177        }
5178
5179        print "</table>\n";
5180
5181        # If XSS prevention is on, we don't include README.html.
5182        # TODO: Allow a readme in some safe format.
5183        if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5184                print "<div class=\"title\">readme</div>\n" .
5185                      "<div class=\"readme\">\n";
5186                insert_file("$projectroot/$project/README.html");
5187                print "\n</div>\n"; # class="readme"
5188        }
5189
5190        # we need to request one more than 16 (0..15) to check if
5191        # those 16 are all
5192        my @commitlist = $head ? parse_commits($head, 17) : ();
5193        if (@commitlist) {
5194                git_print_header_div('shortlog');
5195                git_shortlog_body(\@commitlist, 0, 15, $refs,
5196                                  $#commitlist <=  15 ? undef :
5197                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
5198        }
5199
5200        if (@taglist) {
5201                git_print_header_div('tags');
5202                git_tags_body(\@taglist, 0, 15,
5203                              $#taglist <=  15 ? undef :
5204                              $cgi->a({-href => href(action=>"tags")}, "..."));
5205        }
5206
5207        if (@headlist) {
5208                git_print_header_div('heads');
5209                git_heads_body(\@headlist, $head, 0, 15,
5210                               $#headlist <= 15 ? undef :
5211                               $cgi->a({-href => href(action=>"heads")}, "..."));
5212        }
5213
5214        if (@forklist) {
5215                git_print_header_div('forks');
5216                git_project_list_body(\@forklist, 'age', 0, 15,
5217                                      $#forklist <= 15 ? undef :
5218                                      $cgi->a({-href => href(action=>"forks")}, "..."),
5219                                      'no_header');
5220        }
5221
5222        git_footer_html();
5223}
5224
5225sub git_tag {
5226        my %tag = parse_tag($hash);
5227
5228        if (! %tag) {
5229                die_error(404, "Unknown tag object");
5230        }
5231
5232        my $head = git_get_head_hash($project);
5233        git_header_html();
5234        git_print_page_nav('','', $head,undef,$head);
5235        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5236        print "<div class=\"title_text\">\n" .
5237              "<table class=\"object_header\">\n" .
5238              "<tr>\n" .
5239              "<td>object</td>\n" .
5240              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5241                               $tag{'object'}) . "</td>\n" .
5242              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5243                                              $tag{'type'}) . "</td>\n" .
5244              "</tr>\n";
5245        if (defined($tag{'author'})) {
5246                git_print_authorship_rows(\%tag, 'author');
5247        }
5248        print "</table>\n\n" .
5249              "</div>\n";
5250        print "<div class=\"page_body\">";
5251        my $comment = $tag{'comment'};
5252        foreach my $line (@$comment) {
5253                chomp $line;
5254                print esc_html($line, -nbsp=>1) . "<br/>\n";
5255        }
5256        print "</div>\n";
5257        git_footer_html();
5258}
5259
5260sub git_blame_common {
5261        my $format = shift || 'porcelain';
5262        if ($format eq 'porcelain' && $cgi->param('js')) {
5263                $format = 'incremental';
5264                $action = 'blame_incremental'; # for page title etc
5265        }
5266
5267        # permissions
5268        gitweb_check_feature('blame')
5269                or die_error(403, "Blame view not allowed");
5270
5271        # error checking
5272        die_error(400, "No file name given") unless $file_name;
5273        $hash_base ||= git_get_head_hash($project);
5274        die_error(404, "Couldn't find base commit") unless $hash_base;
5275        my %co = parse_commit($hash_base)
5276                or die_error(404, "Commit not found");
5277        my $ftype = "blob";
5278        if (!defined $hash) {
5279                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5280                        or die_error(404, "Error looking up file");
5281        } else {
5282                $ftype = git_get_type($hash);
5283                if ($ftype !~ "blob") {
5284                        die_error(400, "Object is not a blob");
5285                }
5286        }
5287
5288        my $fd;
5289        if ($format eq 'incremental') {
5290                # get file contents (as base)
5291                open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5292                        or die_error(500, "Open git-cat-file failed");
5293        } elsif ($format eq 'data') {
5294                # run git-blame --incremental
5295                open $fd, "-|", git_cmd(), "blame", "--incremental",
5296                        $hash_base, "--", $file_name
5297                        or die_error(500, "Open git-blame --incremental failed");
5298        } else {
5299                # run git-blame --porcelain
5300                open $fd, "-|", git_cmd(), "blame", '-p',
5301                        $hash_base, '--', $file_name
5302                        or die_error(500, "Open git-blame --porcelain failed");
5303        }
5304
5305        # incremental blame data returns early
5306        if ($format eq 'data') {
5307                print $cgi->header(
5308                        -type=>"text/plain", -charset => "utf-8",
5309                        -status=> "200 OK");
5310                local $| = 1; # output autoflush
5311                print while <$fd>;
5312                close $fd
5313                        or print "ERROR $!\n";
5314
5315                print 'END';
5316                if (defined $t0 && gitweb_check_feature('timed')) {
5317                        print ' '.
5318                              Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
5319                              ' '.$number_of_git_cmds;
5320                }
5321                print "\n";
5322
5323                return;
5324        }
5325
5326        # page header
5327        git_header_html();
5328        my $formats_nav =
5329                $cgi->a({-href => href(action=>"blob", -replay=>1)},
5330                        "blob") .
5331                " | ";
5332        if ($format eq 'incremental') {
5333                $formats_nav .=
5334                        $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5335                                "blame") . " (non-incremental)";
5336        } else {
5337                $formats_nav .=
5338                        $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5339                                "blame") . " (incremental)";
5340        }
5341        $formats_nav .=
5342                " | " .
5343                $cgi->a({-href => href(action=>"history", -replay=>1)},
5344                        "history") .
5345                " | " .
5346                $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5347                        "HEAD");
5348        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5349        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5350        git_print_page_path($file_name, $ftype, $hash_base);
5351
5352        # page body
5353        if ($format eq 'incremental') {
5354                print "<noscript>\n<div class=\"error\"><center><b>\n".
5355                      "This page requires JavaScript to run.\n Use ".
5356                      $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5357                              'this page').
5358                      " instead.\n".
5359                      "</b></center></div>\n</noscript>\n";
5360
5361                print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5362        }
5363
5364        print qq!<div class="page_body">\n!;
5365        print qq!<div id="progress_info">... / ...</div>\n!
5366                if ($format eq 'incremental');
5367        print qq!<table id="blame_table" class="blame" width="100%">\n!.
5368              #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5369              qq!<thead>\n!.
5370              qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5371              qq!</thead>\n!.
5372              qq!<tbody>\n!;
5373
5374        my @rev_color = qw(light dark);
5375        my $num_colors = scalar(@rev_color);
5376        my $current_color = 0;
5377
5378        if ($format eq 'incremental') {
5379                my $color_class = $rev_color[$current_color];
5380
5381                #contents of a file
5382                my $linenr = 0;
5383        LINE:
5384                while (my $line = <$fd>) {
5385                        chomp $line;
5386                        $linenr++;
5387
5388                        print qq!<tr id="l$linenr" class="$color_class">!.
5389                              qq!<td class="sha1"><a href=""> </a></td>!.
5390                              qq!<td class="linenr">!.
5391                              qq!<a class="linenr" href="">$linenr</a></td>!;
5392                        print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5393                        print qq!</tr>\n!;
5394                }
5395
5396        } else { # porcelain, i.e. ordinary blame
5397                my %metainfo = (); # saves information about commits
5398
5399                # blame data
5400        LINE:
5401                while (my $line = <$fd>) {
5402                        chomp $line;
5403                        # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5404                        # no <lines in group> for subsequent lines in group of lines
5405                        my ($full_rev, $orig_lineno, $lineno, $group_size) =
5406                           ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5407                        if (!exists $metainfo{$full_rev}) {
5408                                $metainfo{$full_rev} = { 'nprevious' => 0 };
5409                        }
5410                        my $meta = $metainfo{$full_rev};
5411                        my $data;
5412                        while ($data = <$fd>) {
5413                                chomp $data;
5414                                last if ($data =~ s/^\t//); # contents of line
5415                                if ($data =~ /^(\S+)(?: (.*))?$/) {
5416                                        $meta->{$1} = $2 unless exists $meta->{$1};
5417                                }
5418                                if ($data =~ /^previous /) {
5419                                        $meta->{'nprevious'}++;
5420                                }
5421                        }
5422                        my $short_rev = substr($full_rev, 0, 8);
5423                        my $author = $meta->{'author'};
5424                        my %date =
5425                                parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5426                        my $date = $date{'iso-tz'};
5427                        if ($group_size) {
5428                                $current_color = ($current_color + 1) % $num_colors;
5429                        }
5430                        my $tr_class = $rev_color[$current_color];
5431                        $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5432                        $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5433                        $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5434                        print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5435                        if ($group_size) {
5436                                print "<td class=\"sha1\"";
5437                                print " title=\"". esc_html($author) . ", $date\"";
5438                                print " rowspan=\"$group_size\"" if ($group_size > 1);
5439                                print ">";
5440                                print $cgi->a({-href => href(action=>"commit",
5441                                                             hash=>$full_rev,
5442                                                             file_name=>$file_name)},
5443                                              esc_html($short_rev));
5444                                if ($group_size >= 2) {
5445                                        my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5446                                        if (@author_initials) {
5447                                                print "<br />" .
5448                                                      esc_html(join('', @author_initials));
5449                                                #           or join('.', ...)
5450                                        }
5451                                }
5452                                print "</td>\n";
5453                        }
5454                        # 'previous' <sha1 of parent commit> <filename at commit>
5455                        if (exists $meta->{'previous'} &&
5456                            $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5457                                $meta->{'parent'} = $1;
5458                                $meta->{'file_parent'} = unquote($2);
5459                        }
5460                        my $linenr_commit =
5461                                exists($meta->{'parent'}) ?
5462                                $meta->{'parent'} : $full_rev;
5463                        my $linenr_filename =
5464                                exists($meta->{'file_parent'}) ?
5465                                $meta->{'file_parent'} : unquote($meta->{'filename'});
5466                        my $blamed = href(action => 'blame',
5467                                          file_name => $linenr_filename,
5468                                          hash_base => $linenr_commit);
5469                        print "<td class=\"linenr\">";
5470                        print $cgi->a({ -href => "$blamed#l$orig_lineno",
5471                                        -class => "linenr" },
5472                                      esc_html($lineno));
5473                        print "</td>";
5474                        print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5475                        print "</tr>\n";
5476                } # end while
5477
5478        }
5479
5480        # footer
5481        print "</tbody>\n".
5482              "</table>\n"; # class="blame"
5483        print "</div>\n";   # class="blame_body"
5484        close $fd
5485                or print "Reading blob failed\n";
5486
5487        git_footer_html();
5488}
5489
5490sub git_blame {
5491        git_blame_common();
5492}
5493
5494sub git_blame_incremental {
5495        git_blame_common('incremental');
5496}
5497
5498sub git_blame_data {
5499        git_blame_common('data');
5500}
5501
5502sub git_tags {
5503        my $head = git_get_head_hash($project);
5504        git_header_html();
5505        git_print_page_nav('','', $head,undef,$head);
5506        git_print_header_div('summary', $project);
5507
5508        my @tagslist = git_get_tags_list();
5509        if (@tagslist) {
5510                git_tags_body(\@tagslist);
5511        }
5512        git_footer_html();
5513}
5514
5515sub git_heads {
5516        my $head = git_get_head_hash($project);
5517        git_header_html();
5518        git_print_page_nav('','', $head,undef,$head);
5519        git_print_header_div('summary', $project);
5520
5521        my @headslist = git_get_heads_list();
5522        if (@headslist) {
5523                git_heads_body(\@headslist, $head);
5524        }
5525        git_footer_html();
5526}
5527
5528sub git_blob_plain {
5529        my $type = shift;
5530        my $expires;
5531
5532        if (!defined $hash) {
5533                if (defined $file_name) {
5534                        my $base = $hash_base || git_get_head_hash($project);
5535                        $hash = git_get_hash_by_path($base, $file_name, "blob")
5536                                or die_error(404, "Cannot find file");
5537                } else {
5538                        die_error(400, "No file name defined");
5539                }
5540        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5541                # blobs defined by non-textual hash id's can be cached
5542                $expires = "+1d";
5543        }
5544
5545        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5546                or die_error(500, "Open git-cat-file blob '$hash' failed");
5547
5548        # content-type (can include charset)
5549        $type = blob_contenttype($fd, $file_name, $type);
5550
5551        # "save as" filename, even when no $file_name is given
5552        my $save_as = "$hash";
5553        if (defined $file_name) {
5554                $save_as = $file_name;
5555        } elsif ($type =~ m/^text\//) {
5556                $save_as .= '.txt';
5557        }
5558
5559        # With XSS prevention on, blobs of all types except a few known safe
5560        # ones are served with "Content-Disposition: attachment" to make sure
5561        # they don't run in our security domain.  For certain image types,
5562        # blob view writes an <img> tag referring to blob_plain view, and we
5563        # want to be sure not to break that by serving the image as an
5564        # attachment (though Firefox 3 doesn't seem to care).
5565        my $sandbox = $prevent_xss &&
5566                $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5567
5568        print $cgi->header(
5569                -type => $type,
5570                -expires => $expires,
5571                -content_disposition =>
5572                        ($sandbox ? 'attachment' : 'inline')
5573                        . '; filename="' . $save_as . '"');
5574        local $/ = undef;
5575        binmode STDOUT, ':raw';
5576        print <$fd>;
5577        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5578        close $fd;
5579}
5580
5581sub git_blob {
5582        my $expires;
5583
5584        if (!defined $hash) {
5585                if (defined $file_name) {
5586                        my $base = $hash_base || git_get_head_hash($project);
5587                        $hash = git_get_hash_by_path($base, $file_name, "blob")
5588                                or die_error(404, "Cannot find file");
5589                } else {
5590                        die_error(400, "No file name defined");
5591                }
5592        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5593                # blobs defined by non-textual hash id's can be cached
5594                $expires = "+1d";
5595        }
5596
5597        my $have_blame = gitweb_check_feature('blame');
5598        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5599                or die_error(500, "Couldn't cat $file_name, $hash");
5600        my $mimetype = blob_mimetype($fd, $file_name);
5601        # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
5602        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5603                close $fd;
5604                return git_blob_plain($mimetype);
5605        }
5606        # we can have blame only for text/* mimetype
5607        $have_blame &&= ($mimetype =~ m!^text/!);
5608
5609        my $highlight = gitweb_check_feature('highlight');
5610        my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
5611        $fd = run_highlighter($fd, $highlight, $syntax)
5612                if $syntax;
5613
5614        git_header_html(undef, $expires);
5615        my $formats_nav = '';
5616        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5617                if (defined $file_name) {
5618                        if ($have_blame) {
5619                                $formats_nav .=
5620                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
5621                                                "blame") .
5622                                        " | ";
5623                        }
5624                        $formats_nav .=
5625                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5626                                        "history") .
5627                                " | " .
5628                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5629                                        "raw") .
5630                                " | " .
5631                                $cgi->a({-href => href(action=>"blob",
5632                                                       hash_base=>"HEAD", file_name=>$file_name)},
5633                                        "HEAD");
5634                } else {
5635                        $formats_nav .=
5636                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5637                                        "raw");
5638                }
5639                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5640                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5641        } else {
5642                print "<div class=\"page_nav\">\n" .
5643                      "<br/><br/></div>\n" .
5644                      "<div class=\"title\">$hash</div>\n";
5645        }
5646        git_print_page_path($file_name, "blob", $hash_base);
5647        print "<div class=\"page_body\">\n";
5648        if ($mimetype =~ m!^image/!) {
5649                print qq!<img type="$mimetype"!;
5650                if ($file_name) {
5651                        print qq! alt="$file_name" title="$file_name"!;
5652                }
5653                print qq! src="! .
5654                      href(action=>"blob_plain", hash=>$hash,
5655                           hash_base=>$hash_base, file_name=>$file_name) .
5656                      qq!" />\n!;
5657        } else {
5658                my $nr;
5659                while (my $line = <$fd>) {
5660                        chomp $line;
5661                        $nr++;
5662                        $line = untabify($line);
5663                        printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
5664                               $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
5665                }
5666        }
5667        close $fd
5668                or print "Reading blob failed.\n";
5669        print "</div>";
5670        git_footer_html();
5671}
5672
5673sub git_tree {
5674        if (!defined $hash_base) {
5675                $hash_base = "HEAD";
5676        }
5677        if (!defined $hash) {
5678                if (defined $file_name) {
5679                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5680                } else {
5681                        $hash = $hash_base;
5682                }
5683        }
5684        die_error(404, "No such tree") unless defined($hash);
5685
5686        my $show_sizes = gitweb_check_feature('show-sizes');
5687        my $have_blame = gitweb_check_feature('blame');
5688
5689        my @entries = ();
5690        {
5691                local $/ = "\0";
5692                open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5693                        ($show_sizes ? '-l' : ()), @extra_options, $hash
5694                        or die_error(500, "Open git-ls-tree failed");
5695                @entries = map { chomp; $_ } <$fd>;
5696                close $fd
5697                        or die_error(404, "Reading tree failed");
5698        }
5699
5700        my $refs = git_get_references();
5701        my $ref = format_ref_marker($refs, $hash_base);
5702        git_header_html();
5703        my $basedir = '';
5704        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5705                my @views_nav = ();
5706                if (defined $file_name) {
5707                        push @views_nav,
5708                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5709                                        "history"),
5710                                $cgi->a({-href => href(action=>"tree",
5711                                                       hash_base=>"HEAD", file_name=>$file_name)},
5712                                        "HEAD"),
5713                }
5714                my $snapshot_links = format_snapshot_links($hash);
5715                if (defined $snapshot_links) {
5716                        # FIXME: Should be available when we have no hash base as well.
5717                        push @views_nav, $snapshot_links;
5718                }
5719                git_print_page_nav('tree','', $hash_base, undef, undef,
5720                                   join(' | ', @views_nav));
5721                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5722        } else {
5723                undef $hash_base;
5724                print "<div class=\"page_nav\">\n";
5725                print "<br/><br/></div>\n";
5726                print "<div class=\"title\">$hash</div>\n";
5727        }
5728        if (defined $file_name) {
5729                $basedir = $file_name;
5730                if ($basedir ne '' && substr($basedir, -1) ne '/') {
5731                        $basedir .= '/';
5732                }
5733                git_print_page_path($file_name, 'tree', $hash_base);
5734        }
5735        print "<div class=\"page_body\">\n";
5736        print "<table class=\"tree\">\n";
5737        my $alternate = 1;
5738        # '..' (top directory) link if possible
5739        if (defined $hash_base &&
5740            defined $file_name && $file_name =~ m![^/]+$!) {
5741                if ($alternate) {
5742                        print "<tr class=\"dark\">\n";
5743                } else {
5744                        print "<tr class=\"light\">\n";
5745                }
5746                $alternate ^= 1;
5747
5748                my $up = $file_name;
5749                $up =~ s!/?[^/]+$!!;
5750                undef $up unless $up;
5751                # based on git_print_tree_entry
5752                print '<td class="mode">' . mode_str('040000') . "</td>\n";
5753                print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
5754                print '<td class="list">';
5755                print $cgi->a({-href => href(action=>"tree",
5756                                             hash_base=>$hash_base,
5757                                             file_name=>$up)},
5758                              "..");
5759                print "</td>\n";
5760                print "<td class=\"link\"></td>\n";
5761
5762                print "</tr>\n";
5763        }
5764        foreach my $line (@entries) {
5765                my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5766
5767                if ($alternate) {
5768                        print "<tr class=\"dark\">\n";
5769                } else {
5770                        print "<tr class=\"light\">\n";
5771                }
5772                $alternate ^= 1;
5773
5774                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5775
5776                print "</tr>\n";
5777        }
5778        print "</table>\n" .
5779              "</div>";
5780        git_footer_html();
5781}
5782
5783sub snapshot_name {
5784        my ($project, $hash) = @_;
5785
5786        # path/to/project.git  -> project
5787        # path/to/project/.git -> project
5788        my $name = to_utf8($project);
5789        $name =~ s,([^/])/*\.git$,$1,;
5790        $name = basename($name);
5791        # sanitize name
5792        $name =~ s/[[:cntrl:]]/?/g;
5793
5794        my $ver = $hash;
5795        if ($hash =~ /^[0-9a-fA-F]+$/) {
5796                # shorten SHA-1 hash
5797                my $full_hash = git_get_full_hash($project, $hash);
5798                if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5799                        $ver = git_get_short_hash($project, $hash);
5800                }
5801        } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5802                # tags don't need shortened SHA-1 hash
5803                $ver = $1;
5804        } else {
5805                # branches and other need shortened SHA-1 hash
5806                if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5807                        $ver = $1;
5808                }
5809                $ver .= '-' . git_get_short_hash($project, $hash);
5810        }
5811        # in case of hierarchical branch names
5812        $ver =~ s!/!.!g;
5813
5814        # name = project-version_string
5815        $name = "$name-$ver";
5816
5817        return wantarray ? ($name, $name) : $name;
5818}
5819
5820sub git_snapshot {
5821        my $format = $input_params{'snapshot_format'};
5822        if (!@snapshot_fmts) {
5823                die_error(403, "Snapshots not allowed");
5824        }
5825        # default to first supported snapshot format
5826        $format ||= $snapshot_fmts[0];
5827        if ($format !~ m/^[a-z0-9]+$/) {
5828                die_error(400, "Invalid snapshot format parameter");
5829        } elsif (!exists($known_snapshot_formats{$format})) {
5830                die_error(400, "Unknown snapshot format");
5831        } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5832                die_error(403, "Snapshot format not allowed");
5833        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5834                die_error(403, "Unsupported snapshot format");
5835        }
5836
5837        my $type = git_get_type("$hash^{}");
5838        if (!$type) {
5839                die_error(404, 'Object does not exist');
5840        }  elsif ($type eq 'blob') {
5841                die_error(400, 'Object is not a tree-ish');
5842        }
5843
5844        my ($name, $prefix) = snapshot_name($project, $hash);
5845        my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5846        my $cmd = quote_command(
5847                git_cmd(), 'archive',
5848                "--format=$known_snapshot_formats{$format}{'format'}",
5849                "--prefix=$prefix/", $hash);
5850        if (exists $known_snapshot_formats{$format}{'compressor'}) {
5851                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5852        }
5853
5854        $filename =~ s/(["\\])/\\$1/g;
5855        print $cgi->header(
5856                -type => $known_snapshot_formats{$format}{'type'},
5857                -content_disposition => 'inline; filename="' . $filename . '"',
5858                -status => '200 OK');
5859
5860        open my $fd, "-|", $cmd
5861                or die_error(500, "Execute git-archive failed");
5862        binmode STDOUT, ':raw';
5863        print <$fd>;
5864        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5865        close $fd;
5866}
5867
5868sub git_log_generic {
5869        my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
5870
5871        my $head = git_get_head_hash($project);
5872        if (!defined $base) {
5873                $base = $head;
5874        }
5875        if (!defined $page) {
5876                $page = 0;
5877        }
5878        my $refs = git_get_references();
5879
5880        my $commit_hash = $base;
5881        if (defined $parent) {
5882                $commit_hash = "$parent..$base";
5883        }
5884        my @commitlist =
5885                parse_commits($commit_hash, 101, (100 * $page),
5886                              defined $file_name ? ($file_name, "--full-history") : ());
5887
5888        my $ftype;
5889        if (!defined $file_hash && defined $file_name) {
5890                # some commits could have deleted file in question,
5891                # and not have it in tree, but one of them has to have it
5892                for (my $i = 0; $i < @commitlist; $i++) {
5893                        $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5894                        last if defined $file_hash;
5895                }
5896        }
5897        if (defined $file_hash) {
5898                $ftype = git_get_type($file_hash);
5899        }
5900        if (defined $file_name && !defined $ftype) {
5901                die_error(500, "Unknown type of object");
5902        }
5903        my %co;
5904        if (defined $file_name) {
5905                %co = parse_commit($base)
5906                        or die_error(404, "Unknown commit object");
5907        }
5908
5909
5910        my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
5911        my $next_link = '';
5912        if ($#commitlist >= 100) {
5913                $next_link =
5914                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5915                                 -accesskey => "n", -title => "Alt-n"}, "next");
5916        }
5917        my $patch_max = gitweb_get_feature('patches');
5918        if ($patch_max && !defined $file_name) {
5919                if ($patch_max < 0 || @commitlist <= $patch_max) {
5920                        $paging_nav .= " &sdot; " .
5921                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
5922                                        "patches");
5923                }
5924        }
5925
5926        git_header_html();
5927        git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
5928        if (defined $file_name) {
5929                git_print_header_div('commit', esc_html($co{'title'}), $base);
5930        } else {
5931                git_print_header_div('summary', $project)
5932        }
5933        git_print_page_path($file_name, $ftype, $hash_base)
5934                if (defined $file_name);
5935
5936        $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
5937                     $file_name, $file_hash, $ftype);
5938
5939        git_footer_html();
5940}
5941
5942sub git_log {
5943        git_log_generic('log', \&git_log_body,
5944                        $hash, $hash_parent);
5945}
5946
5947sub git_commit {
5948        $hash ||= $hash_base || "HEAD";
5949        my %co = parse_commit($hash)
5950            or die_error(404, "Unknown commit object");
5951
5952        my $parent  = $co{'parent'};
5953        my $parents = $co{'parents'}; # listref
5954
5955        # we need to prepare $formats_nav before any parameter munging
5956        my $formats_nav;
5957        if (!defined $parent) {
5958                # --root commitdiff
5959                $formats_nav .= '(initial)';
5960        } elsif (@$parents == 1) {
5961                # single parent commit
5962                $formats_nav .=
5963                        '(parent: ' .
5964                        $cgi->a({-href => href(action=>"commit",
5965                                               hash=>$parent)},
5966                                esc_html(substr($parent, 0, 7))) .
5967                        ')';
5968        } else {
5969                # merge commit
5970                $formats_nav .=
5971                        '(merge: ' .
5972                        join(' ', map {
5973                                $cgi->a({-href => href(action=>"commit",
5974                                                       hash=>$_)},
5975                                        esc_html(substr($_, 0, 7)));
5976                        } @$parents ) .
5977                        ')';
5978        }
5979        if (gitweb_check_feature('patches') && @$parents <= 1) {
5980                $formats_nav .= " | " .
5981                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
5982                                "patch");
5983        }
5984
5985        if (!defined $parent) {
5986                $parent = "--root";
5987        }
5988        my @difftree;
5989        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5990                @diff_opts,
5991                (@$parents <= 1 ? $parent : '-c'),
5992                $hash, "--"
5993                or die_error(500, "Open git-diff-tree failed");
5994        @difftree = map { chomp; $_ } <$fd>;
5995        close $fd or die_error(404, "Reading git-diff-tree failed");
5996
5997        # non-textual hash id's can be cached
5998        my $expires;
5999        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6000                $expires = "+1d";
6001        }
6002        my $refs = git_get_references();
6003        my $ref = format_ref_marker($refs, $co{'id'});
6004
6005        git_header_html(undef, $expires);
6006        git_print_page_nav('commit', '',
6007                           $hash, $co{'tree'}, $hash,
6008                           $formats_nav);
6009
6010        if (defined $co{'parent'}) {
6011                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
6012        } else {
6013                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
6014        }
6015        print "<div class=\"title_text\">\n" .
6016              "<table class=\"object_header\">\n";
6017        git_print_authorship_rows(\%co);
6018        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
6019        print "<tr>" .
6020              "<td>tree</td>" .
6021              "<td class=\"sha1\">" .
6022              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
6023                       class => "list"}, $co{'tree'}) .
6024              "</td>" .
6025              "<td class=\"link\">" .
6026              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
6027                      "tree");
6028        my $snapshot_links = format_snapshot_links($hash);
6029        if (defined $snapshot_links) {
6030                print " | " . $snapshot_links;
6031        }
6032        print "</td>" .
6033              "</tr>\n";
6034
6035        foreach my $par (@$parents) {
6036                print "<tr>" .
6037                      "<td>parent</td>" .
6038                      "<td class=\"sha1\">" .
6039                      $cgi->a({-href => href(action=>"commit", hash=>$par),
6040                               class => "list"}, $par) .
6041                      "</td>" .
6042                      "<td class=\"link\">" .
6043                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
6044                      " | " .
6045                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
6046                      "</td>" .
6047                      "</tr>\n";
6048        }
6049        print "</table>".
6050              "</div>\n";
6051
6052        print "<div class=\"page_body\">\n";
6053        git_print_log($co{'comment'});
6054        print "</div>\n";
6055
6056        git_difftree_body(\@difftree, $hash, @$parents);
6057
6058        git_footer_html();
6059}
6060
6061sub git_object {
6062        # object is defined by:
6063        # - hash or hash_base alone
6064        # - hash_base and file_name
6065        my $type;
6066
6067        # - hash or hash_base alone
6068        if ($hash || ($hash_base && !defined $file_name)) {
6069                my $object_id = $hash || $hash_base;
6070
6071                open my $fd, "-|", quote_command(
6072                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6073                        or die_error(404, "Object does not exist");
6074                $type = <$fd>;
6075                chomp $type;
6076                close $fd
6077                        or die_error(404, "Object does not exist");
6078
6079        # - hash_base and file_name
6080        } elsif ($hash_base && defined $file_name) {
6081                $file_name =~ s,/+$,,;
6082
6083                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6084                        or die_error(404, "Base object does not exist");
6085
6086                # here errors should not hapen
6087                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6088                        or die_error(500, "Open git-ls-tree failed");
6089                my $line = <$fd>;
6090                close $fd;
6091
6092                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
6093                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6094                        die_error(404, "File or directory for given base does not exist");
6095                }
6096                $type = $2;
6097                $hash = $3;
6098        } else {
6099                die_error(400, "Not enough information to find object");
6100        }
6101
6102        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6103                                          hash=>$hash, hash_base=>$hash_base,
6104                                          file_name=>$file_name),
6105                             -status => '302 Found');
6106}
6107
6108sub git_blobdiff {
6109        my $format = shift || 'html';
6110
6111        my $fd;
6112        my @difftree;
6113        my %diffinfo;
6114        my $expires;
6115
6116        # preparing $fd and %diffinfo for git_patchset_body
6117        # new style URI
6118        if (defined $hash_base && defined $hash_parent_base) {
6119                if (defined $file_name) {
6120                        # read raw output
6121                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6122                                $hash_parent_base, $hash_base,
6123                                "--", (defined $file_parent ? $file_parent : ()), $file_name
6124                                or die_error(500, "Open git-diff-tree failed");
6125                        @difftree = map { chomp; $_ } <$fd>;
6126                        close $fd
6127                                or die_error(404, "Reading git-diff-tree failed");
6128                        @difftree
6129                                or die_error(404, "Blob diff not found");
6130
6131                } elsif (defined $hash &&
6132                         $hash =~ /[0-9a-fA-F]{40}/) {
6133                        # try to find filename from $hash
6134
6135                        # read filtered raw output
6136                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6137                                $hash_parent_base, $hash_base, "--"
6138                                or die_error(500, "Open git-diff-tree failed");
6139                        @difftree =
6140                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
6141                                # $hash == to_id
6142                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6143                                map { chomp; $_ } <$fd>;
6144                        close $fd
6145                                or die_error(404, "Reading git-diff-tree failed");
6146                        @difftree
6147                                or die_error(404, "Blob diff not found");
6148
6149                } else {
6150                        die_error(400, "Missing one of the blob diff parameters");
6151                }
6152
6153                if (@difftree > 1) {
6154                        die_error(400, "Ambiguous blob diff specification");
6155                }
6156
6157                %diffinfo = parse_difftree_raw_line($difftree[0]);
6158                $file_parent ||= $diffinfo{'from_file'} || $file_name;
6159                $file_name   ||= $diffinfo{'to_file'};
6160
6161                $hash_parent ||= $diffinfo{'from_id'};
6162                $hash        ||= $diffinfo{'to_id'};
6163
6164                # non-textual hash id's can be cached
6165                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6166                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6167                        $expires = '+1d';
6168                }
6169
6170                # open patch output
6171                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6172                        '-p', ($format eq 'html' ? "--full-index" : ()),
6173                        $hash_parent_base, $hash_base,
6174                        "--", (defined $file_parent ? $file_parent : ()), $file_name
6175                        or die_error(500, "Open git-diff-tree failed");
6176        }
6177
6178        # old/legacy style URI -- not generated anymore since 1.4.3.
6179        if (!%diffinfo) {
6180                die_error('404 Not Found', "Missing one of the blob diff parameters")
6181        }
6182
6183        # header
6184        if ($format eq 'html') {
6185                my $formats_nav =
6186                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6187                                "raw");
6188                git_header_html(undef, $expires);
6189                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6190                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6191                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6192                } else {
6193                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6194                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
6195                }
6196                if (defined $file_name) {
6197                        git_print_page_path($file_name, "blob", $hash_base);
6198                } else {
6199                        print "<div class=\"page_path\"></div>\n";
6200                }
6201
6202        } elsif ($format eq 'plain') {
6203                print $cgi->header(
6204                        -type => 'text/plain',
6205                        -charset => 'utf-8',
6206                        -expires => $expires,
6207                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6208
6209                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6210
6211        } else {
6212                die_error(400, "Unknown blobdiff format");
6213        }
6214
6215        # patch
6216        if ($format eq 'html') {
6217                print "<div class=\"page_body\">\n";
6218
6219                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6220                close $fd;
6221
6222                print "</div>\n"; # class="page_body"
6223                git_footer_html();
6224
6225        } else {
6226                while (my $line = <$fd>) {
6227                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6228                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6229
6230                        print $line;
6231
6232                        last if $line =~ m!^\+\+\+!;
6233                }
6234                local $/ = undef;
6235                print <$fd>;
6236                close $fd;
6237        }
6238}
6239
6240sub git_blobdiff_plain {
6241        git_blobdiff('plain');
6242}
6243
6244sub git_commitdiff {
6245        my %params = @_;
6246        my $format = $params{-format} || 'html';
6247
6248        my ($patch_max) = gitweb_get_feature('patches');
6249        if ($format eq 'patch') {
6250                die_error(403, "Patch view not allowed") unless $patch_max;
6251        }
6252
6253        $hash ||= $hash_base || "HEAD";
6254        my %co = parse_commit($hash)
6255            or die_error(404, "Unknown commit object");
6256
6257        # choose format for commitdiff for merge
6258        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6259                $hash_parent = '--cc';
6260        }
6261        # we need to prepare $formats_nav before almost any parameter munging
6262        my $formats_nav;
6263        if ($format eq 'html') {
6264                $formats_nav =
6265                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6266                                "raw");
6267                if ($patch_max && @{$co{'parents'}} <= 1) {
6268                        $formats_nav .= " | " .
6269                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
6270                                        "patch");
6271                }
6272
6273                if (defined $hash_parent &&
6274                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
6275                        # commitdiff with two commits given
6276                        my $hash_parent_short = $hash_parent;
6277                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6278                                $hash_parent_short = substr($hash_parent, 0, 7);
6279                        }
6280                        $formats_nav .=
6281                                ' (from';
6282                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6283                                if ($co{'parents'}[$i] eq $hash_parent) {
6284                                        $formats_nav .= ' parent ' . ($i+1);
6285                                        last;
6286                                }
6287                        }
6288                        $formats_nav .= ': ' .
6289                                $cgi->a({-href => href(action=>"commitdiff",
6290                                                       hash=>$hash_parent)},
6291                                        esc_html($hash_parent_short)) .
6292                                ')';
6293                } elsif (!$co{'parent'}) {
6294                        # --root commitdiff
6295                        $formats_nav .= ' (initial)';
6296                } elsif (scalar @{$co{'parents'}} == 1) {
6297                        # single parent commit
6298                        $formats_nav .=
6299                                ' (parent: ' .
6300                                $cgi->a({-href => href(action=>"commitdiff",
6301                                                       hash=>$co{'parent'})},
6302                                        esc_html(substr($co{'parent'}, 0, 7))) .
6303                                ')';
6304                } else {
6305                        # merge commit
6306                        if ($hash_parent eq '--cc') {
6307                                $formats_nav .= ' | ' .
6308                                        $cgi->a({-href => href(action=>"commitdiff",
6309                                                               hash=>$hash, hash_parent=>'-c')},
6310                                                'combined');
6311                        } else { # $hash_parent eq '-c'
6312                                $formats_nav .= ' | ' .
6313                                        $cgi->a({-href => href(action=>"commitdiff",
6314                                                               hash=>$hash, hash_parent=>'--cc')},
6315                                                'compact');
6316                        }
6317                        $formats_nav .=
6318                                ' (merge: ' .
6319                                join(' ', map {
6320                                        $cgi->a({-href => href(action=>"commitdiff",
6321                                                               hash=>$_)},
6322                                                esc_html(substr($_, 0, 7)));
6323                                } @{$co{'parents'}} ) .
6324                                ')';
6325                }
6326        }
6327
6328        my $hash_parent_param = $hash_parent;
6329        if (!defined $hash_parent_param) {
6330                # --cc for multiple parents, --root for parentless
6331                $hash_parent_param =
6332                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6333        }
6334
6335        # read commitdiff
6336        my $fd;
6337        my @difftree;
6338        if ($format eq 'html') {
6339                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6340                        "--no-commit-id", "--patch-with-raw", "--full-index",
6341                        $hash_parent_param, $hash, "--"
6342                        or die_error(500, "Open git-diff-tree failed");
6343
6344                while (my $line = <$fd>) {
6345                        chomp $line;
6346                        # empty line ends raw part of diff-tree output
6347                        last unless $line;
6348                        push @difftree, scalar parse_difftree_raw_line($line);
6349                }
6350
6351        } elsif ($format eq 'plain') {
6352                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6353                        '-p', $hash_parent_param, $hash, "--"
6354                        or die_error(500, "Open git-diff-tree failed");
6355        } elsif ($format eq 'patch') {
6356                # For commit ranges, we limit the output to the number of
6357                # patches specified in the 'patches' feature.
6358                # For single commits, we limit the output to a single patch,
6359                # diverging from the git-format-patch default.
6360                my @commit_spec = ();
6361                if ($hash_parent) {
6362                        if ($patch_max > 0) {
6363                                push @commit_spec, "-$patch_max";
6364                        }
6365                        push @commit_spec, '-n', "$hash_parent..$hash";
6366                } else {
6367                        if ($params{-single}) {
6368                                push @commit_spec, '-1';
6369                        } else {
6370                                if ($patch_max > 0) {
6371                                        push @commit_spec, "-$patch_max";
6372                                }
6373                                push @commit_spec, "-n";
6374                        }
6375                        push @commit_spec, '--root', $hash;
6376                }
6377                open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6378                        '--encoding=utf8', '--stdout', @commit_spec
6379                        or die_error(500, "Open git-format-patch failed");
6380        } else {
6381                die_error(400, "Unknown commitdiff format");
6382        }
6383
6384        # non-textual hash id's can be cached
6385        my $expires;
6386        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6387                $expires = "+1d";
6388        }
6389
6390        # write commit message
6391        if ($format eq 'html') {
6392                my $refs = git_get_references();
6393                my $ref = format_ref_marker($refs, $co{'id'});
6394
6395                git_header_html(undef, $expires);
6396                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6397                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6398                print "<div class=\"title_text\">\n" .
6399                      "<table class=\"object_header\">\n";
6400                git_print_authorship_rows(\%co);
6401                print "</table>".
6402                      "</div>\n";
6403                print "<div class=\"page_body\">\n";
6404                if (@{$co{'comment'}} > 1) {
6405                        print "<div class=\"log\">\n";
6406                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6407                        print "</div>\n"; # class="log"
6408                }
6409
6410        } elsif ($format eq 'plain') {
6411                my $refs = git_get_references("tags");
6412                my $tagname = git_get_rev_name_tags($hash);
6413                my $filename = basename($project) . "-$hash.patch";
6414
6415                print $cgi->header(
6416                        -type => 'text/plain',
6417                        -charset => 'utf-8',
6418                        -expires => $expires,
6419                        -content_disposition => 'inline; filename="' . "$filename" . '"');
6420                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6421                print "From: " . to_utf8($co{'author'}) . "\n";
6422                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6423                print "Subject: " . to_utf8($co{'title'}) . "\n";
6424
6425                print "X-Git-Tag: $tagname\n" if $tagname;
6426                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6427
6428                foreach my $line (@{$co{'comment'}}) {
6429                        print to_utf8($line) . "\n";
6430                }
6431                print "---\n\n";
6432        } elsif ($format eq 'patch') {
6433                my $filename = basename($project) . "-$hash.patch";
6434
6435                print $cgi->header(
6436                        -type => 'text/plain',
6437                        -charset => 'utf-8',
6438                        -expires => $expires,
6439                        -content_disposition => 'inline; filename="' . "$filename" . '"');
6440        }
6441
6442        # write patch
6443        if ($format eq 'html') {
6444                my $use_parents = !defined $hash_parent ||
6445                        $hash_parent eq '-c' || $hash_parent eq '--cc';
6446                git_difftree_body(\@difftree, $hash,
6447                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
6448                print "<br/>\n";
6449
6450                git_patchset_body($fd, \@difftree, $hash,
6451                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
6452                close $fd;
6453                print "</div>\n"; # class="page_body"
6454                git_footer_html();
6455
6456        } elsif ($format eq 'plain') {
6457                local $/ = undef;
6458                print <$fd>;
6459                close $fd
6460                        or print "Reading git-diff-tree failed\n";
6461        } elsif ($format eq 'patch') {
6462                local $/ = undef;
6463                print <$fd>;
6464                close $fd
6465                        or print "Reading git-format-patch failed\n";
6466        }
6467}
6468
6469sub git_commitdiff_plain {
6470        git_commitdiff(-format => 'plain');
6471}
6472
6473# format-patch-style patches
6474sub git_patch {
6475        git_commitdiff(-format => 'patch', -single => 1);
6476}
6477
6478sub git_patches {
6479        git_commitdiff(-format => 'patch');
6480}
6481
6482sub git_history {
6483        git_log_generic('history', \&git_history_body,
6484                        $hash_base, $hash_parent_base,
6485                        $file_name, $hash);
6486}
6487
6488sub git_search {
6489        gitweb_check_feature('search') or die_error(403, "Search is disabled");
6490        if (!defined $searchtext) {
6491                die_error(400, "Text field is empty");
6492        }
6493        if (!defined $hash) {
6494                $hash = git_get_head_hash($project);
6495        }
6496        my %co = parse_commit($hash);
6497        if (!%co) {
6498                die_error(404, "Unknown commit object");
6499        }
6500        if (!defined $page) {
6501                $page = 0;
6502        }
6503
6504        $searchtype ||= 'commit';
6505        if ($searchtype eq 'pickaxe') {
6506                # pickaxe may take all resources of your box and run for several minutes
6507                # with every query - so decide by yourself how public you make this feature
6508                gitweb_check_feature('pickaxe')
6509                    or die_error(403, "Pickaxe is disabled");
6510        }
6511        if ($searchtype eq 'grep') {
6512                gitweb_check_feature('grep')
6513                    or die_error(403, "Grep is disabled");
6514        }
6515
6516        git_header_html();
6517
6518        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6519                my $greptype;
6520                if ($searchtype eq 'commit') {
6521                        $greptype = "--grep=";
6522                } elsif ($searchtype eq 'author') {
6523                        $greptype = "--author=";
6524                } elsif ($searchtype eq 'committer') {
6525                        $greptype = "--committer=";
6526                }
6527                $greptype .= $searchtext;
6528                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6529                                               $greptype, '--regexp-ignore-case',
6530                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6531
6532                my $paging_nav = '';
6533                if ($page > 0) {
6534                        $paging_nav .=
6535                                $cgi->a({-href => href(action=>"search", hash=>$hash,
6536                                                       searchtext=>$searchtext,
6537                                                       searchtype=>$searchtype)},
6538                                        "first");
6539                        $paging_nav .= " &sdot; " .
6540                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
6541                                         -accesskey => "p", -title => "Alt-p"}, "prev");
6542                } else {
6543                        $paging_nav .= "first";
6544                        $paging_nav .= " &sdot; prev";
6545                }
6546                my $next_link = '';
6547                if ($#commitlist >= 100) {
6548                        $next_link =
6549                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
6550                                         -accesskey => "n", -title => "Alt-n"}, "next");
6551                        $paging_nav .= " &sdot; $next_link";
6552                } else {
6553                        $paging_nav .= " &sdot; next";
6554                }
6555
6556                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6557                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6558                if ($page == 0 && !@commitlist) {
6559                        print "<p>No match.</p>\n";
6560                } else {
6561                        git_search_grep_body(\@commitlist, 0, 99, $next_link);
6562                }
6563        }
6564
6565        if ($searchtype eq 'pickaxe') {
6566                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6567                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6568
6569                print "<table class=\"pickaxe search\">\n";
6570                my $alternate = 1;
6571                local $/ = "\n";
6572                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6573                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6574                        ($search_use_regexp ? '--pickaxe-regex' : ());
6575                undef %co;
6576                my @files;
6577                while (my $line = <$fd>) {
6578                        chomp $line;
6579                        next unless $line;
6580
6581                        my %set = parse_difftree_raw_line($line);
6582                        if (defined $set{'commit'}) {
6583                                # finish previous commit
6584                                if (%co) {
6585                                        print "</td>\n" .
6586                                              "<td class=\"link\">" .
6587                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6588                                              " | " .
6589                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6590                                        print "</td>\n" .
6591                                              "</tr>\n";
6592                                }
6593
6594                                if ($alternate) {
6595                                        print "<tr class=\"dark\">\n";
6596                                } else {
6597                                        print "<tr class=\"light\">\n";
6598                                }
6599                                $alternate ^= 1;
6600                                %co = parse_commit($set{'commit'});
6601                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6602                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6603                                      "<td><i>$author</i></td>\n" .
6604                                      "<td>" .
6605                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6606                                              -class => "list subject"},
6607                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6608                        } elsif (defined $set{'to_id'}) {
6609                                next if ($set{'to_id'} =~ m/^0{40}$/);
6610
6611                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6612                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6613                                              -class => "list"},
6614                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6615                                      "<br/>\n";
6616                        }
6617                }
6618                close $fd;
6619
6620                # finish last commit (warning: repetition!)
6621                if (%co) {
6622                        print "</td>\n" .
6623                              "<td class=\"link\">" .
6624                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6625                              " | " .
6626                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6627                        print "</td>\n" .
6628                              "</tr>\n";
6629                }
6630
6631                print "</table>\n";
6632        }
6633
6634        if ($searchtype eq 'grep') {
6635                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6636                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6637
6638                print "<table class=\"grep_search\">\n";
6639                my $alternate = 1;
6640                my $matches = 0;
6641                local $/ = "\n";
6642                open my $fd, "-|", git_cmd(), 'grep', '-n',
6643                        $search_use_regexp ? ('-E', '-i') : '-F',
6644                        $searchtext, $co{'tree'};
6645                my $lastfile = '';
6646                while (my $line = <$fd>) {
6647                        chomp $line;
6648                        my ($file, $lno, $ltext, $binary);
6649                        last if ($matches++ > 1000);
6650                        if ($line =~ /^Binary file (.+) matches$/) {
6651                                $file = $1;
6652                                $binary = 1;
6653                        } else {
6654                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6655                        }
6656                        if ($file ne $lastfile) {
6657                                $lastfile and print "</td></tr>\n";
6658                                if ($alternate++) {
6659                                        print "<tr class=\"dark\">\n";
6660                                } else {
6661                                        print "<tr class=\"light\">\n";
6662                                }
6663                                print "<td class=\"list\">".
6664                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6665                                                               file_name=>"$file"),
6666                                                -class => "list"}, esc_path($file));
6667                                print "</td><td>\n";
6668                                $lastfile = $file;
6669                        }
6670                        if ($binary) {
6671                                print "<div class=\"binary\">Binary file</div>\n";
6672                        } else {
6673                                $ltext = untabify($ltext);
6674                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6675                                        $ltext = esc_html($1, -nbsp=>1);
6676                                        $ltext .= '<span class="match">';
6677                                        $ltext .= esc_html($2, -nbsp=>1);
6678                                        $ltext .= '</span>';
6679                                        $ltext .= esc_html($3, -nbsp=>1);
6680                                } else {
6681                                        $ltext = esc_html($ltext, -nbsp=>1);
6682                                }
6683                                print "<div class=\"pre\">" .
6684                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6685                                                               file_name=>"$file").'#l'.$lno,
6686                                                -class => "linenr"}, sprintf('%4i', $lno))
6687                                        . ' ' .  $ltext . "</div>\n";
6688                        }
6689                }
6690                if ($lastfile) {
6691                        print "</td></tr>\n";
6692                        if ($matches > 1000) {
6693                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6694                        }
6695                } else {
6696                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
6697                }
6698                close $fd;
6699
6700                print "</table>\n";
6701        }
6702        git_footer_html();
6703}
6704
6705sub git_search_help {
6706        git_header_html();
6707        git_print_page_nav('','', $hash,$hash,$hash);
6708        print <<EOT;
6709<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6710regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6711the pattern entered is recognized as the POSIX extended
6712<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6713insensitive).</p>
6714<dl>
6715<dt><b>commit</b></dt>
6716<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6717EOT
6718        my $have_grep = gitweb_check_feature('grep');
6719        if ($have_grep) {
6720                print <<EOT;
6721<dt><b>grep</b></dt>
6722<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6723    a different one) are searched for the given pattern. On large trees, this search can take
6724a while and put some strain on the server, so please use it with some consideration. Note that
6725due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6726case-sensitive.</dd>
6727EOT
6728        }
6729        print <<EOT;
6730<dt><b>author</b></dt>
6731<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6732<dt><b>committer</b></dt>
6733<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6734EOT
6735        my $have_pickaxe = gitweb_check_feature('pickaxe');
6736        if ($have_pickaxe) {
6737                print <<EOT;
6738<dt><b>pickaxe</b></dt>
6739<dd>All commits that caused the string to appear or disappear from any file (changes that
6740added, removed or "modified" the string) will be listed. This search can take a while and
6741takes a lot of strain on the server, so please use it wisely. Note that since you may be
6742interested even in changes just changing the case as well, this search is case sensitive.</dd>
6743EOT
6744        }
6745        print "</dl>\n";
6746        git_footer_html();
6747}
6748
6749sub git_shortlog {
6750        git_log_generic('shortlog', \&git_shortlog_body,
6751                        $hash, $hash_parent);
6752}
6753
6754## ......................................................................
6755## feeds (RSS, Atom; OPML)
6756
6757sub git_feed {
6758        my $format = shift || 'atom';
6759        my $have_blame = gitweb_check_feature('blame');
6760
6761        # Atom: http://www.atomenabled.org/developers/syndication/
6762        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6763        if ($format ne 'rss' && $format ne 'atom') {
6764                die_error(400, "Unknown web feed format");
6765        }
6766
6767        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6768        my $head = $hash || 'HEAD';
6769        my @commitlist = parse_commits($head, 150, 0, $file_name);
6770
6771        my %latest_commit;
6772        my %latest_date;
6773        my $content_type = "application/$format+xml";
6774        if (defined $cgi->http('HTTP_ACCEPT') &&
6775                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6776                # browser (feed reader) prefers text/xml
6777                $content_type = 'text/xml';
6778        }
6779        if (defined($commitlist[0])) {
6780                %latest_commit = %{$commitlist[0]};
6781                my $latest_epoch = $latest_commit{'committer_epoch'};
6782                %latest_date   = parse_date($latest_epoch);
6783                my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6784                if (defined $if_modified) {
6785                        my $since;
6786                        if (eval { require HTTP::Date; 1; }) {
6787                                $since = HTTP::Date::str2time($if_modified);
6788                        } elsif (eval { require Time::ParseDate; 1; }) {
6789                                $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6790                        }
6791                        if (defined $since && $latest_epoch <= $since) {
6792                                print $cgi->header(
6793                                        -type => $content_type,
6794                                        -charset => 'utf-8',
6795                                        -last_modified => $latest_date{'rfc2822'},
6796                                        -status => '304 Not Modified');
6797                                return;
6798                        }
6799                }
6800                print $cgi->header(
6801                        -type => $content_type,
6802                        -charset => 'utf-8',
6803                        -last_modified => $latest_date{'rfc2822'});
6804        } else {
6805                print $cgi->header(
6806                        -type => $content_type,
6807                        -charset => 'utf-8');
6808        }
6809
6810        # Optimization: skip generating the body if client asks only
6811        # for Last-Modified date.
6812        return if ($cgi->request_method() eq 'HEAD');
6813
6814        # header variables
6815        my $title = "$site_name - $project/$action";
6816        my $feed_type = 'log';
6817        if (defined $hash) {
6818                $title .= " - '$hash'";
6819                $feed_type = 'branch log';
6820                if (defined $file_name) {
6821                        $title .= " :: $file_name";
6822                        $feed_type = 'history';
6823                }
6824        } elsif (defined $file_name) {
6825                $title .= " - $file_name";
6826                $feed_type = 'history';
6827        }
6828        $title .= " $feed_type";
6829        my $descr = git_get_project_description($project);
6830        if (defined $descr) {
6831                $descr = esc_html($descr);
6832        } else {
6833                $descr = "$project " .
6834                         ($format eq 'rss' ? 'RSS' : 'Atom') .
6835                         " feed";
6836        }
6837        my $owner = git_get_project_owner($project);
6838        $owner = esc_html($owner);
6839
6840        #header
6841        my $alt_url;
6842        if (defined $file_name) {
6843                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6844        } elsif (defined $hash) {
6845                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6846        } else {
6847                $alt_url = href(-full=>1, action=>"summary");
6848        }
6849        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6850        if ($format eq 'rss') {
6851                print <<XML;
6852<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6853<channel>
6854XML
6855                print "<title>$title</title>\n" .
6856                      "<link>$alt_url</link>\n" .
6857                      "<description>$descr</description>\n" .
6858                      "<language>en</language>\n" .
6859                      # project owner is responsible for 'editorial' content
6860                      "<managingEditor>$owner</managingEditor>\n";
6861                if (defined $logo || defined $favicon) {
6862                        # prefer the logo to the favicon, since RSS
6863                        # doesn't allow both
6864                        my $img = esc_url($logo || $favicon);
6865                        print "<image>\n" .
6866                              "<url>$img</url>\n" .
6867                              "<title>$title</title>\n" .
6868                              "<link>$alt_url</link>\n" .
6869                              "</image>\n";
6870                }
6871                if (%latest_date) {
6872                        print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6873                        print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6874                }
6875                print "<generator>gitweb v.$version/$git_version</generator>\n";
6876        } elsif ($format eq 'atom') {
6877                print <<XML;
6878<feed xmlns="http://www.w3.org/2005/Atom">
6879XML
6880                print "<title>$title</title>\n" .
6881                      "<subtitle>$descr</subtitle>\n" .
6882                      '<link rel="alternate" type="text/html" href="' .
6883                      $alt_url . '" />' . "\n" .
6884                      '<link rel="self" type="' . $content_type . '" href="' .
6885                      $cgi->self_url() . '" />' . "\n" .
6886                      "<id>" . href(-full=>1) . "</id>\n" .
6887                      # use project owner for feed author
6888                      "<author><name>$owner</name></author>\n";
6889                if (defined $favicon) {
6890                        print "<icon>" . esc_url($favicon) . "</icon>\n";
6891                }
6892                if (defined $logo_url) {
6893                        # not twice as wide as tall: 72 x 27 pixels
6894                        print "<logo>" . esc_url($logo) . "</logo>\n";
6895                }
6896                if (! %latest_date) {
6897                        # dummy date to keep the feed valid until commits trickle in:
6898                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
6899                } else {
6900                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
6901                }
6902                print "<generator version='$version/$git_version'>gitweb</generator>\n";
6903        }
6904
6905        # contents
6906        for (my $i = 0; $i <= $#commitlist; $i++) {
6907                my %co = %{$commitlist[$i]};
6908                my $commit = $co{'id'};
6909                # we read 150, we always show 30 and the ones more recent than 48 hours
6910                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6911                        last;
6912                }
6913                my %cd = parse_date($co{'author_epoch'});
6914
6915                # get list of changed files
6916                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6917                        $co{'parent'} || "--root",
6918                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
6919                        or next;
6920                my @difftree = map { chomp; $_ } <$fd>;
6921                close $fd
6922                        or next;
6923
6924                # print element (entry, item)
6925                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6926                if ($format eq 'rss') {
6927                        print "<item>\n" .
6928                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
6929                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
6930                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6931                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6932                              "<link>$co_url</link>\n" .
6933                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
6934                              "<content:encoded>" .
6935                              "<![CDATA[\n";
6936                } elsif ($format eq 'atom') {
6937                        print "<entry>\n" .
6938                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6939                              "<updated>$cd{'iso-8601'}</updated>\n" .
6940                              "<author>\n" .
6941                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6942                        if ($co{'author_email'}) {
6943                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6944                        }
6945                        print "</author>\n" .
6946                              # use committer for contributor
6947                              "<contributor>\n" .
6948                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6949                        if ($co{'committer_email'}) {
6950                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6951                        }
6952                        print "</contributor>\n" .
6953                              "<published>$cd{'iso-8601'}</published>\n" .
6954                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6955                              "<id>$co_url</id>\n" .
6956                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6957                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6958                }
6959                my $comment = $co{'comment'};
6960                print "<pre>\n";
6961                foreach my $line (@$comment) {
6962                        $line = esc_html($line);
6963                        print "$line\n";
6964                }
6965                print "</pre><ul>\n";
6966                foreach my $difftree_line (@difftree) {
6967                        my %difftree = parse_difftree_raw_line($difftree_line);
6968                        next if !$difftree{'from_id'};
6969
6970                        my $file = $difftree{'file'} || $difftree{'to_file'};
6971
6972                        print "<li>" .
6973                              "[" .
6974                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6975                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6976                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6977                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
6978                                      -title => "diff"}, 'D');
6979                        if ($have_blame) {
6980                                print $cgi->a({-href => href(-full=>1, action=>"blame",
6981                                                             file_name=>$file, hash_base=>$commit),
6982                                              -title => "blame"}, 'B');
6983                        }
6984                        # if this is not a feed of a file history
6985                        if (!defined $file_name || $file_name ne $file) {
6986                                print $cgi->a({-href => href(-full=>1, action=>"history",
6987                                                             file_name=>$file, hash=>$commit),
6988                                              -title => "history"}, 'H');
6989                        }
6990                        $file = esc_path($file);
6991                        print "] ".
6992                              "$file</li>\n";
6993                }
6994                if ($format eq 'rss') {
6995                        print "</ul>]]>\n" .
6996                              "</content:encoded>\n" .
6997                              "</item>\n";
6998                } elsif ($format eq 'atom') {
6999                        print "</ul>\n</div>\n" .
7000                              "</content>\n" .
7001                              "</entry>\n";
7002                }
7003        }
7004
7005        # end of feed
7006        if ($format eq 'rss') {
7007                print "</channel>\n</rss>\n";
7008        } elsif ($format eq 'atom') {
7009                print "</feed>\n";
7010        }
7011}
7012
7013sub git_rss {
7014        git_feed('rss');
7015}
7016
7017sub git_atom {
7018        git_feed('atom');
7019}
7020
7021sub git_opml {
7022        my @list = git_get_projects_list();
7023
7024        print $cgi->header(
7025                -type => 'text/xml',
7026                -charset => 'utf-8',
7027                -content_disposition => 'inline; filename="opml.xml"');
7028
7029        print <<XML;
7030<?xml version="1.0" encoding="utf-8"?>
7031<opml version="1.0">
7032<head>
7033  <title>$site_name OPML Export</title>
7034</head>
7035<body>
7036<outline text="git RSS feeds">
7037XML
7038
7039        foreach my $pr (@list) {
7040                my %proj = %$pr;
7041                my $head = git_get_head_hash($proj{'path'});
7042                if (!defined $head) {
7043                        next;
7044                }
7045                $git_dir = "$projectroot/$proj{'path'}";
7046                my %co = parse_commit($head);
7047                if (!%co) {
7048                        next;
7049                }
7050
7051                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
7052                my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7053                my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7054                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7055        }
7056        print <<XML;
7057</outline>
7058</body>
7059</opml>
7060XML
7061}