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