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