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