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