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