gitweb / gitweb.perlon commit gitweb: Smarter snapshot names (b629275)
   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        return git_get_full_hash(shift, 'HEAD');
1987}
1988
1989sub git_get_full_hash {
1990        return git_get_hash(@_);
1991}
1992
1993sub git_get_short_hash {
1994        return git_get_hash(@_, '--short=7');
1995}
1996
1997sub git_get_hash {
1998        my ($project, $hash, @options) = @_;
1999        my $o_git_dir = $git_dir;
2000        my $retval = undef;
2001        $git_dir = "$projectroot/$project";
2002        if (open my $fd, '-|', git_cmd(), 'rev-parse',
2003            '--verify', '-q', @options, $hash) {
2004                $retval = <$fd>;
2005                chomp $retval if defined $retval;
2006                close $fd;
2007        }
2008        if (defined $o_git_dir) {
2009                $git_dir = $o_git_dir;
2010        }
2011        return $retval;
2012}
2013
2014# get type of given object
2015sub git_get_type {
2016        my $hash = shift;
2017
2018        open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2019        my $type = <$fd>;
2020        close $fd or return;
2021        chomp $type;
2022        return $type;
2023}
2024
2025# repository configuration
2026our $config_file = '';
2027our %config;
2028
2029# store multiple values for single key as anonymous array reference
2030# single values stored directly in the hash, not as [ <value> ]
2031sub hash_set_multi {
2032        my ($hash, $key, $value) = @_;
2033
2034        if (!exists $hash->{$key}) {
2035                $hash->{$key} = $value;
2036        } elsif (!ref $hash->{$key}) {
2037                $hash->{$key} = [ $hash->{$key}, $value ];
2038        } else {
2039                push @{$hash->{$key}}, $value;
2040        }
2041}
2042
2043# return hash of git project configuration
2044# optionally limited to some section, e.g. 'gitweb'
2045sub git_parse_project_config {
2046        my $section_regexp = shift;
2047        my %config;
2048
2049        local $/ = "\0";
2050
2051        open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2052                or return;
2053
2054        while (my $keyval = <$fh>) {
2055                chomp $keyval;
2056                my ($key, $value) = split(/\n/, $keyval, 2);
2057
2058                hash_set_multi(\%config, $key, $value)
2059                        if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2060        }
2061        close $fh;
2062
2063        return %config;
2064}
2065
2066# convert config value to boolean: 'true' or 'false'
2067# no value, number > 0, 'true' and 'yes' values are true
2068# rest of values are treated as false (never as error)
2069sub config_to_bool {
2070        my $val = shift;
2071
2072        return 1 if !defined $val;             # section.key
2073
2074        # strip leading and trailing whitespace
2075        $val =~ s/^\s+//;
2076        $val =~ s/\s+$//;
2077
2078        return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2079                ($val =~ /^(?:true|yes)$/i));  # section.key = true
2080}
2081
2082# convert config value to simple decimal number
2083# an optional value suffix of 'k', 'm', or 'g' will cause the value
2084# to be multiplied by 1024, 1048576, or 1073741824
2085sub config_to_int {
2086        my $val = shift;
2087
2088        # strip leading and trailing whitespace
2089        $val =~ s/^\s+//;
2090        $val =~ s/\s+$//;
2091
2092        if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2093                $unit = lc($unit);
2094                # unknown unit is treated as 1
2095                return $num * ($unit eq 'g' ? 1073741824 :
2096                               $unit eq 'm' ?    1048576 :
2097                               $unit eq 'k' ?       1024 : 1);
2098        }
2099        return $val;
2100}
2101
2102# convert config value to array reference, if needed
2103sub config_to_multi {
2104        my $val = shift;
2105
2106        return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2107}
2108
2109sub git_get_project_config {
2110        my ($key, $type) = @_;
2111
2112        # key sanity check
2113        return unless ($key);
2114        $key =~ s/^gitweb\.//;
2115        return if ($key =~ m/\W/);
2116
2117        # type sanity check
2118        if (defined $type) {
2119                $type =~ s/^--//;
2120                $type = undef
2121                        unless ($type eq 'bool' || $type eq 'int');
2122        }
2123
2124        # get config
2125        if (!defined $config_file ||
2126            $config_file ne "$git_dir/config") {
2127                %config = git_parse_project_config('gitweb');
2128                $config_file = "$git_dir/config";
2129        }
2130
2131        # check if config variable (key) exists
2132        return unless exists $config{"gitweb.$key"};
2133
2134        # ensure given type
2135        if (!defined $type) {
2136                return $config{"gitweb.$key"};
2137        } elsif ($type eq 'bool') {
2138                # backward compatibility: 'git config --bool' returns true/false
2139                return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2140        } elsif ($type eq 'int') {
2141                return config_to_int($config{"gitweb.$key"});
2142        }
2143        return $config{"gitweb.$key"};
2144}
2145
2146# get hash of given path at given ref
2147sub git_get_hash_by_path {
2148        my $base = shift;
2149        my $path = shift || return undef;
2150        my $type = shift;
2151
2152        $path =~ s,/+$,,;
2153
2154        open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2155                or die_error(500, "Open git-ls-tree failed");
2156        my $line = <$fd>;
2157        close $fd or return undef;
2158
2159        if (!defined $line) {
2160                # there is no tree or hash given by $path at $base
2161                return undef;
2162        }
2163
2164        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2165        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2166        if (defined $type && $type ne $2) {
2167                # type doesn't match
2168                return undef;
2169        }
2170        return $3;
2171}
2172
2173# get path of entry with given hash at given tree-ish (ref)
2174# used to get 'from' filename for combined diff (merge commit) for renames
2175sub git_get_path_by_hash {
2176        my $base = shift || return;
2177        my $hash = shift || return;
2178
2179        local $/ = "\0";
2180
2181        open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2182                or return undef;
2183        while (my $line = <$fd>) {
2184                chomp $line;
2185
2186                #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2187                #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2188                if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2189                        close $fd;
2190                        return $1;
2191                }
2192        }
2193        close $fd;
2194        return undef;
2195}
2196
2197## ......................................................................
2198## git utility functions, directly accessing git repository
2199
2200sub git_get_project_description {
2201        my $path = shift;
2202
2203        $git_dir = "$projectroot/$path";
2204        open my $fd, '<', "$git_dir/description"
2205                or return git_get_project_config('description');
2206        my $descr = <$fd>;
2207        close $fd;
2208        if (defined $descr) {
2209                chomp $descr;
2210        }
2211        return $descr;
2212}
2213
2214sub git_get_project_ctags {
2215        my $path = shift;
2216        my $ctags = {};
2217
2218        $git_dir = "$projectroot/$path";
2219        opendir my $dh, "$git_dir/ctags"
2220                or return $ctags;
2221        foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2222                open my $ct, '<', $_ or next;
2223                my $val = <$ct>;
2224                chomp $val;
2225                close $ct;
2226                my $ctag = $_; $ctag =~ s#.*/##;
2227                $ctags->{$ctag} = $val;
2228        }
2229        closedir $dh;
2230        $ctags;
2231}
2232
2233sub git_populate_project_tagcloud {
2234        my $ctags = shift;
2235
2236        # First, merge different-cased tags; tags vote on casing
2237        my %ctags_lc;
2238        foreach (keys %$ctags) {
2239                $ctags_lc{lc $_}->{count} += $ctags->{$_};
2240                if (not $ctags_lc{lc $_}->{topcount}
2241                    or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2242                        $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2243                        $ctags_lc{lc $_}->{topname} = $_;
2244                }
2245        }
2246
2247        my $cloud;
2248        if (eval { require HTML::TagCloud; 1; }) {
2249                $cloud = HTML::TagCloud->new;
2250                foreach (sort keys %ctags_lc) {
2251                        # Pad the title with spaces so that the cloud looks
2252                        # less crammed.
2253                        my $title = $ctags_lc{$_}->{topname};
2254                        $title =~ s/ /&nbsp;/g;
2255                        $title =~ s/^/&nbsp;/g;
2256                        $title =~ s/$/&nbsp;/g;
2257                        $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2258                }
2259        } else {
2260                $cloud = \%ctags_lc;
2261        }
2262        $cloud;
2263}
2264
2265sub git_show_project_tagcloud {
2266        my ($cloud, $count) = @_;
2267        print STDERR ref($cloud)."..\n";
2268        if (ref $cloud eq 'HTML::TagCloud') {
2269                return $cloud->html_and_css($count);
2270        } else {
2271                my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2272                return '<p align="center">' . join (', ', map {
2273                        "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2274                } splice(@tags, 0, $count)) . '</p>';
2275        }
2276}
2277
2278sub git_get_project_url_list {
2279        my $path = shift;
2280
2281        $git_dir = "$projectroot/$path";
2282        open my $fd, '<', "$git_dir/cloneurl"
2283                or return wantarray ?
2284                @{ config_to_multi(git_get_project_config('url')) } :
2285                   config_to_multi(git_get_project_config('url'));
2286        my @git_project_url_list = map { chomp; $_ } <$fd>;
2287        close $fd;
2288
2289        return wantarray ? @git_project_url_list : \@git_project_url_list;
2290}
2291
2292sub git_get_projects_list {
2293        my ($filter) = @_;
2294        my @list;
2295
2296        $filter ||= '';
2297        $filter =~ s/\.git$//;
2298
2299        my $check_forks = gitweb_check_feature('forks');
2300
2301        if (-d $projects_list) {
2302                # search in directory
2303                my $dir = $projects_list . ($filter ? "/$filter" : '');
2304                # remove the trailing "/"
2305                $dir =~ s!/+$!!;
2306                my $pfxlen = length("$dir");
2307                my $pfxdepth = ($dir =~ tr!/!!);
2308
2309                File::Find::find({
2310                        follow_fast => 1, # follow symbolic links
2311                        follow_skip => 2, # ignore duplicates
2312                        dangling_symlinks => 0, # ignore dangling symlinks, silently
2313                        wanted => sub {
2314                                # skip project-list toplevel, if we get it.
2315                                return if (m!^[/.]$!);
2316                                # only directories can be git repositories
2317                                return unless (-d $_);
2318                                # don't traverse too deep (Find is super slow on os x)
2319                                if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2320                                        $File::Find::prune = 1;
2321                                        return;
2322                                }
2323
2324                                my $subdir = substr($File::Find::name, $pfxlen + 1);
2325                                # we check related file in $projectroot
2326                                my $path = ($filter ? "$filter/" : '') . $subdir;
2327                                if (check_export_ok("$projectroot/$path")) {
2328                                        push @list, { path => $path };
2329                                        $File::Find::prune = 1;
2330                                }
2331                        },
2332                }, "$dir");
2333
2334        } elsif (-f $projects_list) {
2335                # read from file(url-encoded):
2336                # 'git%2Fgit.git Linus+Torvalds'
2337                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2338                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2339                my %paths;
2340                open my $fd, '<', $projects_list or return;
2341        PROJECT:
2342                while (my $line = <$fd>) {
2343                        chomp $line;
2344                        my ($path, $owner) = split ' ', $line;
2345                        $path = unescape($path);
2346                        $owner = unescape($owner);
2347                        if (!defined $path) {
2348                                next;
2349                        }
2350                        if ($filter ne '') {
2351                                # looking for forks;
2352                                my $pfx = substr($path, 0, length($filter));
2353                                if ($pfx ne $filter) {
2354                                        next PROJECT;
2355                                }
2356                                my $sfx = substr($path, length($filter));
2357                                if ($sfx !~ /^\/.*\.git$/) {
2358                                        next PROJECT;
2359                                }
2360                        } elsif ($check_forks) {
2361                        PATH:
2362                                foreach my $filter (keys %paths) {
2363                                        # looking for forks;
2364                                        my $pfx = substr($path, 0, length($filter));
2365                                        if ($pfx ne $filter) {
2366                                                next PATH;
2367                                        }
2368                                        my $sfx = substr($path, length($filter));
2369                                        if ($sfx !~ /^\/.*\.git$/) {
2370                                                next PATH;
2371                                        }
2372                                        # is a fork, don't include it in
2373                                        # the list
2374                                        next PROJECT;
2375                                }
2376                        }
2377                        if (check_export_ok("$projectroot/$path")) {
2378                                my $pr = {
2379                                        path => $path,
2380                                        owner => to_utf8($owner),
2381                                };
2382                                push @list, $pr;
2383                                (my $forks_path = $path) =~ s/\.git$//;
2384                                $paths{$forks_path}++;
2385                        }
2386                }
2387                close $fd;
2388        }
2389        return @list;
2390}
2391
2392our $gitweb_project_owner = undef;
2393sub git_get_project_list_from_file {
2394
2395        return if (defined $gitweb_project_owner);
2396
2397        $gitweb_project_owner = {};
2398        # read from file (url-encoded):
2399        # 'git%2Fgit.git Linus+Torvalds'
2400        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2401        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2402        if (-f $projects_list) {
2403                open(my $fd, '<', $projects_list);
2404                while (my $line = <$fd>) {
2405                        chomp $line;
2406                        my ($pr, $ow) = split ' ', $line;
2407                        $pr = unescape($pr);
2408                        $ow = unescape($ow);
2409                        $gitweb_project_owner->{$pr} = to_utf8($ow);
2410                }
2411                close $fd;
2412        }
2413}
2414
2415sub git_get_project_owner {
2416        my $project = shift;
2417        my $owner;
2418
2419        return undef unless $project;
2420        $git_dir = "$projectroot/$project";
2421
2422        if (!defined $gitweb_project_owner) {
2423                git_get_project_list_from_file();
2424        }
2425
2426        if (exists $gitweb_project_owner->{$project}) {
2427                $owner = $gitweb_project_owner->{$project};
2428        }
2429        if (!defined $owner){
2430                $owner = git_get_project_config('owner');
2431        }
2432        if (!defined $owner) {
2433                $owner = get_file_owner("$git_dir");
2434        }
2435
2436        return $owner;
2437}
2438
2439sub git_get_last_activity {
2440        my ($path) = @_;
2441        my $fd;
2442
2443        $git_dir = "$projectroot/$path";
2444        open($fd, "-|", git_cmd(), 'for-each-ref',
2445             '--format=%(committer)',
2446             '--sort=-committerdate',
2447             '--count=1',
2448             'refs/heads') or return;
2449        my $most_recent = <$fd>;
2450        close $fd or return;
2451        if (defined $most_recent &&
2452            $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2453                my $timestamp = $1;
2454                my $age = time - $timestamp;
2455                return ($age, age_string($age));
2456        }
2457        return (undef, undef);
2458}
2459
2460sub git_get_references {
2461        my $type = shift || "";
2462        my %refs;
2463        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2464        # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2465        open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2466                ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2467                or return;
2468
2469        while (my $line = <$fd>) {
2470                chomp $line;
2471                if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2472                        if (defined $refs{$1}) {
2473                                push @{$refs{$1}}, $2;
2474                        } else {
2475                                $refs{$1} = [ $2 ];
2476                        }
2477                }
2478        }
2479        close $fd or return;
2480        return \%refs;
2481}
2482
2483sub git_get_rev_name_tags {
2484        my $hash = shift || return undef;
2485
2486        open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2487                or return;
2488        my $name_rev = <$fd>;
2489        close $fd;
2490
2491        if ($name_rev =~ m|^$hash tags/(.*)$|) {
2492                return $1;
2493        } else {
2494                # catches also '$hash undefined' output
2495                return undef;
2496        }
2497}
2498
2499## ----------------------------------------------------------------------
2500## parse to hash functions
2501
2502sub parse_date {
2503        my $epoch = shift;
2504        my $tz = shift || "-0000";
2505
2506        my %date;
2507        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2508        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2509        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2510        $date{'hour'} = $hour;
2511        $date{'minute'} = $min;
2512        $date{'mday'} = $mday;
2513        $date{'day'} = $days[$wday];
2514        $date{'month'} = $months[$mon];
2515        $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2516                             $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2517        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2518                             $mday, $months[$mon], $hour ,$min;
2519        $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2520                             1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2521
2522        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2523        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2524        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2525        $date{'hour_local'} = $hour;
2526        $date{'minute_local'} = $min;
2527        $date{'tz_local'} = $tz;
2528        $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2529                                  1900+$year, $mon+1, $mday,
2530                                  $hour, $min, $sec, $tz);
2531        return %date;
2532}
2533
2534sub parse_tag {
2535        my $tag_id = shift;
2536        my %tag;
2537        my @comment;
2538
2539        open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2540        $tag{'id'} = $tag_id;
2541        while (my $line = <$fd>) {
2542                chomp $line;
2543                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2544                        $tag{'object'} = $1;
2545                } elsif ($line =~ m/^type (.+)$/) {
2546                        $tag{'type'} = $1;
2547                } elsif ($line =~ m/^tag (.+)$/) {
2548                        $tag{'name'} = $1;
2549                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2550                        $tag{'author'} = $1;
2551                        $tag{'author_epoch'} = $2;
2552                        $tag{'author_tz'} = $3;
2553                        if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2554                                $tag{'author_name'}  = $1;
2555                                $tag{'author_email'} = $2;
2556                        } else {
2557                                $tag{'author_name'} = $tag{'author'};
2558                        }
2559                } elsif ($line =~ m/--BEGIN/) {
2560                        push @comment, $line;
2561                        last;
2562                } elsif ($line eq "") {
2563                        last;
2564                }
2565        }
2566        push @comment, <$fd>;
2567        $tag{'comment'} = \@comment;
2568        close $fd or return;
2569        if (!defined $tag{'name'}) {
2570                return
2571        };
2572        return %tag
2573}
2574
2575sub parse_commit_text {
2576        my ($commit_text, $withparents) = @_;
2577        my @commit_lines = split '\n', $commit_text;
2578        my %co;
2579
2580        pop @commit_lines; # Remove '\0'
2581
2582        if (! @commit_lines) {
2583                return;
2584        }
2585
2586        my $header = shift @commit_lines;
2587        if ($header !~ m/^[0-9a-fA-F]{40}/) {
2588                return;
2589        }
2590        ($co{'id'}, my @parents) = split ' ', $header;
2591        while (my $line = shift @commit_lines) {
2592                last if $line eq "\n";
2593                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2594                        $co{'tree'} = $1;
2595                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2596                        push @parents, $1;
2597                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2598                        $co{'author'} = to_utf8($1);
2599                        $co{'author_epoch'} = $2;
2600                        $co{'author_tz'} = $3;
2601                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2602                                $co{'author_name'}  = $1;
2603                                $co{'author_email'} = $2;
2604                        } else {
2605                                $co{'author_name'} = $co{'author'};
2606                        }
2607                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2608                        $co{'committer'} = to_utf8($1);
2609                        $co{'committer_epoch'} = $2;
2610                        $co{'committer_tz'} = $3;
2611                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2612                                $co{'committer_name'}  = $1;
2613                                $co{'committer_email'} = $2;
2614                        } else {
2615                                $co{'committer_name'} = $co{'committer'};
2616                        }
2617                }
2618        }
2619        if (!defined $co{'tree'}) {
2620                return;
2621        };
2622        $co{'parents'} = \@parents;
2623        $co{'parent'} = $parents[0];
2624
2625        foreach my $title (@commit_lines) {
2626                $title =~ s/^    //;
2627                if ($title ne "") {
2628                        $co{'title'} = chop_str($title, 80, 5);
2629                        # remove leading stuff of merges to make the interesting part visible
2630                        if (length($title) > 50) {
2631                                $title =~ s/^Automatic //;
2632                                $title =~ s/^merge (of|with) /Merge ... /i;
2633                                if (length($title) > 50) {
2634                                        $title =~ s/(http|rsync):\/\///;
2635                                }
2636                                if (length($title) > 50) {
2637                                        $title =~ s/(master|www|rsync)\.//;
2638                                }
2639                                if (length($title) > 50) {
2640                                        $title =~ s/kernel.org:?//;
2641                                }
2642                                if (length($title) > 50) {
2643                                        $title =~ s/\/pub\/scm//;
2644                                }
2645                        }
2646                        $co{'title_short'} = chop_str($title, 50, 5);
2647                        last;
2648                }
2649        }
2650        if (! defined $co{'title'} || $co{'title'} eq "") {
2651                $co{'title'} = $co{'title_short'} = '(no commit message)';
2652        }
2653        # remove added spaces
2654        foreach my $line (@commit_lines) {
2655                $line =~ s/^    //;
2656        }
2657        $co{'comment'} = \@commit_lines;
2658
2659        my $age = time - $co{'committer_epoch'};
2660        $co{'age'} = $age;
2661        $co{'age_string'} = age_string($age);
2662        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2663        if ($age > 60*60*24*7*2) {
2664                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2665                $co{'age_string_age'} = $co{'age_string'};
2666        } else {
2667                $co{'age_string_date'} = $co{'age_string'};
2668                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2669        }
2670        return %co;
2671}
2672
2673sub parse_commit {
2674        my ($commit_id) = @_;
2675        my %co;
2676
2677        local $/ = "\0";
2678
2679        open my $fd, "-|", git_cmd(), "rev-list",
2680                "--parents",
2681                "--header",
2682                "--max-count=1",
2683                $commit_id,
2684                "--",
2685                or die_error(500, "Open git-rev-list failed");
2686        %co = parse_commit_text(<$fd>, 1);
2687        close $fd;
2688
2689        return %co;
2690}
2691
2692sub parse_commits {
2693        my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2694        my @cos;
2695
2696        $maxcount ||= 1;
2697        $skip ||= 0;
2698
2699        local $/ = "\0";
2700
2701        open my $fd, "-|", git_cmd(), "rev-list",
2702                "--header",
2703                @args,
2704                ("--max-count=" . $maxcount),
2705                ("--skip=" . $skip),
2706                @extra_options,
2707                $commit_id,
2708                "--",
2709                ($filename ? ($filename) : ())
2710                or die_error(500, "Open git-rev-list failed");
2711        while (my $line = <$fd>) {
2712                my %co = parse_commit_text($line);
2713                push @cos, \%co;
2714        }
2715        close $fd;
2716
2717        return wantarray ? @cos : \@cos;
2718}
2719
2720# parse line of git-diff-tree "raw" output
2721sub parse_difftree_raw_line {
2722        my $line = shift;
2723        my %res;
2724
2725        # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2726        # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2727        if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2728                $res{'from_mode'} = $1;
2729                $res{'to_mode'} = $2;
2730                $res{'from_id'} = $3;
2731                $res{'to_id'} = $4;
2732                $res{'status'} = $5;
2733                $res{'similarity'} = $6;
2734                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2735                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2736                } else {
2737                        $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2738                }
2739        }
2740        # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2741        # combined diff (for merge commit)
2742        elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2743                $res{'nparents'}  = length($1);
2744                $res{'from_mode'} = [ split(' ', $2) ];
2745                $res{'to_mode'} = pop @{$res{'from_mode'}};
2746                $res{'from_id'} = [ split(' ', $3) ];
2747                $res{'to_id'} = pop @{$res{'from_id'}};
2748                $res{'status'} = [ split('', $4) ];
2749                $res{'to_file'} = unquote($5);
2750        }
2751        # 'c512b523472485aef4fff9e57b229d9d243c967f'
2752        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2753                $res{'commit'} = $1;
2754        }
2755
2756        return wantarray ? %res : \%res;
2757}
2758
2759# wrapper: return parsed line of git-diff-tree "raw" output
2760# (the argument might be raw line, or parsed info)
2761sub parsed_difftree_line {
2762        my $line_or_ref = shift;
2763
2764        if (ref($line_or_ref) eq "HASH") {
2765                # pre-parsed (or generated by hand)
2766                return $line_or_ref;
2767        } else {
2768                return parse_difftree_raw_line($line_or_ref);
2769        }
2770}
2771
2772# parse line of git-ls-tree output
2773sub parse_ls_tree_line {
2774        my $line = shift;
2775        my %opts = @_;
2776        my %res;
2777
2778        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2779        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2780
2781        $res{'mode'} = $1;
2782        $res{'type'} = $2;
2783        $res{'hash'} = $3;
2784        if ($opts{'-z'}) {
2785                $res{'name'} = $4;
2786        } else {
2787                $res{'name'} = unquote($4);
2788        }
2789
2790        return wantarray ? %res : \%res;
2791}
2792
2793# generates _two_ hashes, references to which are passed as 2 and 3 argument
2794sub parse_from_to_diffinfo {
2795        my ($diffinfo, $from, $to, @parents) = @_;
2796
2797        if ($diffinfo->{'nparents'}) {
2798                # combined diff
2799                $from->{'file'} = [];
2800                $from->{'href'} = [];
2801                fill_from_file_info($diffinfo, @parents)
2802                        unless exists $diffinfo->{'from_file'};
2803                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2804                        $from->{'file'}[$i] =
2805                                defined $diffinfo->{'from_file'}[$i] ?
2806                                        $diffinfo->{'from_file'}[$i] :
2807                                        $diffinfo->{'to_file'};
2808                        if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2809                                $from->{'href'}[$i] = href(action=>"blob",
2810                                                           hash_base=>$parents[$i],
2811                                                           hash=>$diffinfo->{'from_id'}[$i],
2812                                                           file_name=>$from->{'file'}[$i]);
2813                        } else {
2814                                $from->{'href'}[$i] = undef;
2815                        }
2816                }
2817        } else {
2818                # ordinary (not combined) diff
2819                $from->{'file'} = $diffinfo->{'from_file'};
2820                if ($diffinfo->{'status'} ne "A") { # not new (added) file
2821                        $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2822                                               hash=>$diffinfo->{'from_id'},
2823                                               file_name=>$from->{'file'});
2824                } else {
2825                        delete $from->{'href'};
2826                }
2827        }
2828
2829        $to->{'file'} = $diffinfo->{'to_file'};
2830        if (!is_deleted($diffinfo)) { # file exists in result
2831                $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2832                                     hash=>$diffinfo->{'to_id'},
2833                                     file_name=>$to->{'file'});
2834        } else {
2835                delete $to->{'href'};
2836        }
2837}
2838
2839## ......................................................................
2840## parse to array of hashes functions
2841
2842sub git_get_heads_list {
2843        my $limit = shift;
2844        my @headslist;
2845
2846        open my $fd, '-|', git_cmd(), 'for-each-ref',
2847                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2848                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2849                'refs/heads'
2850                or return;
2851        while (my $line = <$fd>) {
2852                my %ref_item;
2853
2854                chomp $line;
2855                my ($refinfo, $committerinfo) = split(/\0/, $line);
2856                my ($hash, $name, $title) = split(' ', $refinfo, 3);
2857                my ($committer, $epoch, $tz) =
2858                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2859                $ref_item{'fullname'}  = $name;
2860                $name =~ s!^refs/heads/!!;
2861
2862                $ref_item{'name'}  = $name;
2863                $ref_item{'id'}    = $hash;
2864                $ref_item{'title'} = $title || '(no commit message)';
2865                $ref_item{'epoch'} = $epoch;
2866                if ($epoch) {
2867                        $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2868                } else {
2869                        $ref_item{'age'} = "unknown";
2870                }
2871
2872                push @headslist, \%ref_item;
2873        }
2874        close $fd;
2875
2876        return wantarray ? @headslist : \@headslist;
2877}
2878
2879sub git_get_tags_list {
2880        my $limit = shift;
2881        my @tagslist;
2882
2883        open my $fd, '-|', git_cmd(), 'for-each-ref',
2884                ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2885                '--format=%(objectname) %(objecttype) %(refname) '.
2886                '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2887                'refs/tags'
2888                or return;
2889        while (my $line = <$fd>) {
2890                my %ref_item;
2891
2892                chomp $line;
2893                my ($refinfo, $creatorinfo) = split(/\0/, $line);
2894                my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2895                my ($creator, $epoch, $tz) =
2896                        ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2897                $ref_item{'fullname'} = $name;
2898                $name =~ s!^refs/tags/!!;
2899
2900                $ref_item{'type'} = $type;
2901                $ref_item{'id'} = $id;
2902                $ref_item{'name'} = $name;
2903                if ($type eq "tag") {
2904                        $ref_item{'subject'} = $title;
2905                        $ref_item{'reftype'} = $reftype;
2906                        $ref_item{'refid'}   = $refid;
2907                } else {
2908                        $ref_item{'reftype'} = $type;
2909                        $ref_item{'refid'}   = $id;
2910                }
2911
2912                if ($type eq "tag" || $type eq "commit") {
2913                        $ref_item{'epoch'} = $epoch;
2914                        if ($epoch) {
2915                                $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2916                        } else {
2917                                $ref_item{'age'} = "unknown";
2918                        }
2919                }
2920
2921                push @tagslist, \%ref_item;
2922        }
2923        close $fd;
2924
2925        return wantarray ? @tagslist : \@tagslist;
2926}
2927
2928## ----------------------------------------------------------------------
2929## filesystem-related functions
2930
2931sub get_file_owner {
2932        my $path = shift;
2933
2934        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2935        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2936        if (!defined $gcos) {
2937                return undef;
2938        }
2939        my $owner = $gcos;
2940        $owner =~ s/[,;].*$//;
2941        return to_utf8($owner);
2942}
2943
2944# assume that file exists
2945sub insert_file {
2946        my $filename = shift;
2947
2948        open my $fd, '<', $filename;
2949        print map { to_utf8($_) } <$fd>;
2950        close $fd;
2951}
2952
2953## ......................................................................
2954## mimetype related functions
2955
2956sub mimetype_guess_file {
2957        my $filename = shift;
2958        my $mimemap = shift;
2959        -r $mimemap or return undef;
2960
2961        my %mimemap;
2962        open(my $mh, '<', $mimemap) or return undef;
2963        while (<$mh>) {
2964                next if m/^#/; # skip comments
2965                my ($mimetype, $exts) = split(/\t+/);
2966                if (defined $exts) {
2967                        my @exts = split(/\s+/, $exts);
2968                        foreach my $ext (@exts) {
2969                                $mimemap{$ext} = $mimetype;
2970                        }
2971                }
2972        }
2973        close($mh);
2974
2975        $filename =~ /\.([^.]*)$/;
2976        return $mimemap{$1};
2977}
2978
2979sub mimetype_guess {
2980        my $filename = shift;
2981        my $mime;
2982        $filename =~ /\./ or return undef;
2983
2984        if ($mimetypes_file) {
2985                my $file = $mimetypes_file;
2986                if ($file !~ m!^/!) { # if it is relative path
2987                        # it is relative to project
2988                        $file = "$projectroot/$project/$file";
2989                }
2990                $mime = mimetype_guess_file($filename, $file);
2991        }
2992        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2993        return $mime;
2994}
2995
2996sub blob_mimetype {
2997        my $fd = shift;
2998        my $filename = shift;
2999
3000        if ($filename) {
3001                my $mime = mimetype_guess($filename);
3002                $mime and return $mime;
3003        }
3004
3005        # just in case
3006        return $default_blob_plain_mimetype unless $fd;
3007
3008        if (-T $fd) {
3009                return 'text/plain';
3010        } elsif (! $filename) {
3011                return 'application/octet-stream';
3012        } elsif ($filename =~ m/\.png$/i) {
3013                return 'image/png';
3014        } elsif ($filename =~ m/\.gif$/i) {
3015                return 'image/gif';
3016        } elsif ($filename =~ m/\.jpe?g$/i) {
3017                return 'image/jpeg';
3018        } else {
3019                return 'application/octet-stream';
3020        }
3021}
3022
3023sub blob_contenttype {
3024        my ($fd, $file_name, $type) = @_;
3025
3026        $type ||= blob_mimetype($fd, $file_name);
3027        if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3028                $type .= "; charset=$default_text_plain_charset";
3029        }
3030
3031        return $type;
3032}
3033
3034## ======================================================================
3035## functions printing HTML: header, footer, error page
3036
3037sub git_header_html {
3038        my $status = shift || "200 OK";
3039        my $expires = shift;
3040
3041        my $title = "$site_name";
3042        if (defined $project) {
3043                $title .= " - " . to_utf8($project);
3044                if (defined $action) {
3045                        $title .= "/$action";
3046                        if (defined $file_name) {
3047                                $title .= " - " . esc_path($file_name);
3048                                if ($action eq "tree" && $file_name !~ m|/$|) {
3049                                        $title .= "/";
3050                                }
3051                        }
3052                }
3053        }
3054        my $content_type;
3055        # require explicit support from the UA if we are to send the page as
3056        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3057        # we have to do this because MSIE sometimes globs '*/*', pretending to
3058        # support xhtml+xml but choking when it gets what it asked for.
3059        if (defined $cgi->http('HTTP_ACCEPT') &&
3060            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3061            $cgi->Accept('application/xhtml+xml') != 0) {
3062                $content_type = 'application/xhtml+xml';
3063        } else {
3064                $content_type = 'text/html';
3065        }
3066        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3067                           -status=> $status, -expires => $expires);
3068        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3069        print <<EOF;
3070<?xml version="1.0" encoding="utf-8"?>
3071<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3072<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3073<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3074<!-- git core binaries version $git_version -->
3075<head>
3076<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3077<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3078<meta name="robots" content="index, nofollow"/>
3079<title>$title</title>
3080EOF
3081        # the stylesheet, favicon etc urls won't work correctly with path_info
3082        # unless we set the appropriate base URL
3083        if ($ENV{'PATH_INFO'}) {
3084                print "<base href=\"".esc_url($base_url)."\" />\n";
3085        }
3086        # print out each stylesheet that exist, providing backwards capability
3087        # for those people who defined $stylesheet in a config file
3088        if (defined $stylesheet) {
3089                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3090        } else {
3091                foreach my $stylesheet (@stylesheets) {
3092                        next unless $stylesheet;
3093                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3094                }
3095        }
3096        if (defined $project) {
3097                my %href_params = get_feed_info();
3098                if (!exists $href_params{'-title'}) {
3099                        $href_params{'-title'} = 'log';
3100                }
3101
3102                foreach my $format qw(RSS Atom) {
3103                        my $type = lc($format);
3104                        my %link_attr = (
3105                                '-rel' => 'alternate',
3106                                '-title' => "$project - $href_params{'-title'} - $format feed",
3107                                '-type' => "application/$type+xml"
3108                        );
3109
3110                        $href_params{'action'} = $type;
3111                        $link_attr{'-href'} = href(%href_params);
3112                        print "<link ".
3113                              "rel=\"$link_attr{'-rel'}\" ".
3114                              "title=\"$link_attr{'-title'}\" ".
3115                              "href=\"$link_attr{'-href'}\" ".
3116                              "type=\"$link_attr{'-type'}\" ".
3117                              "/>\n";
3118
3119                        $href_params{'extra_options'} = '--no-merges';
3120                        $link_attr{'-href'} = href(%href_params);
3121                        $link_attr{'-title'} .= ' (no merges)';
3122                        print "<link ".
3123                              "rel=\"$link_attr{'-rel'}\" ".
3124                              "title=\"$link_attr{'-title'}\" ".
3125                              "href=\"$link_attr{'-href'}\" ".
3126                              "type=\"$link_attr{'-type'}\" ".
3127                              "/>\n";
3128                }
3129
3130        } else {
3131                printf('<link rel="alternate" title="%s projects list" '.
3132                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
3133                       $site_name, href(project=>undef, action=>"project_index"));
3134                printf('<link rel="alternate" title="%s projects feeds" '.
3135                       'href="%s" type="text/x-opml" />'."\n",
3136                       $site_name, href(project=>undef, action=>"opml"));
3137        }
3138        if (defined $favicon) {
3139                print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3140        }
3141
3142        print "</head>\n" .
3143              "<body>\n";
3144
3145        if (-f $site_header) {
3146                insert_file($site_header);
3147        }
3148
3149        print "<div class=\"page_header\">\n" .
3150              $cgi->a({-href => esc_url($logo_url),
3151                       -title => $logo_label},
3152                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3153        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3154        if (defined $project) {
3155                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3156                if (defined $action) {
3157                        print " / $action";
3158                }
3159                print "\n";
3160        }
3161        print "</div>\n";
3162
3163        my $have_search = gitweb_check_feature('search');
3164        if (defined $project && $have_search) {
3165                if (!defined $searchtext) {
3166                        $searchtext = "";
3167                }
3168                my $search_hash;
3169                if (defined $hash_base) {
3170                        $search_hash = $hash_base;
3171                } elsif (defined $hash) {
3172                        $search_hash = $hash;
3173                } else {
3174                        $search_hash = "HEAD";
3175                }
3176                my $action = $my_uri;
3177                my $use_pathinfo = gitweb_check_feature('pathinfo');
3178                if ($use_pathinfo) {
3179                        $action .= "/".esc_url($project);
3180                }
3181                print $cgi->startform(-method => "get", -action => $action) .
3182                      "<div class=\"search\">\n" .
3183                      (!$use_pathinfo &&
3184                      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3185                      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3186                      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3187                      $cgi->popup_menu(-name => 'st', -default => 'commit',
3188                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3189                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3190                      " search:\n",
3191                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3192                      "<span title=\"Extended regular expression\">" .
3193                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3194                                     -checked => $search_use_regexp) .
3195                      "</span>" .
3196                      "</div>" .
3197                      $cgi->end_form() . "\n";
3198        }
3199}
3200
3201sub git_footer_html {
3202        my $feed_class = 'rss_logo';
3203
3204        print "<div class=\"page_footer\">\n";
3205        if (defined $project) {
3206                my $descr = git_get_project_description($project);
3207                if (defined $descr) {
3208                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3209                }
3210
3211                my %href_params = get_feed_info();
3212                if (!%href_params) {
3213                        $feed_class .= ' generic';
3214                }
3215                $href_params{'-title'} ||= 'log';
3216
3217                foreach my $format qw(RSS Atom) {
3218                        $href_params{'action'} = lc($format);
3219                        print $cgi->a({-href => href(%href_params),
3220                                      -title => "$href_params{'-title'} $format feed",
3221                                      -class => $feed_class}, $format)."\n";
3222                }
3223
3224        } else {
3225                print $cgi->a({-href => href(project=>undef, action=>"opml"),
3226                              -class => $feed_class}, "OPML") . " ";
3227                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3228                              -class => $feed_class}, "TXT") . "\n";
3229        }
3230        print "</div>\n"; # class="page_footer"
3231
3232        if (-f $site_footer) {
3233                insert_file($site_footer);
3234        }
3235
3236        print "</body>\n" .
3237              "</html>";
3238}
3239
3240# die_error(<http_status_code>, <error_message>)
3241# Example: die_error(404, 'Hash not found')
3242# By convention, use the following status codes (as defined in RFC 2616):
3243# 400: Invalid or missing CGI parameters, or
3244#      requested object exists but has wrong type.
3245# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3246#      this server or project.
3247# 404: Requested object/revision/project doesn't exist.
3248# 500: The server isn't configured properly, or
3249#      an internal error occurred (e.g. failed assertions caused by bugs), or
3250#      an unknown error occurred (e.g. the git binary died unexpectedly).
3251sub die_error {
3252        my $status = shift || 500;
3253        my $error = shift || "Internal server error";
3254
3255        my %http_responses = (400 => '400 Bad Request',
3256                              403 => '403 Forbidden',
3257                              404 => '404 Not Found',
3258                              500 => '500 Internal Server Error');
3259        git_header_html($http_responses{$status});
3260        print <<EOF;
3261<div class="page_body">
3262<br /><br />
3263$status - $error
3264<br />
3265</div>
3266EOF
3267        git_footer_html();
3268        exit;
3269}
3270
3271## ----------------------------------------------------------------------
3272## functions printing or outputting HTML: navigation
3273
3274sub git_print_page_nav {
3275        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3276        $extra = '' if !defined $extra; # pager or formats
3277
3278        my @navs = qw(summary shortlog log commit commitdiff tree);
3279        if ($suppress) {
3280                @navs = grep { $_ ne $suppress } @navs;
3281        }
3282
3283        my %arg = map { $_ => {action=>$_} } @navs;
3284        if (defined $head) {
3285                for (qw(commit commitdiff)) {
3286                        $arg{$_}{'hash'} = $head;
3287                }
3288                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3289                        for (qw(shortlog log)) {
3290                                $arg{$_}{'hash'} = $head;
3291                        }
3292                }
3293        }
3294
3295        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3296        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3297
3298        my @actions = gitweb_get_feature('actions');
3299        my %repl = (
3300                '%' => '%',
3301                'n' => $project,         # project name
3302                'f' => $git_dir,         # project path within filesystem
3303                'h' => $treehead || '',  # current hash ('h' parameter)
3304                'b' => $treebase || '',  # hash base ('hb' parameter)
3305        );
3306        while (@actions) {
3307                my ($label, $link, $pos) = splice(@actions,0,3);
3308                # insert
3309                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3310                # munch munch
3311                $link =~ s/%([%nfhb])/$repl{$1}/g;
3312                $arg{$label}{'_href'} = $link;
3313        }
3314
3315        print "<div class=\"page_nav\">\n" .
3316                (join " | ",
3317                 map { $_ eq $current ?
3318                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3319                 } @navs);
3320        print "<br/>\n$extra<br/>\n" .
3321              "</div>\n";
3322}
3323
3324sub format_paging_nav {
3325        my ($action, $hash, $head, $page, $has_next_link) = @_;
3326        my $paging_nav;
3327
3328
3329        if ($hash ne $head || $page) {
3330                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3331        } else {
3332                $paging_nav .= "HEAD";
3333        }
3334
3335        if ($page > 0) {
3336                $paging_nav .= " &sdot; " .
3337                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
3338                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3339        } else {
3340                $paging_nav .= " &sdot; prev";
3341        }
3342
3343        if ($has_next_link) {
3344                $paging_nav .= " &sdot; " .
3345                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
3346                                 -accesskey => "n", -title => "Alt-n"}, "next");
3347        } else {
3348                $paging_nav .= " &sdot; next";
3349        }
3350
3351        return $paging_nav;
3352}
3353
3354## ......................................................................
3355## functions printing or outputting HTML: div
3356
3357sub git_print_header_div {
3358        my ($action, $title, $hash, $hash_base) = @_;
3359        my %args = ();
3360
3361        $args{'action'} = $action;
3362        $args{'hash'} = $hash if $hash;
3363        $args{'hash_base'} = $hash_base if $hash_base;
3364
3365        print "<div class=\"header\">\n" .
3366              $cgi->a({-href => href(%args), -class => "title"},
3367              $title ? $title : $action) .
3368              "\n</div>\n";
3369}
3370
3371sub print_local_time {
3372        my %date = @_;
3373        if ($date{'hour_local'} < 6) {
3374                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3375                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3376        } else {
3377                printf(" (%02d:%02d %s)",
3378                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3379        }
3380}
3381
3382# Outputs the author name and date in long form
3383sub git_print_authorship {
3384        my $co = shift;
3385        my %opts = @_;
3386        my $tag = $opts{-tag} || 'div';
3387
3388        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3389        print "<$tag class=\"author_date\">" .
3390              esc_html($co->{'author_name'}) .
3391              " [$ad{'rfc2822'}";
3392        print_local_time(%ad) if ($opts{-localtime});
3393        print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3394                  . "</$tag>\n";
3395}
3396
3397# Outputs table rows containing the full author or committer information,
3398# in the format expected for 'commit' view (& similia).
3399# Parameters are a commit hash reference, followed by the list of people
3400# to output information for. If the list is empty it defalts to both
3401# author and committer.
3402sub git_print_authorship_rows {
3403        my $co = shift;
3404        # too bad we can't use @people = @_ || ('author', 'committer')
3405        my @people = @_;
3406        @people = ('author', 'committer') unless @people;
3407        foreach my $who (@people) {
3408                my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3409                print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
3410                      "<td rowspan=\"2\">" .
3411                      git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3412                      "</td></tr>\n" .
3413                      "<tr>" .
3414                      "<td></td><td> $wd{'rfc2822'}";
3415                print_local_time(%wd);
3416                print "</td>" .
3417                      "</tr>\n";
3418        }
3419}
3420
3421sub git_print_page_path {
3422        my $name = shift;
3423        my $type = shift;
3424        my $hb = shift;
3425
3426
3427        print "<div class=\"page_path\">";
3428        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3429                      -title => 'tree root'}, to_utf8("[$project]"));
3430        print " / ";
3431        if (defined $name) {
3432                my @dirname = split '/', $name;
3433                my $basename = pop @dirname;
3434                my $fullname = '';
3435
3436                foreach my $dir (@dirname) {
3437                        $fullname .= ($fullname ? '/' : '') . $dir;
3438                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3439                                                     hash_base=>$hb),
3440                                      -title => $fullname}, esc_path($dir));
3441                        print " / ";
3442                }
3443                if (defined $type && $type eq 'blob') {
3444                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3445                                                     hash_base=>$hb),
3446                                      -title => $name}, esc_path($basename));
3447                } elsif (defined $type && $type eq 'tree') {
3448                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3449                                                     hash_base=>$hb),
3450                                      -title => $name}, esc_path($basename));
3451                        print " / ";
3452                } else {
3453                        print esc_path($basename);
3454                }
3455        }
3456        print "<br/></div>\n";
3457}
3458
3459sub git_print_log {
3460        my $log = shift;
3461        my %opts = @_;
3462
3463        if ($opts{'-remove_title'}) {
3464                # remove title, i.e. first line of log
3465                shift @$log;
3466        }
3467        # remove leading empty lines
3468        while (defined $log->[0] && $log->[0] eq "") {
3469                shift @$log;
3470        }
3471
3472        # print log
3473        my $signoff = 0;
3474        my $empty = 0;
3475        foreach my $line (@$log) {
3476                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3477                        $signoff = 1;
3478                        $empty = 0;
3479                        if (! $opts{'-remove_signoff'}) {
3480                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3481                                next;
3482                        } else {
3483                                # remove signoff lines
3484                                next;
3485                        }
3486                } else {
3487                        $signoff = 0;
3488                }
3489
3490                # print only one empty line
3491                # do not print empty line after signoff
3492                if ($line eq "") {
3493                        next if ($empty || $signoff);
3494                        $empty = 1;
3495                } else {
3496                        $empty = 0;
3497                }
3498
3499                print format_log_line_html($line) . "<br/>\n";
3500        }
3501
3502        if ($opts{'-final_empty_line'}) {
3503                # end with single empty line
3504                print "<br/>\n" unless $empty;
3505        }
3506}
3507
3508# return link target (what link points to)
3509sub git_get_link_target {
3510        my $hash = shift;
3511        my $link_target;
3512
3513        # read link
3514        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3515                or return;
3516        {
3517                local $/ = undef;
3518                $link_target = <$fd>;
3519        }
3520        close $fd
3521                or return;
3522
3523        return $link_target;
3524}
3525
3526# given link target, and the directory (basedir) the link is in,
3527# return target of link relative to top directory (top tree);
3528# return undef if it is not possible (including absolute links).
3529sub normalize_link_target {
3530        my ($link_target, $basedir) = @_;
3531
3532        # absolute symlinks (beginning with '/') cannot be normalized
3533        return if (substr($link_target, 0, 1) eq '/');
3534
3535        # normalize link target to path from top (root) tree (dir)
3536        my $path;
3537        if ($basedir) {
3538                $path = $basedir . '/' . $link_target;
3539        } else {
3540                # we are in top (root) tree (dir)
3541                $path = $link_target;
3542        }
3543
3544        # remove //, /./, and /../
3545        my @path_parts;
3546        foreach my $part (split('/', $path)) {
3547                # discard '.' and ''
3548                next if (!$part || $part eq '.');
3549                # handle '..'
3550                if ($part eq '..') {
3551                        if (@path_parts) {
3552                                pop @path_parts;
3553                        } else {
3554                                # link leads outside repository (outside top dir)
3555                                return;
3556                        }
3557                } else {
3558                        push @path_parts, $part;
3559                }
3560        }
3561        $path = join('/', @path_parts);
3562
3563        return $path;
3564}
3565
3566# print tree entry (row of git_tree), but without encompassing <tr> element
3567sub git_print_tree_entry {
3568        my ($t, $basedir, $hash_base, $have_blame) = @_;
3569
3570        my %base_key = ();
3571        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3572
3573        # The format of a table row is: mode list link.  Where mode is
3574        # the mode of the entry, list is the name of the entry, an href,
3575        # and link is the action links of the entry.
3576
3577        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3578        if ($t->{'type'} eq "blob") {
3579                print "<td class=\"list\">" .
3580                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3581                                               file_name=>"$basedir$t->{'name'}", %base_key),
3582                                -class => "list"}, esc_path($t->{'name'}));
3583                if (S_ISLNK(oct $t->{'mode'})) {
3584                        my $link_target = git_get_link_target($t->{'hash'});
3585                        if ($link_target) {
3586                                my $norm_target = normalize_link_target($link_target, $basedir);
3587                                if (defined $norm_target) {
3588                                        print " -> " .
3589                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3590                                                                     file_name=>$norm_target),
3591                                                       -title => $norm_target}, esc_path($link_target));
3592                                } else {
3593                                        print " -> " . esc_path($link_target);
3594                                }
3595                        }
3596                }
3597                print "</td>\n";
3598                print "<td class=\"link\">";
3599                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3600                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3601                              "blob");
3602                if ($have_blame) {
3603                        print " | " .
3604                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3605                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3606                                      "blame");
3607                }
3608                if (defined $hash_base) {
3609                        print " | " .
3610                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3611                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3612                                      "history");
3613                }
3614                print " | " .
3615                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3616                                               file_name=>"$basedir$t->{'name'}")},
3617                                "raw");
3618                print "</td>\n";
3619
3620        } elsif ($t->{'type'} eq "tree") {
3621                print "<td class=\"list\">";
3622                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3623                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3624                              esc_path($t->{'name'}));
3625                print "</td>\n";
3626                print "<td class=\"link\">";
3627                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3628                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3629                              "tree");
3630                if (defined $hash_base) {
3631                        print " | " .
3632                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3633                                                     file_name=>"$basedir$t->{'name'}")},
3634                                      "history");
3635                }
3636                print "</td>\n";
3637        } else {
3638                # unknown object: we can only present history for it
3639                # (this includes 'commit' object, i.e. submodule support)
3640                print "<td class=\"list\">" .
3641                      esc_path($t->{'name'}) .
3642                      "</td>\n";
3643                print "<td class=\"link\">";
3644                if (defined $hash_base) {
3645                        print $cgi->a({-href => href(action=>"history",
3646                                                     hash_base=>$hash_base,
3647                                                     file_name=>"$basedir$t->{'name'}")},
3648                                      "history");
3649                }
3650                print "</td>\n";
3651        }
3652}
3653
3654## ......................................................................
3655## functions printing large fragments of HTML
3656
3657# get pre-image filenames for merge (combined) diff
3658sub fill_from_file_info {
3659        my ($diff, @parents) = @_;
3660
3661        $diff->{'from_file'} = [ ];
3662        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3663        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3664                if ($diff->{'status'}[$i] eq 'R' ||
3665                    $diff->{'status'}[$i] eq 'C') {
3666                        $diff->{'from_file'}[$i] =
3667                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3668                }
3669        }
3670
3671        return $diff;
3672}
3673
3674# is current raw difftree line of file deletion
3675sub is_deleted {
3676        my $diffinfo = shift;
3677
3678        return $diffinfo->{'to_id'} eq ('0' x 40);
3679}
3680
3681# does patch correspond to [previous] difftree raw line
3682# $diffinfo  - hashref of parsed raw diff format
3683# $patchinfo - hashref of parsed patch diff format
3684#              (the same keys as in $diffinfo)
3685sub is_patch_split {
3686        my ($diffinfo, $patchinfo) = @_;
3687
3688        return defined $diffinfo && defined $patchinfo
3689                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3690}
3691
3692
3693sub git_difftree_body {
3694        my ($difftree, $hash, @parents) = @_;
3695        my ($parent) = $parents[0];
3696        my $have_blame = gitweb_check_feature('blame');
3697        print "<div class=\"list_head\">\n";
3698        if ($#{$difftree} > 10) {
3699                print(($#{$difftree} + 1) . " files changed:\n");
3700        }
3701        print "</div>\n";
3702
3703        print "<table class=\"" .
3704              (@parents > 1 ? "combined " : "") .
3705              "diff_tree\">\n";
3706
3707        # header only for combined diff in 'commitdiff' view
3708        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3709        if ($has_header) {
3710                # table header
3711                print "<thead><tr>\n" .
3712                       "<th></th><th></th>\n"; # filename, patchN link
3713                for (my $i = 0; $i < @parents; $i++) {
3714                        my $par = $parents[$i];
3715                        print "<th>" .
3716                              $cgi->a({-href => href(action=>"commitdiff",
3717                                                     hash=>$hash, hash_parent=>$par),
3718                                       -title => 'commitdiff to parent number ' .
3719                                                  ($i+1) . ': ' . substr($par,0,7)},
3720                                      $i+1) .
3721                              "&nbsp;</th>\n";
3722                }
3723                print "</tr></thead>\n<tbody>\n";
3724        }
3725
3726        my $alternate = 1;
3727        my $patchno = 0;
3728        foreach my $line (@{$difftree}) {
3729                my $diff = parsed_difftree_line($line);
3730
3731                if ($alternate) {
3732                        print "<tr class=\"dark\">\n";
3733                } else {
3734                        print "<tr class=\"light\">\n";
3735                }
3736                $alternate ^= 1;
3737
3738                if (exists $diff->{'nparents'}) { # combined diff
3739
3740                        fill_from_file_info($diff, @parents)
3741                                unless exists $diff->{'from_file'};
3742
3743                        if (!is_deleted($diff)) {
3744                                # file exists in the result (child) commit
3745                                print "<td>" .
3746                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3747                                                             file_name=>$diff->{'to_file'},
3748                                                             hash_base=>$hash),
3749                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3750                                      "</td>\n";
3751                        } else {
3752                                print "<td>" .
3753                                      esc_path($diff->{'to_file'}) .
3754                                      "</td>\n";
3755                        }
3756
3757                        if ($action eq 'commitdiff') {
3758                                # link to patch
3759                                $patchno++;
3760                                print "<td class=\"link\">" .
3761                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3762                                      " | " .
3763                                      "</td>\n";
3764                        }
3765
3766                        my $has_history = 0;
3767                        my $not_deleted = 0;
3768                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3769                                my $hash_parent = $parents[$i];
3770                                my $from_hash = $diff->{'from_id'}[$i];
3771                                my $from_path = $diff->{'from_file'}[$i];
3772                                my $status = $diff->{'status'}[$i];
3773
3774                                $has_history ||= ($status ne 'A');
3775                                $not_deleted ||= ($status ne 'D');
3776
3777                                if ($status eq 'A') {
3778                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3779                                } elsif ($status eq 'D') {
3780                                        print "<td class=\"link\">" .
3781                                              $cgi->a({-href => href(action=>"blob",
3782                                                                     hash_base=>$hash,
3783                                                                     hash=>$from_hash,
3784                                                                     file_name=>$from_path)},
3785                                                      "blob" . ($i+1)) .
3786                                              " | </td>\n";
3787                                } else {
3788                                        if ($diff->{'to_id'} eq $from_hash) {
3789                                                print "<td class=\"link nochange\">";
3790                                        } else {
3791                                                print "<td class=\"link\">";
3792                                        }
3793                                        print $cgi->a({-href => href(action=>"blobdiff",
3794                                                                     hash=>$diff->{'to_id'},
3795                                                                     hash_parent=>$from_hash,
3796                                                                     hash_base=>$hash,
3797                                                                     hash_parent_base=>$hash_parent,
3798                                                                     file_name=>$diff->{'to_file'},
3799                                                                     file_parent=>$from_path)},
3800                                                      "diff" . ($i+1)) .
3801                                              " | </td>\n";
3802                                }
3803                        }
3804
3805                        print "<td class=\"link\">";
3806                        if ($not_deleted) {
3807                                print $cgi->a({-href => href(action=>"blob",
3808                                                             hash=>$diff->{'to_id'},
3809                                                             file_name=>$diff->{'to_file'},
3810                                                             hash_base=>$hash)},
3811                                              "blob");
3812                                print " | " if ($has_history);
3813                        }
3814                        if ($has_history) {
3815                                print $cgi->a({-href => href(action=>"history",
3816                                                             file_name=>$diff->{'to_file'},
3817                                                             hash_base=>$hash)},
3818                                              "history");
3819                        }
3820                        print "</td>\n";
3821
3822                        print "</tr>\n";
3823                        next; # instead of 'else' clause, to avoid extra indent
3824                }
3825                # else ordinary diff
3826
3827                my ($to_mode_oct, $to_mode_str, $to_file_type);
3828                my ($from_mode_oct, $from_mode_str, $from_file_type);
3829                if ($diff->{'to_mode'} ne ('0' x 6)) {
3830                        $to_mode_oct = oct $diff->{'to_mode'};
3831                        if (S_ISREG($to_mode_oct)) { # only for regular file
3832                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3833                        }
3834                        $to_file_type = file_type($diff->{'to_mode'});
3835                }
3836                if ($diff->{'from_mode'} ne ('0' x 6)) {
3837                        $from_mode_oct = oct $diff->{'from_mode'};
3838                        if (S_ISREG($to_mode_oct)) { # only for regular file
3839                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3840                        }
3841                        $from_file_type = file_type($diff->{'from_mode'});
3842                }
3843
3844                if ($diff->{'status'} eq "A") { # created
3845                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3846                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3847                        $mode_chng   .= "]</span>";
3848                        print "<td>";
3849                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3850                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3851                                      -class => "list"}, esc_path($diff->{'file'}));
3852                        print "</td>\n";
3853                        print "<td>$mode_chng</td>\n";
3854                        print "<td class=\"link\">";
3855                        if ($action eq 'commitdiff') {
3856                                # link to patch
3857                                $patchno++;
3858                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3859                                print " | ";
3860                        }
3861                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3862                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3863                                      "blob");
3864                        print "</td>\n";
3865
3866                } elsif ($diff->{'status'} eq "D") { # deleted
3867                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3868                        print "<td>";
3869                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3870                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3871                                       -class => "list"}, esc_path($diff->{'file'}));
3872                        print "</td>\n";
3873                        print "<td>$mode_chng</td>\n";
3874                        print "<td class=\"link\">";
3875                        if ($action eq 'commitdiff') {
3876                                # link to patch
3877                                $patchno++;
3878                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3879                                print " | ";
3880                        }
3881                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3882                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3883                                      "blob") . " | ";
3884                        if ($have_blame) {
3885                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3886                                                             file_name=>$diff->{'file'})},
3887                                              "blame") . " | ";
3888                        }
3889                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3890                                                     file_name=>$diff->{'file'})},
3891                                      "history");
3892                        print "</td>\n";
3893
3894                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3895                        my $mode_chnge = "";
3896                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3897                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3898                                if ($from_file_type ne $to_file_type) {
3899                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3900                                }
3901                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3902                                        if ($from_mode_str && $to_mode_str) {
3903                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3904                                        } elsif ($to_mode_str) {
3905                                                $mode_chnge .= " mode: $to_mode_str";
3906                                        }
3907                                }
3908                                $mode_chnge .= "]</span>\n";
3909                        }
3910                        print "<td>";
3911                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3912                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3913                                      -class => "list"}, esc_path($diff->{'file'}));
3914                        print "</td>\n";
3915                        print "<td>$mode_chnge</td>\n";
3916                        print "<td class=\"link\">";
3917                        if ($action eq 'commitdiff') {
3918                                # link to patch
3919                                $patchno++;
3920                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3921                                      " | ";
3922                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3923                                # "commit" view and modified file (not onlu mode changed)
3924                                print $cgi->a({-href => href(action=>"blobdiff",
3925                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3926                                                             hash_base=>$hash, hash_parent_base=>$parent,
3927                                                             file_name=>$diff->{'file'})},
3928                                              "diff") .
3929                                      " | ";
3930                        }
3931                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3932                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3933                                       "blob") . " | ";
3934                        if ($have_blame) {
3935                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3936                                                             file_name=>$diff->{'file'})},
3937                                              "blame") . " | ";
3938                        }
3939                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3940                                                     file_name=>$diff->{'file'})},
3941                                      "history");
3942                        print "</td>\n";
3943
3944                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3945                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3946                        my $nstatus = $status_name{$diff->{'status'}};
3947                        my $mode_chng = "";
3948                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3949                                # mode also for directories, so we cannot use $to_mode_str
3950                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3951                        }
3952                        print "<td>" .
3953                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3954                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3955                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3956                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3957                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3958                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3959                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3960                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3961                              "<td class=\"link\">";
3962                        if ($action eq 'commitdiff') {
3963                                # link to patch
3964                                $patchno++;
3965                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3966                                      " | ";
3967                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3968                                # "commit" view and modified file (not only pure rename or copy)
3969                                print $cgi->a({-href => href(action=>"blobdiff",
3970                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3971                                                             hash_base=>$hash, hash_parent_base=>$parent,
3972                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3973                                              "diff") .
3974                                      " | ";
3975                        }
3976                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3977                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3978                                      "blob") . " | ";
3979                        if ($have_blame) {
3980                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3981                                                             file_name=>$diff->{'to_file'})},
3982                                              "blame") . " | ";
3983                        }
3984                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3985                                                    file_name=>$diff->{'to_file'})},
3986                                      "history");
3987                        print "</td>\n";
3988
3989                } # we should not encounter Unmerged (U) or Unknown (X) status
3990                print "</tr>\n";
3991        }
3992        print "</tbody>" if $has_header;
3993        print "</table>\n";
3994}
3995
3996sub git_patchset_body {
3997        my ($fd, $difftree, $hash, @hash_parents) = @_;
3998        my ($hash_parent) = $hash_parents[0];
3999
4000        my $is_combined = (@hash_parents > 1);
4001        my $patch_idx = 0;
4002        my $patch_number = 0;
4003        my $patch_line;
4004        my $diffinfo;
4005        my $to_name;
4006        my (%from, %to);
4007
4008        print "<div class=\"patchset\">\n";
4009
4010        # skip to first patch
4011        while ($patch_line = <$fd>) {
4012                chomp $patch_line;
4013
4014                last if ($patch_line =~ m/^diff /);
4015        }
4016
4017 PATCH:
4018        while ($patch_line) {
4019
4020                # parse "git diff" header line
4021                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4022                        # $1 is from_name, which we do not use
4023                        $to_name = unquote($2);
4024                        $to_name =~ s!^b/!!;
4025                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4026                        # $1 is 'cc' or 'combined', which we do not use
4027                        $to_name = unquote($2);
4028                } else {
4029                        $to_name = undef;
4030                }
4031
4032                # check if current patch belong to current raw line
4033                # and parse raw git-diff line if needed
4034                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4035                        # this is continuation of a split patch
4036                        print "<div class=\"patch cont\">\n";
4037                } else {
4038                        # advance raw git-diff output if needed
4039                        $patch_idx++ if defined $diffinfo;
4040
4041                        # read and prepare patch information
4042                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4043
4044                        # compact combined diff output can have some patches skipped
4045                        # find which patch (using pathname of result) we are at now;
4046                        if ($is_combined) {
4047                                while ($to_name ne $diffinfo->{'to_file'}) {
4048                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4049                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
4050                                              "</div>\n";  # class="patch"
4051
4052                                        $patch_idx++;
4053                                        $patch_number++;
4054
4055                                        last if $patch_idx > $#$difftree;
4056                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4057                                }
4058                        }
4059
4060                        # modifies %from, %to hashes
4061                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4062
4063                        # this is first patch for raw difftree line with $patch_idx index
4064                        # we index @$difftree array from 0, but number patches from 1
4065                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4066                }
4067
4068                # git diff header
4069                #assert($patch_line =~ m/^diff /) if DEBUG;
4070                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4071                $patch_number++;
4072                # print "git diff" header
4073                print format_git_diff_header_line($patch_line, $diffinfo,
4074                                                  \%from, \%to);
4075
4076                # print extended diff header
4077                print "<div class=\"diff extended_header\">\n";
4078        EXTENDED_HEADER:
4079                while ($patch_line = <$fd>) {
4080                        chomp $patch_line;
4081
4082                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4083
4084                        print format_extended_diff_header_line($patch_line, $diffinfo,
4085                                                               \%from, \%to);
4086                }
4087                print "</div>\n"; # class="diff extended_header"
4088
4089                # from-file/to-file diff header
4090                if (! $patch_line) {
4091                        print "</div>\n"; # class="patch"
4092                        last PATCH;
4093                }
4094                next PATCH if ($patch_line =~ m/^diff /);
4095                #assert($patch_line =~ m/^---/) if DEBUG;
4096
4097                my $last_patch_line = $patch_line;
4098                $patch_line = <$fd>;
4099                chomp $patch_line;
4100                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4101
4102                print format_diff_from_to_header($last_patch_line, $patch_line,
4103                                                 $diffinfo, \%from, \%to,
4104                                                 @hash_parents);
4105
4106                # the patch itself
4107        LINE:
4108                while ($patch_line = <$fd>) {
4109                        chomp $patch_line;
4110
4111                        next PATCH if ($patch_line =~ m/^diff /);
4112
4113                        print format_diff_line($patch_line, \%from, \%to);
4114                }
4115
4116        } continue {
4117                print "</div>\n"; # class="patch"
4118        }
4119
4120        # for compact combined (--cc) format, with chunk and patch simpliciaction
4121        # patchset might be empty, but there might be unprocessed raw lines
4122        for (++$patch_idx if $patch_number > 0;
4123             $patch_idx < @$difftree;
4124             ++$patch_idx) {
4125                # read and prepare patch information
4126                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4127
4128                # generate anchor for "patch" links in difftree / whatchanged part
4129                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4130                      format_diff_cc_simplified($diffinfo, @hash_parents) .
4131                      "</div>\n";  # class="patch"
4132
4133                $patch_number++;
4134        }
4135
4136        if ($patch_number == 0) {
4137                if (@hash_parents > 1) {
4138                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4139                } else {
4140                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
4141                }
4142        }
4143
4144        print "</div>\n"; # class="patchset"
4145}
4146
4147# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4148
4149# fills project list info (age, description, owner, forks) for each
4150# project in the list, removing invalid projects from returned list
4151# NOTE: modifies $projlist, but does not remove entries from it
4152sub fill_project_list_info {
4153        my ($projlist, $check_forks) = @_;
4154        my @projects;
4155
4156        my $show_ctags = gitweb_check_feature('ctags');
4157 PROJECT:
4158        foreach my $pr (@$projlist) {
4159                my (@activity) = git_get_last_activity($pr->{'path'});
4160                unless (@activity) {
4161                        next PROJECT;
4162                }
4163                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4164                if (!defined $pr->{'descr'}) {
4165                        my $descr = git_get_project_description($pr->{'path'}) || "";
4166                        $descr = to_utf8($descr);
4167                        $pr->{'descr_long'} = $descr;
4168                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4169                }
4170                if (!defined $pr->{'owner'}) {
4171                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4172                }
4173                if ($check_forks) {
4174                        my $pname = $pr->{'path'};
4175                        if (($pname =~ s/\.git$//) &&
4176                            ($pname !~ /\/$/) &&
4177                            (-d "$projectroot/$pname")) {
4178                                $pr->{'forks'} = "-d $projectroot/$pname";
4179                        } else {
4180                                $pr->{'forks'} = 0;
4181                        }
4182                }
4183                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4184                push @projects, $pr;
4185        }
4186
4187        return @projects;
4188}
4189
4190# print 'sort by' <th> element, generating 'sort by $name' replay link
4191# if that order is not selected
4192sub print_sort_th {
4193        my ($name, $order, $header) = @_;
4194        $header ||= ucfirst($name);
4195
4196        if ($order eq $name) {
4197                print "<th>$header</th>\n";
4198        } else {
4199                print "<th>" .
4200                      $cgi->a({-href => href(-replay=>1, order=>$name),
4201                               -class => "header"}, $header) .
4202                      "</th>\n";
4203        }
4204}
4205
4206sub git_project_list_body {
4207        # actually uses global variable $project
4208        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4209
4210        my $check_forks = gitweb_check_feature('forks');
4211        my @projects = fill_project_list_info($projlist, $check_forks);
4212
4213        $order ||= $default_projects_order;
4214        $from = 0 unless defined $from;
4215        $to = $#projects if (!defined $to || $#projects < $to);
4216
4217        my %order_info = (
4218                project => { key => 'path', type => 'str' },
4219                descr => { key => 'descr_long', type => 'str' },
4220                owner => { key => 'owner', type => 'str' },
4221                age => { key => 'age', type => 'num' }
4222        );
4223        my $oi = $order_info{$order};
4224        if ($oi->{'type'} eq 'str') {
4225                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4226        } else {
4227                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4228        }
4229
4230        my $show_ctags = gitweb_check_feature('ctags');
4231        if ($show_ctags) {
4232                my %ctags;
4233                foreach my $p (@projects) {
4234                        foreach my $ct (keys %{$p->{'ctags'}}) {
4235                                $ctags{$ct} += $p->{'ctags'}->{$ct};
4236                        }
4237                }
4238                my $cloud = git_populate_project_tagcloud(\%ctags);
4239                print git_show_project_tagcloud($cloud, 64);
4240        }
4241
4242        print "<table class=\"project_list\">\n";
4243        unless ($no_header) {
4244                print "<tr>\n";
4245                if ($check_forks) {
4246                        print "<th></th>\n";
4247                }
4248                print_sort_th('project', $order, 'Project');
4249                print_sort_th('descr', $order, 'Description');
4250                print_sort_th('owner', $order, 'Owner');
4251                print_sort_th('age', $order, 'Last Change');
4252                print "<th></th>\n" . # for links
4253                      "</tr>\n";
4254        }
4255        my $alternate = 1;
4256        my $tagfilter = $cgi->param('by_tag');
4257        for (my $i = $from; $i <= $to; $i++) {
4258                my $pr = $projects[$i];
4259
4260                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4261                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4262                        and not $pr->{'descr_long'} =~ /$searchtext/;
4263                # Weed out forks or non-matching entries of search
4264                if ($check_forks) {
4265                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4266                        $forkbase="^$forkbase" if $forkbase;
4267                        next if not $searchtext and not $tagfilter and $show_ctags
4268                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4269                }
4270
4271                if ($alternate) {
4272                        print "<tr class=\"dark\">\n";
4273                } else {
4274                        print "<tr class=\"light\">\n";
4275                }
4276                $alternate ^= 1;
4277                if ($check_forks) {
4278                        print "<td>";
4279                        if ($pr->{'forks'}) {
4280                                print "<!-- $pr->{'forks'} -->\n";
4281                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4282                        }
4283                        print "</td>\n";
4284                }
4285                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4286                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4287                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4288                                        -class => "list", -title => $pr->{'descr_long'}},
4289                                        esc_html($pr->{'descr'})) . "</td>\n" .
4290                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4291                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4292                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4293                      "<td class=\"link\">" .
4294                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4295                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4296                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4297                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4298                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4299                      "</td>\n" .
4300                      "</tr>\n";
4301        }
4302        if (defined $extra) {
4303                print "<tr>\n";
4304                if ($check_forks) {
4305                        print "<td></td>\n";
4306                }
4307                print "<td colspan=\"5\">$extra</td>\n" .
4308                      "</tr>\n";
4309        }
4310        print "</table>\n";
4311}
4312
4313sub git_shortlog_body {
4314        # uses global variable $project
4315        my ($commitlist, $from, $to, $refs, $extra) = @_;
4316
4317        $from = 0 unless defined $from;
4318        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4319
4320        print "<table class=\"shortlog\">\n";
4321        my $alternate = 1;
4322        for (my $i = $from; $i <= $to; $i++) {
4323                my %co = %{$commitlist->[$i]};
4324                my $commit = $co{'id'};
4325                my $ref = format_ref_marker($refs, $commit);
4326                if ($alternate) {
4327                        print "<tr class=\"dark\">\n";
4328                } else {
4329                        print "<tr class=\"light\">\n";
4330                }
4331                $alternate ^= 1;
4332                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4333                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4334                      format_author_html('td', \%co, 10) . "<td>";
4335                print format_subject_html($co{'title'}, $co{'title_short'},
4336                                          href(action=>"commit", hash=>$commit), $ref);
4337                print "</td>\n" .
4338                      "<td class=\"link\">" .
4339                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4340                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4341                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4342                my $snapshot_links = format_snapshot_links($commit);
4343                if (defined $snapshot_links) {
4344                        print " | " . $snapshot_links;
4345                }
4346                print "</td>\n" .
4347                      "</tr>\n";
4348        }
4349        if (defined $extra) {
4350                print "<tr>\n" .
4351                      "<td colspan=\"4\">$extra</td>\n" .
4352                      "</tr>\n";
4353        }
4354        print "</table>\n";
4355}
4356
4357sub git_history_body {
4358        # Warning: assumes constant type (blob or tree) during history
4359        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4360
4361        $from = 0 unless defined $from;
4362        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4363
4364        print "<table class=\"history\">\n";
4365        my $alternate = 1;
4366        for (my $i = $from; $i <= $to; $i++) {
4367                my %co = %{$commitlist->[$i]};
4368                if (!%co) {
4369                        next;
4370                }
4371                my $commit = $co{'id'};
4372
4373                my $ref = format_ref_marker($refs, $commit);
4374
4375                if ($alternate) {
4376                        print "<tr class=\"dark\">\n";
4377                } else {
4378                        print "<tr class=\"light\">\n";
4379                }
4380                $alternate ^= 1;
4381                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4382        # shortlog:   format_author_html('td', \%co, 10)
4383                      format_author_html('td', \%co, 15, 3) . "<td>";
4384                # originally git_history used chop_str($co{'title'}, 50)
4385                print format_subject_html($co{'title'}, $co{'title_short'},
4386                                          href(action=>"commit", hash=>$commit), $ref);
4387                print "</td>\n" .
4388                      "<td class=\"link\">" .
4389                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4390                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4391
4392                if ($ftype eq 'blob') {
4393                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4394                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4395                        if (defined $blob_current && defined $blob_parent &&
4396                                        $blob_current ne $blob_parent) {
4397                                print " | " .
4398                                        $cgi->a({-href => href(action=>"blobdiff",
4399                                                               hash=>$blob_current, hash_parent=>$blob_parent,
4400                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
4401                                                               file_name=>$file_name)},
4402                                                "diff to current");
4403                        }
4404                }
4405                print "</td>\n" .
4406                      "</tr>\n";
4407        }
4408        if (defined $extra) {
4409                print "<tr>\n" .
4410                      "<td colspan=\"4\">$extra</td>\n" .
4411                      "</tr>\n";
4412        }
4413        print "</table>\n";
4414}
4415
4416sub git_tags_body {
4417        # uses global variable $project
4418        my ($taglist, $from, $to, $extra) = @_;
4419        $from = 0 unless defined $from;
4420        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4421
4422        print "<table class=\"tags\">\n";
4423        my $alternate = 1;
4424        for (my $i = $from; $i <= $to; $i++) {
4425                my $entry = $taglist->[$i];
4426                my %tag = %$entry;
4427                my $comment = $tag{'subject'};
4428                my $comment_short;
4429                if (defined $comment) {
4430                        $comment_short = chop_str($comment, 30, 5);
4431                }
4432                if ($alternate) {
4433                        print "<tr class=\"dark\">\n";
4434                } else {
4435                        print "<tr class=\"light\">\n";
4436                }
4437                $alternate ^= 1;
4438                if (defined $tag{'age'}) {
4439                        print "<td><i>$tag{'age'}</i></td>\n";
4440                } else {
4441                        print "<td></td>\n";
4442                }
4443                print "<td>" .
4444                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4445                               -class => "list name"}, esc_html($tag{'name'})) .
4446                      "</td>\n" .
4447                      "<td>";
4448                if (defined $comment) {
4449                        print format_subject_html($comment, $comment_short,
4450                                                  href(action=>"tag", hash=>$tag{'id'}));
4451                }
4452                print "</td>\n" .
4453                      "<td class=\"selflink\">";
4454                if ($tag{'type'} eq "tag") {
4455                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4456                } else {
4457                        print "&nbsp;";
4458                }
4459                print "</td>\n" .
4460                      "<td class=\"link\">" . " | " .
4461                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4462                if ($tag{'reftype'} eq "commit") {
4463                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4464                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4465                } elsif ($tag{'reftype'} eq "blob") {
4466                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4467                }
4468                print "</td>\n" .
4469                      "</tr>";
4470        }
4471        if (defined $extra) {
4472                print "<tr>\n" .
4473                      "<td colspan=\"5\">$extra</td>\n" .
4474                      "</tr>\n";
4475        }
4476        print "</table>\n";
4477}
4478
4479sub git_heads_body {
4480        # uses global variable $project
4481        my ($headlist, $head, $from, $to, $extra) = @_;
4482        $from = 0 unless defined $from;
4483        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4484
4485        print "<table class=\"heads\">\n";
4486        my $alternate = 1;
4487        for (my $i = $from; $i <= $to; $i++) {
4488                my $entry = $headlist->[$i];
4489                my %ref = %$entry;
4490                my $curr = $ref{'id'} eq $head;
4491                if ($alternate) {
4492                        print "<tr class=\"dark\">\n";
4493                } else {
4494                        print "<tr class=\"light\">\n";
4495                }
4496                $alternate ^= 1;
4497                print "<td><i>$ref{'age'}</i></td>\n" .
4498                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4499                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4500                               -class => "list name"},esc_html($ref{'name'})) .
4501                      "</td>\n" .
4502                      "<td class=\"link\">" .
4503                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4504                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4505                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4506                      "</td>\n" .
4507                      "</tr>";
4508        }
4509        if (defined $extra) {
4510                print "<tr>\n" .
4511                      "<td colspan=\"3\">$extra</td>\n" .
4512                      "</tr>\n";
4513        }
4514        print "</table>\n";
4515}
4516
4517sub git_search_grep_body {
4518        my ($commitlist, $from, $to, $extra) = @_;
4519        $from = 0 unless defined $from;
4520        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4521
4522        print "<table class=\"commit_search\">\n";
4523        my $alternate = 1;
4524        for (my $i = $from; $i <= $to; $i++) {
4525                my %co = %{$commitlist->[$i]};
4526                if (!%co) {
4527                        next;
4528                }
4529                my $commit = $co{'id'};
4530                if ($alternate) {
4531                        print "<tr class=\"dark\">\n";
4532                } else {
4533                        print "<tr class=\"light\">\n";
4534                }
4535                $alternate ^= 1;
4536                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4537                      format_author_html('td', \%co, 15, 5) .
4538                      "<td>" .
4539                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4540                               -class => "list subject"},
4541                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4542                my $comment = $co{'comment'};
4543                foreach my $line (@$comment) {
4544                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4545                                my ($lead, $match, $trail) = ($1, $2, $3);
4546                                $match = chop_str($match, 70, 5, 'center');
4547                                my $contextlen = int((80 - length($match))/2);
4548                                $contextlen = 30 if ($contextlen > 30);
4549                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4550                                $trail = chop_str($trail, $contextlen, 10, 'right');
4551
4552                                $lead  = esc_html($lead);
4553                                $match = esc_html($match);
4554                                $trail = esc_html($trail);
4555
4556                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4557                        }
4558                }
4559                print "</td>\n" .
4560                      "<td class=\"link\">" .
4561                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4562                      " | " .
4563                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4564                      " | " .
4565                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4566                print "</td>\n" .
4567                      "</tr>\n";
4568        }
4569        if (defined $extra) {
4570                print "<tr>\n" .
4571                      "<td colspan=\"3\">$extra</td>\n" .
4572                      "</tr>\n";
4573        }
4574        print "</table>\n";
4575}
4576
4577## ======================================================================
4578## ======================================================================
4579## actions
4580
4581sub git_project_list {
4582        my $order = $input_params{'order'};
4583        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4584                die_error(400, "Unknown order parameter");
4585        }
4586
4587        my @list = git_get_projects_list();
4588        if (!@list) {
4589                die_error(404, "No projects found");
4590        }
4591
4592        git_header_html();
4593        if (-f $home_text) {
4594                print "<div class=\"index_include\">\n";
4595                insert_file($home_text);
4596                print "</div>\n";
4597        }
4598        print $cgi->startform(-method => "get") .
4599              "<p class=\"projsearch\">Search:\n" .
4600              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4601              "</p>" .
4602              $cgi->end_form() . "\n";
4603        git_project_list_body(\@list, $order);
4604        git_footer_html();
4605}
4606
4607sub git_forks {
4608        my $order = $input_params{'order'};
4609        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4610                die_error(400, "Unknown order parameter");
4611        }
4612
4613        my @list = git_get_projects_list($project);
4614        if (!@list) {
4615                die_error(404, "No forks found");
4616        }
4617
4618        git_header_html();
4619        git_print_page_nav('','');
4620        git_print_header_div('summary', "$project forks");
4621        git_project_list_body(\@list, $order);
4622        git_footer_html();
4623}
4624
4625sub git_project_index {
4626        my @projects = git_get_projects_list($project);
4627
4628        print $cgi->header(
4629                -type => 'text/plain',
4630                -charset => 'utf-8',
4631                -content_disposition => 'inline; filename="index.aux"');
4632
4633        foreach my $pr (@projects) {
4634                if (!exists $pr->{'owner'}) {
4635                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4636                }
4637
4638                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4639                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4640                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4641                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4642                $path  =~ s/ /\+/g;
4643                $owner =~ s/ /\+/g;
4644
4645                print "$path $owner\n";
4646        }
4647}
4648
4649sub git_summary {
4650        my $descr = git_get_project_description($project) || "none";
4651        my %co = parse_commit("HEAD");
4652        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4653        my $head = $co{'id'};
4654
4655        my $owner = git_get_project_owner($project);
4656
4657        my $refs = git_get_references();
4658        # These get_*_list functions return one more to allow us to see if
4659        # there are more ...
4660        my @taglist  = git_get_tags_list(16);
4661        my @headlist = git_get_heads_list(16);
4662        my @forklist;
4663        my $check_forks = gitweb_check_feature('forks');
4664
4665        if ($check_forks) {
4666                @forklist = git_get_projects_list($project);
4667        }
4668
4669        git_header_html();
4670        git_print_page_nav('summary','', $head);
4671
4672        print "<div class=\"title\">&nbsp;</div>\n";
4673        print "<table class=\"projects_list\">\n" .
4674              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4675              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4676        if (defined $cd{'rfc2822'}) {
4677                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4678        }
4679
4680        # use per project git URL list in $projectroot/$project/cloneurl
4681        # or make project git URL from git base URL and project name
4682        my $url_tag = "URL";
4683        my @url_list = git_get_project_url_list($project);
4684        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4685        foreach my $git_url (@url_list) {
4686                next unless $git_url;
4687                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4688                $url_tag = "";
4689        }
4690
4691        # Tag cloud
4692        my $show_ctags = gitweb_check_feature('ctags');
4693        if ($show_ctags) {
4694                my $ctags = git_get_project_ctags($project);
4695                my $cloud = git_populate_project_tagcloud($ctags);
4696                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4697                print "</td>\n<td>" unless %$ctags;
4698                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4699                print "</td>\n<td>" if %$ctags;
4700                print git_show_project_tagcloud($cloud, 48);
4701                print "</td></tr>";
4702        }
4703
4704        print "</table>\n";
4705
4706        # If XSS prevention is on, we don't include README.html.
4707        # TODO: Allow a readme in some safe format.
4708        if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4709                print "<div class=\"title\">readme</div>\n" .
4710                      "<div class=\"readme\">\n";
4711                insert_file("$projectroot/$project/README.html");
4712                print "\n</div>\n"; # class="readme"
4713        }
4714
4715        # we need to request one more than 16 (0..15) to check if
4716        # those 16 are all
4717        my @commitlist = $head ? parse_commits($head, 17) : ();
4718        if (@commitlist) {
4719                git_print_header_div('shortlog');
4720                git_shortlog_body(\@commitlist, 0, 15, $refs,
4721                                  $#commitlist <=  15 ? undef :
4722                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4723        }
4724
4725        if (@taglist) {
4726                git_print_header_div('tags');
4727                git_tags_body(\@taglist, 0, 15,
4728                              $#taglist <=  15 ? undef :
4729                              $cgi->a({-href => href(action=>"tags")}, "..."));
4730        }
4731
4732        if (@headlist) {
4733                git_print_header_div('heads');
4734                git_heads_body(\@headlist, $head, 0, 15,
4735                               $#headlist <= 15 ? undef :
4736                               $cgi->a({-href => href(action=>"heads")}, "..."));
4737        }
4738
4739        if (@forklist) {
4740                git_print_header_div('forks');
4741                git_project_list_body(\@forklist, 'age', 0, 15,
4742                                      $#forklist <= 15 ? undef :
4743                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4744                                      'no_header');
4745        }
4746
4747        git_footer_html();
4748}
4749
4750sub git_tag {
4751        my $head = git_get_head_hash($project);
4752        git_header_html();
4753        git_print_page_nav('','', $head,undef,$head);
4754        my %tag = parse_tag($hash);
4755
4756        if (! %tag) {
4757                die_error(404, "Unknown tag object");
4758        }
4759
4760        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4761        print "<div class=\"title_text\">\n" .
4762              "<table class=\"object_header\">\n" .
4763              "<tr>\n" .
4764              "<td>object</td>\n" .
4765              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4766                               $tag{'object'}) . "</td>\n" .
4767              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4768                                              $tag{'type'}) . "</td>\n" .
4769              "</tr>\n";
4770        if (defined($tag{'author'})) {
4771                git_print_authorship_rows(\%tag, 'author');
4772        }
4773        print "</table>\n\n" .
4774              "</div>\n";
4775        print "<div class=\"page_body\">";
4776        my $comment = $tag{'comment'};
4777        foreach my $line (@$comment) {
4778                chomp $line;
4779                print esc_html($line, -nbsp=>1) . "<br/>\n";
4780        }
4781        print "</div>\n";
4782        git_footer_html();
4783}
4784
4785sub git_blame {
4786        # permissions
4787        gitweb_check_feature('blame')
4788                or die_error(403, "Blame view not allowed");
4789
4790        # error checking
4791        die_error(400, "No file name given") unless $file_name;
4792        $hash_base ||= git_get_head_hash($project);
4793        die_error(404, "Couldn't find base commit") unless $hash_base;
4794        my %co = parse_commit($hash_base)
4795                or die_error(404, "Commit not found");
4796        my $ftype = "blob";
4797        if (!defined $hash) {
4798                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4799                        or die_error(404, "Error looking up file");
4800        } else {
4801                $ftype = git_get_type($hash);
4802                if ($ftype !~ "blob") {
4803                        die_error(400, "Object is not a blob");
4804                }
4805        }
4806
4807        # run git-blame --porcelain
4808        open my $fd, "-|", git_cmd(), "blame", '-p',
4809                $hash_base, '--', $file_name
4810                or die_error(500, "Open git-blame failed");
4811
4812        # page header
4813        git_header_html();
4814        my $formats_nav =
4815                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4816                        "blob") .
4817                " | " .
4818                $cgi->a({-href => href(action=>"history", -replay=>1)},
4819                        "history") .
4820                " | " .
4821                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4822                        "HEAD");
4823        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4824        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4825        git_print_page_path($file_name, $ftype, $hash_base);
4826
4827        # page body
4828        my @rev_color = qw(light dark);
4829        my $num_colors = scalar(@rev_color);
4830        my $current_color = 0;
4831        my %metainfo = ();
4832
4833        print <<HTML;
4834<div class="page_body">
4835<table class="blame">
4836<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4837HTML
4838 LINE:
4839        while (my $line = <$fd>) {
4840                chomp $line;
4841                # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4842                # no <lines in group> for subsequent lines in group of lines
4843                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4844                   ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4845                if (!exists $metainfo{$full_rev}) {
4846                        $metainfo{$full_rev} = { 'nprevious' => 0 };
4847                }
4848                my $meta = $metainfo{$full_rev};
4849                my $data;
4850                while ($data = <$fd>) {
4851                        chomp $data;
4852                        last if ($data =~ s/^\t//); # contents of line
4853                        if ($data =~ /^(\S+)(?: (.*))?$/) {
4854                                $meta->{$1} = $2 unless exists $meta->{$1};
4855                        }
4856                        if ($data =~ /^previous /) {
4857                                $meta->{'nprevious'}++;
4858                        }
4859                }
4860                my $short_rev = substr($full_rev, 0, 8);
4861                my $author = $meta->{'author'};
4862                my %date =
4863                        parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4864                my $date = $date{'iso-tz'};
4865                if ($group_size) {
4866                        $current_color = ($current_color + 1) % $num_colors;
4867                }
4868                my $tr_class = $rev_color[$current_color];
4869                $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4870                $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4871                $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4872                print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4873                if ($group_size) {
4874                        print "<td class=\"sha1\"";
4875                        print " title=\"". esc_html($author) . ", $date\"";
4876                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4877                        print ">";
4878                        print $cgi->a({-href => href(action=>"commit",
4879                                                     hash=>$full_rev,
4880                                                     file_name=>$file_name)},
4881                                      esc_html($short_rev));
4882                        if ($group_size >= 2) {
4883                                my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4884                                if (@author_initials) {
4885                                        print "<br />" .
4886                                              esc_html(join('', @author_initials));
4887                                        #           or join('.', ...)
4888                                }
4889                        }
4890                        print "</td>\n";
4891                }
4892                # 'previous' <sha1 of parent commit> <filename at commit>
4893                if (exists $meta->{'previous'} &&
4894                    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4895                        $meta->{'parent'} = $1;
4896                        $meta->{'file_parent'} = unquote($2);
4897                }
4898                my $linenr_commit =
4899                        exists($meta->{'parent'}) ?
4900                        $meta->{'parent'} : $full_rev;
4901                my $linenr_filename =
4902                        exists($meta->{'file_parent'}) ?
4903                        $meta->{'file_parent'} : unquote($meta->{'filename'});
4904                my $blamed = href(action => 'blame',
4905                                  file_name => $linenr_filename,
4906                                  hash_base => $linenr_commit);
4907                print "<td class=\"linenr\">";
4908                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4909                                -class => "linenr" },
4910                              esc_html($lineno));
4911                print "</td>";
4912                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4913                print "</tr>\n";
4914        }
4915        print "</table>\n";
4916        print "</div>";
4917        close $fd
4918                or print "Reading blob failed\n";
4919
4920        # page footer
4921        git_footer_html();
4922}
4923
4924sub git_tags {
4925        my $head = git_get_head_hash($project);
4926        git_header_html();
4927        git_print_page_nav('','', $head,undef,$head);
4928        git_print_header_div('summary', $project);
4929
4930        my @tagslist = git_get_tags_list();
4931        if (@tagslist) {
4932                git_tags_body(\@tagslist);
4933        }
4934        git_footer_html();
4935}
4936
4937sub git_heads {
4938        my $head = git_get_head_hash($project);
4939        git_header_html();
4940        git_print_page_nav('','', $head,undef,$head);
4941        git_print_header_div('summary', $project);
4942
4943        my @headslist = git_get_heads_list();
4944        if (@headslist) {
4945                git_heads_body(\@headslist, $head);
4946        }
4947        git_footer_html();
4948}
4949
4950sub git_blob_plain {
4951        my $type = shift;
4952        my $expires;
4953
4954        if (!defined $hash) {
4955                if (defined $file_name) {
4956                        my $base = $hash_base || git_get_head_hash($project);
4957                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4958                                or die_error(404, "Cannot find file");
4959                } else {
4960                        die_error(400, "No file name defined");
4961                }
4962        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4963                # blobs defined by non-textual hash id's can be cached
4964                $expires = "+1d";
4965        }
4966
4967        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4968                or die_error(500, "Open git-cat-file blob '$hash' failed");
4969
4970        # content-type (can include charset)
4971        $type = blob_contenttype($fd, $file_name, $type);
4972
4973        # "save as" filename, even when no $file_name is given
4974        my $save_as = "$hash";
4975        if (defined $file_name) {
4976                $save_as = $file_name;
4977        } elsif ($type =~ m/^text\//) {
4978                $save_as .= '.txt';
4979        }
4980
4981        # With XSS prevention on, blobs of all types except a few known safe
4982        # ones are served with "Content-Disposition: attachment" to make sure
4983        # they don't run in our security domain.  For certain image types,
4984        # blob view writes an <img> tag referring to blob_plain view, and we
4985        # want to be sure not to break that by serving the image as an
4986        # attachment (though Firefox 3 doesn't seem to care).
4987        my $sandbox = $prevent_xss &&
4988                $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4989
4990        print $cgi->header(
4991                -type => $type,
4992                -expires => $expires,
4993                -content_disposition =>
4994                        ($sandbox ? 'attachment' : 'inline')
4995                        . '; filename="' . $save_as . '"');
4996        local $/ = undef;
4997        binmode STDOUT, ':raw';
4998        print <$fd>;
4999        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5000        close $fd;
5001}
5002
5003sub git_blob {
5004        my $expires;
5005
5006        if (!defined $hash) {
5007                if (defined $file_name) {
5008                        my $base = $hash_base || git_get_head_hash($project);
5009                        $hash = git_get_hash_by_path($base, $file_name, "blob")
5010                                or die_error(404, "Cannot find file");
5011                } else {
5012                        die_error(400, "No file name defined");
5013                }
5014        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5015                # blobs defined by non-textual hash id's can be cached
5016                $expires = "+1d";
5017        }
5018
5019        my $have_blame = gitweb_check_feature('blame');
5020        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5021                or die_error(500, "Couldn't cat $file_name, $hash");
5022        my $mimetype = blob_mimetype($fd, $file_name);
5023        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5024                close $fd;
5025                return git_blob_plain($mimetype);
5026        }
5027        # we can have blame only for text/* mimetype
5028        $have_blame &&= ($mimetype =~ m!^text/!);
5029
5030        git_header_html(undef, $expires);
5031        my $formats_nav = '';
5032        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5033                if (defined $file_name) {
5034                        if ($have_blame) {
5035                                $formats_nav .=
5036                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
5037                                                "blame") .
5038                                        " | ";
5039                        }
5040                        $formats_nav .=
5041                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5042                                        "history") .
5043                                " | " .
5044                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5045                                        "raw") .
5046                                " | " .
5047                                $cgi->a({-href => href(action=>"blob",
5048                                                       hash_base=>"HEAD", file_name=>$file_name)},
5049                                        "HEAD");
5050                } else {
5051                        $formats_nav .=
5052                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5053                                        "raw");
5054                }
5055                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5056                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5057        } else {
5058                print "<div class=\"page_nav\">\n" .
5059                      "<br/><br/></div>\n" .
5060                      "<div class=\"title\">$hash</div>\n";
5061        }
5062        git_print_page_path($file_name, "blob", $hash_base);
5063        print "<div class=\"page_body\">\n";
5064        if ($mimetype =~ m!^image/!) {
5065                print qq!<img type="$mimetype"!;
5066                if ($file_name) {
5067                        print qq! alt="$file_name" title="$file_name"!;
5068                }
5069                print qq! src="! .
5070                      href(action=>"blob_plain", hash=>$hash,
5071                           hash_base=>$hash_base, file_name=>$file_name) .
5072                      qq!" />\n!;
5073        } else {
5074                my $nr;
5075                while (my $line = <$fd>) {
5076                        chomp $line;
5077                        $nr++;
5078                        $line = untabify($line);
5079                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5080                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5081                }
5082        }
5083        close $fd
5084                or print "Reading blob failed.\n";
5085        print "</div>";
5086        git_footer_html();
5087}
5088
5089sub git_tree {
5090        if (!defined $hash_base) {
5091                $hash_base = "HEAD";
5092        }
5093        if (!defined $hash) {
5094                if (defined $file_name) {
5095                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5096                } else {
5097                        $hash = $hash_base;
5098                }
5099        }
5100        die_error(404, "No such tree") unless defined($hash);
5101
5102        my @entries = ();
5103        {
5104                local $/ = "\0";
5105                open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5106                        or die_error(500, "Open git-ls-tree failed");
5107                @entries = map { chomp; $_ } <$fd>;
5108                close $fd
5109                        or die_error(404, "Reading tree failed");
5110        }
5111
5112        my $refs = git_get_references();
5113        my $ref = format_ref_marker($refs, $hash_base);
5114        git_header_html();
5115        my $basedir = '';
5116        my $have_blame = gitweb_check_feature('blame');
5117        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5118                my @views_nav = ();
5119                if (defined $file_name) {
5120                        push @views_nav,
5121                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5122                                        "history"),
5123                                $cgi->a({-href => href(action=>"tree",
5124                                                       hash_base=>"HEAD", file_name=>$file_name)},
5125                                        "HEAD"),
5126                }
5127                my $snapshot_links = format_snapshot_links($hash);
5128                if (defined $snapshot_links) {
5129                        # FIXME: Should be available when we have no hash base as well.
5130                        push @views_nav, $snapshot_links;
5131                }
5132                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5133                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5134        } else {
5135                undef $hash_base;
5136                print "<div class=\"page_nav\">\n";
5137                print "<br/><br/></div>\n";
5138                print "<div class=\"title\">$hash</div>\n";
5139        }
5140        if (defined $file_name) {
5141                $basedir = $file_name;
5142                if ($basedir ne '' && substr($basedir, -1) ne '/') {
5143                        $basedir .= '/';
5144                }
5145                git_print_page_path($file_name, 'tree', $hash_base);
5146        }
5147        print "<div class=\"page_body\">\n";
5148        print "<table class=\"tree\">\n";
5149        my $alternate = 1;
5150        # '..' (top directory) link if possible
5151        if (defined $hash_base &&
5152            defined $file_name && $file_name =~ m![^/]+$!) {
5153                if ($alternate) {
5154                        print "<tr class=\"dark\">\n";
5155                } else {
5156                        print "<tr class=\"light\">\n";
5157                }
5158                $alternate ^= 1;
5159
5160                my $up = $file_name;
5161                $up =~ s!/?[^/]+$!!;
5162                undef $up unless $up;
5163                # based on git_print_tree_entry
5164                print '<td class="mode">' . mode_str('040000') . "</td>\n";
5165                print '<td class="list">';
5166                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5167                                             file_name=>$up)},
5168                              "..");
5169                print "</td>\n";
5170                print "<td class=\"link\"></td>\n";
5171
5172                print "</tr>\n";
5173        }
5174        foreach my $line (@entries) {
5175                my %t = parse_ls_tree_line($line, -z => 1);
5176
5177                if ($alternate) {
5178                        print "<tr class=\"dark\">\n";
5179                } else {
5180                        print "<tr class=\"light\">\n";
5181                }
5182                $alternate ^= 1;
5183
5184                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5185
5186                print "</tr>\n";
5187        }
5188        print "</table>\n" .
5189              "</div>";
5190        git_footer_html();
5191}
5192
5193sub snapshot_name {
5194        my ($project, $hash) = @_;
5195
5196        # path/to/project.git  -> project
5197        # path/to/project/.git -> project
5198        my $name = to_utf8($project);
5199        $name =~ s,([^/])/*\.git$,$1,;
5200        $name = basename($name);
5201        # sanitize name
5202        $name =~ s/[[:cntrl:]]/?/g;
5203
5204        my $ver = $hash;
5205        if ($hash =~ /^[0-9a-fA-F]+$/) {
5206                # shorten SHA-1 hash
5207                my $full_hash = git_get_full_hash($project, $hash);
5208                if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5209                        $ver = git_get_short_hash($project, $hash);
5210                }
5211        } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5212                # tags don't need shortened SHA-1 hash
5213                $ver = $1;
5214        } else {
5215                # branches and other need shortened SHA-1 hash
5216                if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5217                        $ver = $1;
5218                }
5219                $ver .= '-' . git_get_short_hash($project, $hash);
5220        }
5221        # in case of hierarchical branch names
5222        $ver =~ s!/!.!g;
5223
5224        # name = project-version_string
5225        $name = "$name-$ver";
5226
5227        return wantarray ? ($name, $name) : $name;
5228}
5229
5230sub git_snapshot {
5231        my $format = $input_params{'snapshot_format'};
5232        if (!@snapshot_fmts) {
5233                die_error(403, "Snapshots not allowed");
5234        }
5235        # default to first supported snapshot format
5236        $format ||= $snapshot_fmts[0];
5237        if ($format !~ m/^[a-z0-9]+$/) {
5238                die_error(400, "Invalid snapshot format parameter");
5239        } elsif (!exists($known_snapshot_formats{$format})) {
5240                die_error(400, "Unknown snapshot format");
5241        } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5242                die_error(403, "Snapshot format not allowed");
5243        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5244                die_error(403, "Unsupported snapshot format");
5245        }
5246
5247        my $type = git_get_type("$hash^{}");
5248        if (!$type) {
5249                die_error(404, 'Object does not exist');
5250        }  elsif ($type eq 'blob') {
5251                die_error(400, 'Object is not a tree-ish');
5252        }
5253
5254        my ($name, $prefix) = snapshot_name($project, $hash);
5255        my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5256        my $cmd = quote_command(
5257                git_cmd(), 'archive',
5258                "--format=$known_snapshot_formats{$format}{'format'}",
5259                "--prefix=$prefix/", $hash);
5260        if (exists $known_snapshot_formats{$format}{'compressor'}) {
5261                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5262        }
5263
5264        $filename =~ s/(["\\])/\\$1/g;
5265        print $cgi->header(
5266                -type => $known_snapshot_formats{$format}{'type'},
5267                -content_disposition => 'inline; filename="' . $filename . '"',
5268                -status => '200 OK');
5269
5270        open my $fd, "-|", $cmd
5271                or die_error(500, "Execute git-archive failed");
5272        binmode STDOUT, ':raw';
5273        print <$fd>;
5274        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5275        close $fd;
5276}
5277
5278sub git_log {
5279        my $head = git_get_head_hash($project);
5280        if (!defined $hash) {
5281                $hash = $head;
5282        }
5283        if (!defined $page) {
5284                $page = 0;
5285        }
5286        my $refs = git_get_references();
5287
5288        my @commitlist = parse_commits($hash, 101, (100 * $page));
5289
5290        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5291
5292        my ($patch_max) = gitweb_get_feature('patches');
5293        if ($patch_max) {
5294                if ($patch_max < 0 || @commitlist <= $patch_max) {
5295                        $paging_nav .= " &sdot; " .
5296                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
5297                                        "patches");
5298                }
5299        }
5300
5301        git_header_html();
5302        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5303
5304        if (!@commitlist) {
5305                my %co = parse_commit($hash);
5306
5307                git_print_header_div('summary', $project);
5308                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5309        }
5310        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5311        for (my $i = 0; $i <= $to; $i++) {
5312                my %co = %{$commitlist[$i]};
5313                next if !%co;
5314                my $commit = $co{'id'};
5315                my $ref = format_ref_marker($refs, $commit);
5316                my %ad = parse_date($co{'author_epoch'});
5317                git_print_header_div('commit',
5318                               "<span class=\"age\">$co{'age_string'}</span>" .
5319                               esc_html($co{'title'}) . $ref,
5320                               $commit);
5321                print "<div class=\"title_text\">\n" .
5322                      "<div class=\"log_link\">\n" .
5323                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5324                      " | " .
5325                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5326                      " | " .
5327                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5328                      "<br/>\n" .
5329                      "</div>\n";
5330                      git_print_authorship(\%co, -tag => 'span');
5331                      print "<br/>\n</div>\n";
5332
5333                print "<div class=\"log_body\">\n";
5334                git_print_log($co{'comment'}, -final_empty_line=> 1);
5335                print "</div>\n";
5336        }
5337        if ($#commitlist >= 100) {
5338                print "<div class=\"page_nav\">\n";
5339                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5340                               -accesskey => "n", -title => "Alt-n"}, "next");
5341                print "</div>\n";
5342        }
5343        git_footer_html();
5344}
5345
5346sub git_commit {
5347        $hash ||= $hash_base || "HEAD";
5348        my %co = parse_commit($hash)
5349            or die_error(404, "Unknown commit object");
5350
5351        my $parent  = $co{'parent'};
5352        my $parents = $co{'parents'}; # listref
5353
5354        # we need to prepare $formats_nav before any parameter munging
5355        my $formats_nav;
5356        if (!defined $parent) {
5357                # --root commitdiff
5358                $formats_nav .= '(initial)';
5359        } elsif (@$parents == 1) {
5360                # single parent commit
5361                $formats_nav .=
5362                        '(parent: ' .
5363                        $cgi->a({-href => href(action=>"commit",
5364                                               hash=>$parent)},
5365                                esc_html(substr($parent, 0, 7))) .
5366                        ')';
5367        } else {
5368                # merge commit
5369                $formats_nav .=
5370                        '(merge: ' .
5371                        join(' ', map {
5372                                $cgi->a({-href => href(action=>"commit",
5373                                                       hash=>$_)},
5374                                        esc_html(substr($_, 0, 7)));
5375                        } @$parents ) .
5376                        ')';
5377        }
5378        if (gitweb_check_feature('patches')) {
5379                $formats_nav .= " | " .
5380                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
5381                                "patch");
5382        }
5383
5384        if (!defined $parent) {
5385                $parent = "--root";
5386        }
5387        my @difftree;
5388        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5389                @diff_opts,
5390                (@$parents <= 1 ? $parent : '-c'),
5391                $hash, "--"
5392                or die_error(500, "Open git-diff-tree failed");
5393        @difftree = map { chomp; $_ } <$fd>;
5394        close $fd or die_error(404, "Reading git-diff-tree failed");
5395
5396        # non-textual hash id's can be cached
5397        my $expires;
5398        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5399                $expires = "+1d";
5400        }
5401        my $refs = git_get_references();
5402        my $ref = format_ref_marker($refs, $co{'id'});
5403
5404        git_header_html(undef, $expires);
5405        git_print_page_nav('commit', '',
5406                           $hash, $co{'tree'}, $hash,
5407                           $formats_nav);
5408
5409        if (defined $co{'parent'}) {
5410                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5411        } else {
5412                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5413        }
5414        print "<div class=\"title_text\">\n" .
5415              "<table class=\"object_header\">\n";
5416        git_print_authorship_rows(\%co);
5417        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5418        print "<tr>" .
5419              "<td>tree</td>" .
5420              "<td class=\"sha1\">" .
5421              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5422                       class => "list"}, $co{'tree'}) .
5423              "</td>" .
5424              "<td class=\"link\">" .
5425              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5426                      "tree");
5427        my $snapshot_links = format_snapshot_links($hash);
5428        if (defined $snapshot_links) {
5429                print " | " . $snapshot_links;
5430        }
5431        print "</td>" .
5432              "</tr>\n";
5433
5434        foreach my $par (@$parents) {
5435                print "<tr>" .
5436                      "<td>parent</td>" .
5437                      "<td class=\"sha1\">" .
5438                      $cgi->a({-href => href(action=>"commit", hash=>$par),
5439                               class => "list"}, $par) .
5440                      "</td>" .
5441                      "<td class=\"link\">" .
5442                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5443                      " | " .
5444                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5445                      "</td>" .
5446                      "</tr>\n";
5447        }
5448        print "</table>".
5449              "</div>\n";
5450
5451        print "<div class=\"page_body\">\n";
5452        git_print_log($co{'comment'});
5453        print "</div>\n";
5454
5455        git_difftree_body(\@difftree, $hash, @$parents);
5456
5457        git_footer_html();
5458}
5459
5460sub git_object {
5461        # object is defined by:
5462        # - hash or hash_base alone
5463        # - hash_base and file_name
5464        my $type;
5465
5466        # - hash or hash_base alone
5467        if ($hash || ($hash_base && !defined $file_name)) {
5468                my $object_id = $hash || $hash_base;
5469
5470                open my $fd, "-|", quote_command(
5471                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5472                        or die_error(404, "Object does not exist");
5473                $type = <$fd>;
5474                chomp $type;
5475                close $fd
5476                        or die_error(404, "Object does not exist");
5477
5478        # - hash_base and file_name
5479        } elsif ($hash_base && defined $file_name) {
5480                $file_name =~ s,/+$,,;
5481
5482                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5483                        or die_error(404, "Base object does not exist");
5484
5485                # here errors should not hapen
5486                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5487                        or die_error(500, "Open git-ls-tree failed");
5488                my $line = <$fd>;
5489                close $fd;
5490
5491                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5492                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5493                        die_error(404, "File or directory for given base does not exist");
5494                }
5495                $type = $2;
5496                $hash = $3;
5497        } else {
5498                die_error(400, "Not enough information to find object");
5499        }
5500
5501        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5502                                          hash=>$hash, hash_base=>$hash_base,
5503                                          file_name=>$file_name),
5504                             -status => '302 Found');
5505}
5506
5507sub git_blobdiff {
5508        my $format = shift || 'html';
5509
5510        my $fd;
5511        my @difftree;
5512        my %diffinfo;
5513        my $expires;
5514
5515        # preparing $fd and %diffinfo for git_patchset_body
5516        # new style URI
5517        if (defined $hash_base && defined $hash_parent_base) {
5518                if (defined $file_name) {
5519                        # read raw output
5520                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5521                                $hash_parent_base, $hash_base,
5522                                "--", (defined $file_parent ? $file_parent : ()), $file_name
5523                                or die_error(500, "Open git-diff-tree failed");
5524                        @difftree = map { chomp; $_ } <$fd>;
5525                        close $fd
5526                                or die_error(404, "Reading git-diff-tree failed");
5527                        @difftree
5528                                or die_error(404, "Blob diff not found");
5529
5530                } elsif (defined $hash &&
5531                         $hash =~ /[0-9a-fA-F]{40}/) {
5532                        # try to find filename from $hash
5533
5534                        # read filtered raw output
5535                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5536                                $hash_parent_base, $hash_base, "--"
5537                                or die_error(500, "Open git-diff-tree failed");
5538                        @difftree =
5539                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5540                                # $hash == to_id
5541                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5542                                map { chomp; $_ } <$fd>;
5543                        close $fd
5544                                or die_error(404, "Reading git-diff-tree failed");
5545                        @difftree
5546                                or die_error(404, "Blob diff not found");
5547
5548                } else {
5549                        die_error(400, "Missing one of the blob diff parameters");
5550                }
5551
5552                if (@difftree > 1) {
5553                        die_error(400, "Ambiguous blob diff specification");
5554                }
5555
5556                %diffinfo = parse_difftree_raw_line($difftree[0]);
5557                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5558                $file_name   ||= $diffinfo{'to_file'};
5559
5560                $hash_parent ||= $diffinfo{'from_id'};
5561                $hash        ||= $diffinfo{'to_id'};
5562
5563                # non-textual hash id's can be cached
5564                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5565                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5566                        $expires = '+1d';
5567                }
5568
5569                # open patch output
5570                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5571                        '-p', ($format eq 'html' ? "--full-index" : ()),
5572                        $hash_parent_base, $hash_base,
5573                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5574                        or die_error(500, "Open git-diff-tree failed");
5575        }
5576
5577        # old/legacy style URI -- not generated anymore since 1.4.3.
5578        if (!%diffinfo) {
5579                die_error('404 Not Found', "Missing one of the blob diff parameters")
5580        }
5581
5582        # header
5583        if ($format eq 'html') {
5584                my $formats_nav =
5585                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5586                                "raw");
5587                git_header_html(undef, $expires);
5588                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5589                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5590                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5591                } else {
5592                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5593                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5594                }
5595                if (defined $file_name) {
5596                        git_print_page_path($file_name, "blob", $hash_base);
5597                } else {
5598                        print "<div class=\"page_path\"></div>\n";
5599                }
5600
5601        } elsif ($format eq 'plain') {
5602                print $cgi->header(
5603                        -type => 'text/plain',
5604                        -charset => 'utf-8',
5605                        -expires => $expires,
5606                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5607
5608                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5609
5610        } else {
5611                die_error(400, "Unknown blobdiff format");
5612        }
5613
5614        # patch
5615        if ($format eq 'html') {
5616                print "<div class=\"page_body\">\n";
5617
5618                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5619                close $fd;
5620
5621                print "</div>\n"; # class="page_body"
5622                git_footer_html();
5623
5624        } else {
5625                while (my $line = <$fd>) {
5626                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5627                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5628
5629                        print $line;
5630
5631                        last if $line =~ m!^\+\+\+!;
5632                }
5633                local $/ = undef;
5634                print <$fd>;
5635                close $fd;
5636        }
5637}
5638
5639sub git_blobdiff_plain {
5640        git_blobdiff('plain');
5641}
5642
5643sub git_commitdiff {
5644        my %params = @_;
5645        my $format = $params{-format} || 'html';
5646
5647        my ($patch_max) = gitweb_get_feature('patches');
5648        if ($format eq 'patch') {
5649                die_error(403, "Patch view not allowed") unless $patch_max;
5650        }
5651
5652        $hash ||= $hash_base || "HEAD";
5653        my %co = parse_commit($hash)
5654            or die_error(404, "Unknown commit object");
5655
5656        # choose format for commitdiff for merge
5657        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5658                $hash_parent = '--cc';
5659        }
5660        # we need to prepare $formats_nav before almost any parameter munging
5661        my $formats_nav;
5662        if ($format eq 'html') {
5663                $formats_nav =
5664                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5665                                "raw");
5666                if ($patch_max) {
5667                        $formats_nav .= " | " .
5668                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
5669                                        "patch");
5670                }
5671
5672                if (defined $hash_parent &&
5673                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5674                        # commitdiff with two commits given
5675                        my $hash_parent_short = $hash_parent;
5676                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5677                                $hash_parent_short = substr($hash_parent, 0, 7);
5678                        }
5679                        $formats_nav .=
5680                                ' (from';
5681                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5682                                if ($co{'parents'}[$i] eq $hash_parent) {
5683                                        $formats_nav .= ' parent ' . ($i+1);
5684                                        last;
5685                                }
5686                        }
5687                        $formats_nav .= ': ' .
5688                                $cgi->a({-href => href(action=>"commitdiff",
5689                                                       hash=>$hash_parent)},
5690                                        esc_html($hash_parent_short)) .
5691                                ')';
5692                } elsif (!$co{'parent'}) {
5693                        # --root commitdiff
5694                        $formats_nav .= ' (initial)';
5695                } elsif (scalar @{$co{'parents'}} == 1) {
5696                        # single parent commit
5697                        $formats_nav .=
5698                                ' (parent: ' .
5699                                $cgi->a({-href => href(action=>"commitdiff",
5700                                                       hash=>$co{'parent'})},
5701                                        esc_html(substr($co{'parent'}, 0, 7))) .
5702                                ')';
5703                } else {
5704                        # merge commit
5705                        if ($hash_parent eq '--cc') {
5706                                $formats_nav .= ' | ' .
5707                                        $cgi->a({-href => href(action=>"commitdiff",
5708                                                               hash=>$hash, hash_parent=>'-c')},
5709                                                'combined');
5710                        } else { # $hash_parent eq '-c'
5711                                $formats_nav .= ' | ' .
5712                                        $cgi->a({-href => href(action=>"commitdiff",
5713                                                               hash=>$hash, hash_parent=>'--cc')},
5714                                                'compact');
5715                        }
5716                        $formats_nav .=
5717                                ' (merge: ' .
5718                                join(' ', map {
5719                                        $cgi->a({-href => href(action=>"commitdiff",
5720                                                               hash=>$_)},
5721                                                esc_html(substr($_, 0, 7)));
5722                                } @{$co{'parents'}} ) .
5723                                ')';
5724                }
5725        }
5726
5727        my $hash_parent_param = $hash_parent;
5728        if (!defined $hash_parent_param) {
5729                # --cc for multiple parents, --root for parentless
5730                $hash_parent_param =
5731                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5732        }
5733
5734        # read commitdiff
5735        my $fd;
5736        my @difftree;
5737        if ($format eq 'html') {
5738                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5739                        "--no-commit-id", "--patch-with-raw", "--full-index",
5740                        $hash_parent_param, $hash, "--"
5741                        or die_error(500, "Open git-diff-tree failed");
5742
5743                while (my $line = <$fd>) {
5744                        chomp $line;
5745                        # empty line ends raw part of diff-tree output
5746                        last unless $line;
5747                        push @difftree, scalar parse_difftree_raw_line($line);
5748                }
5749
5750        } elsif ($format eq 'plain') {
5751                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5752                        '-p', $hash_parent_param, $hash, "--"
5753                        or die_error(500, "Open git-diff-tree failed");
5754        } elsif ($format eq 'patch') {
5755                # For commit ranges, we limit the output to the number of
5756                # patches specified in the 'patches' feature.
5757                # For single commits, we limit the output to a single patch,
5758                # diverging from the git-format-patch default.
5759                my @commit_spec = ();
5760                if ($hash_parent) {
5761                        if ($patch_max > 0) {
5762                                push @commit_spec, "-$patch_max";
5763                        }
5764                        push @commit_spec, '-n', "$hash_parent..$hash";
5765                } else {
5766                        if ($params{-single}) {
5767                                push @commit_spec, '-1';
5768                        } else {
5769                                if ($patch_max > 0) {
5770                                        push @commit_spec, "-$patch_max";
5771                                }
5772                                push @commit_spec, "-n";
5773                        }
5774                        push @commit_spec, '--root', $hash;
5775                }
5776                open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5777                        '--stdout', @commit_spec
5778                        or die_error(500, "Open git-format-patch failed");
5779        } else {
5780                die_error(400, "Unknown commitdiff format");
5781        }
5782
5783        # non-textual hash id's can be cached
5784        my $expires;
5785        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5786                $expires = "+1d";
5787        }
5788
5789        # write commit message
5790        if ($format eq 'html') {
5791                my $refs = git_get_references();
5792                my $ref = format_ref_marker($refs, $co{'id'});
5793
5794                git_header_html(undef, $expires);
5795                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5796                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5797                print "<div class=\"title_text\">\n" .
5798                      "<table class=\"object_header\">\n";
5799                git_print_authorship_rows(\%co);
5800                print "</table>".
5801                      "</div>\n";
5802                print "<div class=\"page_body\">\n";
5803                if (@{$co{'comment'}} > 1) {
5804                        print "<div class=\"log\">\n";
5805                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5806                        print "</div>\n"; # class="log"
5807                }
5808
5809        } elsif ($format eq 'plain') {
5810                my $refs = git_get_references("tags");
5811                my $tagname = git_get_rev_name_tags($hash);
5812                my $filename = basename($project) . "-$hash.patch";
5813
5814                print $cgi->header(
5815                        -type => 'text/plain',
5816                        -charset => 'utf-8',
5817                        -expires => $expires,
5818                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5819                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5820                print "From: " . to_utf8($co{'author'}) . "\n";
5821                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5822                print "Subject: " . to_utf8($co{'title'}) . "\n";
5823
5824                print "X-Git-Tag: $tagname\n" if $tagname;
5825                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5826
5827                foreach my $line (@{$co{'comment'}}) {
5828                        print to_utf8($line) . "\n";
5829                }
5830                print "---\n\n";
5831        } elsif ($format eq 'patch') {
5832                my $filename = basename($project) . "-$hash.patch";
5833
5834                print $cgi->header(
5835                        -type => 'text/plain',
5836                        -charset => 'utf-8',
5837                        -expires => $expires,
5838                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5839        }
5840
5841        # write patch
5842        if ($format eq 'html') {
5843                my $use_parents = !defined $hash_parent ||
5844                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5845                git_difftree_body(\@difftree, $hash,
5846                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5847                print "<br/>\n";
5848
5849                git_patchset_body($fd, \@difftree, $hash,
5850                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5851                close $fd;
5852                print "</div>\n"; # class="page_body"
5853                git_footer_html();
5854
5855        } elsif ($format eq 'plain') {
5856                local $/ = undef;
5857                print <$fd>;
5858                close $fd
5859                        or print "Reading git-diff-tree failed\n";
5860        } elsif ($format eq 'patch') {
5861                local $/ = undef;
5862                print <$fd>;
5863                close $fd
5864                        or print "Reading git-format-patch failed\n";
5865        }
5866}
5867
5868sub git_commitdiff_plain {
5869        git_commitdiff(-format => 'plain');
5870}
5871
5872# format-patch-style patches
5873sub git_patch {
5874        git_commitdiff(-format => 'patch', -single=> 1);
5875}
5876
5877sub git_patches {
5878        git_commitdiff(-format => 'patch');
5879}
5880
5881sub git_history {
5882        if (!defined $hash_base) {
5883                $hash_base = git_get_head_hash($project);
5884        }
5885        if (!defined $page) {
5886                $page = 0;
5887        }
5888        my $ftype;
5889        my %co = parse_commit($hash_base)
5890            or die_error(404, "Unknown commit object");
5891
5892        my $refs = git_get_references();
5893        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5894
5895        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5896                                       $file_name, "--full-history")
5897            or die_error(404, "No such file or directory on given branch");
5898
5899        if (!defined $hash && defined $file_name) {
5900                # some commits could have deleted file in question,
5901                # and not have it in tree, but one of them has to have it
5902                for (my $i = 0; $i <= @commitlist; $i++) {
5903                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5904                        last if defined $hash;
5905                }
5906        }
5907        if (defined $hash) {
5908                $ftype = git_get_type($hash);
5909        }
5910        if (!defined $ftype) {
5911                die_error(500, "Unknown type of object");
5912        }
5913
5914        my $paging_nav = '';
5915        if ($page > 0) {
5916                $paging_nav .=
5917                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5918                                               file_name=>$file_name)},
5919                                "first");
5920                $paging_nav .= " &sdot; " .
5921                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5922                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5923        } else {
5924                $paging_nav .= "first";
5925                $paging_nav .= " &sdot; prev";
5926        }
5927        my $next_link = '';
5928        if ($#commitlist >= 100) {
5929                $next_link =
5930                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5931                                 -accesskey => "n", -title => "Alt-n"}, "next");
5932                $paging_nav .= " &sdot; $next_link";
5933        } else {
5934                $paging_nav .= " &sdot; next";
5935        }
5936
5937        git_header_html();
5938        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5939        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5940        git_print_page_path($file_name, $ftype, $hash_base);
5941
5942        git_history_body(\@commitlist, 0, 99,
5943                         $refs, $hash_base, $ftype, $next_link);
5944
5945        git_footer_html();
5946}
5947
5948sub git_search {
5949        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5950        if (!defined $searchtext) {
5951                die_error(400, "Text field is empty");
5952        }
5953        if (!defined $hash) {
5954                $hash = git_get_head_hash($project);
5955        }
5956        my %co = parse_commit($hash);
5957        if (!%co) {
5958                die_error(404, "Unknown commit object");
5959        }
5960        if (!defined $page) {
5961                $page = 0;
5962        }
5963
5964        $searchtype ||= 'commit';
5965        if ($searchtype eq 'pickaxe') {
5966                # pickaxe may take all resources of your box and run for several minutes
5967                # with every query - so decide by yourself how public you make this feature
5968                gitweb_check_feature('pickaxe')
5969                    or die_error(403, "Pickaxe is disabled");
5970        }
5971        if ($searchtype eq 'grep') {
5972                gitweb_check_feature('grep')
5973                    or die_error(403, "Grep is disabled");
5974        }
5975
5976        git_header_html();
5977
5978        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5979                my $greptype;
5980                if ($searchtype eq 'commit') {
5981                        $greptype = "--grep=";
5982                } elsif ($searchtype eq 'author') {
5983                        $greptype = "--author=";
5984                } elsif ($searchtype eq 'committer') {
5985                        $greptype = "--committer=";
5986                }
5987                $greptype .= $searchtext;
5988                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5989                                               $greptype, '--regexp-ignore-case',
5990                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5991
5992                my $paging_nav = '';
5993                if ($page > 0) {
5994                        $paging_nav .=
5995                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5996                                                       searchtext=>$searchtext,
5997                                                       searchtype=>$searchtype)},
5998                                        "first");
5999                        $paging_nav .= " &sdot; " .
6000                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
6001                                         -accesskey => "p", -title => "Alt-p"}, "prev");
6002                } else {
6003                        $paging_nav .= "first";
6004                        $paging_nav .= " &sdot; prev";
6005                }
6006                my $next_link = '';
6007                if ($#commitlist >= 100) {
6008                        $next_link =
6009                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
6010                                         -accesskey => "n", -title => "Alt-n"}, "next");
6011                        $paging_nav .= " &sdot; $next_link";
6012                } else {
6013                        $paging_nav .= " &sdot; next";
6014                }
6015
6016                if ($#commitlist >= 100) {
6017                }
6018
6019                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6020                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6021                git_search_grep_body(\@commitlist, 0, 99, $next_link);
6022        }
6023
6024        if ($searchtype eq 'pickaxe') {
6025                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6026                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6027
6028                print "<table class=\"pickaxe search\">\n";
6029                my $alternate = 1;
6030                local $/ = "\n";
6031                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6032                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6033                        ($search_use_regexp ? '--pickaxe-regex' : ());
6034                undef %co;
6035                my @files;
6036                while (my $line = <$fd>) {
6037                        chomp $line;
6038                        next unless $line;
6039
6040                        my %set = parse_difftree_raw_line($line);
6041                        if (defined $set{'commit'}) {
6042                                # finish previous commit
6043                                if (%co) {
6044                                        print "</td>\n" .
6045                                              "<td class=\"link\">" .
6046                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6047                                              " | " .
6048                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6049                                        print "</td>\n" .
6050                                              "</tr>\n";
6051                                }
6052
6053                                if ($alternate) {
6054                                        print "<tr class=\"dark\">\n";
6055                                } else {
6056                                        print "<tr class=\"light\">\n";
6057                                }
6058                                $alternate ^= 1;
6059                                %co = parse_commit($set{'commit'});
6060                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6061                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6062                                      "<td><i>$author</i></td>\n" .
6063                                      "<td>" .
6064                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6065                                              -class => "list subject"},
6066                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6067                        } elsif (defined $set{'to_id'}) {
6068                                next if ($set{'to_id'} =~ m/^0{40}$/);
6069
6070                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6071                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6072                                              -class => "list"},
6073                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6074                                      "<br/>\n";
6075                        }
6076                }
6077                close $fd;
6078
6079                # finish last commit (warning: repetition!)
6080                if (%co) {
6081                        print "</td>\n" .
6082                              "<td class=\"link\">" .
6083                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6084                              " | " .
6085                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6086                        print "</td>\n" .
6087                              "</tr>\n";
6088                }
6089
6090                print "</table>\n";
6091        }
6092
6093        if ($searchtype eq 'grep') {
6094                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6095                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6096
6097                print "<table class=\"grep_search\">\n";
6098                my $alternate = 1;
6099                my $matches = 0;
6100                local $/ = "\n";
6101                open my $fd, "-|", git_cmd(), 'grep', '-n',
6102                        $search_use_regexp ? ('-E', '-i') : '-F',
6103                        $searchtext, $co{'tree'};
6104                my $lastfile = '';
6105                while (my $line = <$fd>) {
6106                        chomp $line;
6107                        my ($file, $lno, $ltext, $binary);
6108                        last if ($matches++ > 1000);
6109                        if ($line =~ /^Binary file (.+) matches$/) {
6110                                $file = $1;
6111                                $binary = 1;
6112                        } else {
6113                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6114                        }
6115                        if ($file ne $lastfile) {
6116                                $lastfile and print "</td></tr>\n";
6117                                if ($alternate++) {
6118                                        print "<tr class=\"dark\">\n";
6119                                } else {
6120                                        print "<tr class=\"light\">\n";
6121                                }
6122                                print "<td class=\"list\">".
6123                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6124                                                               file_name=>"$file"),
6125                                                -class => "list"}, esc_path($file));
6126                                print "</td><td>\n";
6127                                $lastfile = $file;
6128                        }
6129                        if ($binary) {
6130                                print "<div class=\"binary\">Binary file</div>\n";
6131                        } else {
6132                                $ltext = untabify($ltext);
6133                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6134                                        $ltext = esc_html($1, -nbsp=>1);
6135                                        $ltext .= '<span class="match">';
6136                                        $ltext .= esc_html($2, -nbsp=>1);
6137                                        $ltext .= '</span>';
6138                                        $ltext .= esc_html($3, -nbsp=>1);
6139                                } else {
6140                                        $ltext = esc_html($ltext, -nbsp=>1);
6141                                }
6142                                print "<div class=\"pre\">" .
6143                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6144                                                               file_name=>"$file").'#l'.$lno,
6145                                                -class => "linenr"}, sprintf('%4i', $lno))
6146                                        . ' ' .  $ltext . "</div>\n";
6147                        }
6148                }
6149                if ($lastfile) {
6150                        print "</td></tr>\n";
6151                        if ($matches > 1000) {
6152                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6153                        }
6154                } else {
6155                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
6156                }
6157                close $fd;
6158
6159                print "</table>\n";
6160        }
6161        git_footer_html();
6162}
6163
6164sub git_search_help {
6165        git_header_html();
6166        git_print_page_nav('','', $hash,$hash,$hash);
6167        print <<EOT;
6168<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6169regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6170the pattern entered is recognized as the POSIX extended
6171<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6172insensitive).</p>
6173<dl>
6174<dt><b>commit</b></dt>
6175<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6176EOT
6177        my $have_grep = gitweb_check_feature('grep');
6178        if ($have_grep) {
6179                print <<EOT;
6180<dt><b>grep</b></dt>
6181<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6182    a different one) are searched for the given pattern. On large trees, this search can take
6183a while and put some strain on the server, so please use it with some consideration. Note that
6184due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6185case-sensitive.</dd>
6186EOT
6187        }
6188        print <<EOT;
6189<dt><b>author</b></dt>
6190<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6191<dt><b>committer</b></dt>
6192<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6193EOT
6194        my $have_pickaxe = gitweb_check_feature('pickaxe');
6195        if ($have_pickaxe) {
6196                print <<EOT;
6197<dt><b>pickaxe</b></dt>
6198<dd>All commits that caused the string to appear or disappear from any file (changes that
6199added, removed or "modified" the string) will be listed. This search can take a while and
6200takes a lot of strain on the server, so please use it wisely. Note that since you may be
6201interested even in changes just changing the case as well, this search is case sensitive.</dd>
6202EOT
6203        }
6204        print "</dl>\n";
6205        git_footer_html();
6206}
6207
6208sub git_shortlog {
6209        my $head = git_get_head_hash($project);
6210        if (!defined $hash) {
6211                $hash = $head;
6212        }
6213        if (!defined $page) {
6214                $page = 0;
6215        }
6216        my $refs = git_get_references();
6217
6218        my $commit_hash = $hash;
6219        if (defined $hash_parent) {
6220                $commit_hash = "$hash_parent..$hash";
6221        }
6222        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6223
6224        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6225        my $next_link = '';
6226        if ($#commitlist >= 100) {
6227                $next_link =
6228                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
6229                                 -accesskey => "n", -title => "Alt-n"}, "next");
6230        }
6231        my $patch_max = gitweb_check_feature('patches');
6232        if ($patch_max) {
6233                if ($patch_max < 0 || @commitlist <= $patch_max) {
6234                        $paging_nav .= " &sdot; " .
6235                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
6236                                        "patches");
6237                }
6238        }
6239
6240        git_header_html();
6241        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6242        git_print_header_div('summary', $project);
6243
6244        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6245
6246        git_footer_html();
6247}
6248
6249## ......................................................................
6250## feeds (RSS, Atom; OPML)
6251
6252sub git_feed {
6253        my $format = shift || 'atom';
6254        my $have_blame = gitweb_check_feature('blame');
6255
6256        # Atom: http://www.atomenabled.org/developers/syndication/
6257        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6258        if ($format ne 'rss' && $format ne 'atom') {
6259                die_error(400, "Unknown web feed format");
6260        }
6261
6262        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6263        my $head = $hash || 'HEAD';
6264        my @commitlist = parse_commits($head, 150, 0, $file_name);
6265
6266        my %latest_commit;
6267        my %latest_date;
6268        my $content_type = "application/$format+xml";
6269        if (defined $cgi->http('HTTP_ACCEPT') &&
6270                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6271                # browser (feed reader) prefers text/xml
6272                $content_type = 'text/xml';
6273        }
6274        if (defined($commitlist[0])) {
6275                %latest_commit = %{$commitlist[0]};
6276                my $latest_epoch = $latest_commit{'committer_epoch'};
6277                %latest_date   = parse_date($latest_epoch);
6278                my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6279                if (defined $if_modified) {
6280                        my $since;
6281                        if (eval { require HTTP::Date; 1; }) {
6282                                $since = HTTP::Date::str2time($if_modified);
6283                        } elsif (eval { require Time::ParseDate; 1; }) {
6284                                $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6285                        }
6286                        if (defined $since && $latest_epoch <= $since) {
6287                                print $cgi->header(
6288                                        -type => $content_type,
6289                                        -charset => 'utf-8',
6290                                        -last_modified => $latest_date{'rfc2822'},
6291                                        -status => '304 Not Modified');
6292                                return;
6293                        }
6294                }
6295                print $cgi->header(
6296                        -type => $content_type,
6297                        -charset => 'utf-8',
6298                        -last_modified => $latest_date{'rfc2822'});
6299        } else {
6300                print $cgi->header(
6301                        -type => $content_type,
6302                        -charset => 'utf-8');
6303        }
6304
6305        # Optimization: skip generating the body if client asks only
6306        # for Last-Modified date.
6307        return if ($cgi->request_method() eq 'HEAD');
6308
6309        # header variables
6310        my $title = "$site_name - $project/$action";
6311        my $feed_type = 'log';
6312        if (defined $hash) {
6313                $title .= " - '$hash'";
6314                $feed_type = 'branch log';
6315                if (defined $file_name) {
6316                        $title .= " :: $file_name";
6317                        $feed_type = 'history';
6318                }
6319        } elsif (defined $file_name) {
6320                $title .= " - $file_name";
6321                $feed_type = 'history';
6322        }
6323        $title .= " $feed_type";
6324        my $descr = git_get_project_description($project);
6325        if (defined $descr) {
6326                $descr = esc_html($descr);
6327        } else {
6328                $descr = "$project " .
6329                         ($format eq 'rss' ? 'RSS' : 'Atom') .
6330                         " feed";
6331        }
6332        my $owner = git_get_project_owner($project);
6333        $owner = esc_html($owner);
6334
6335        #header
6336        my $alt_url;
6337        if (defined $file_name) {
6338                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6339        } elsif (defined $hash) {
6340                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6341        } else {
6342                $alt_url = href(-full=>1, action=>"summary");
6343        }
6344        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6345        if ($format eq 'rss') {
6346                print <<XML;
6347<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6348<channel>
6349XML
6350                print "<title>$title</title>\n" .
6351                      "<link>$alt_url</link>\n" .
6352                      "<description>$descr</description>\n" .
6353                      "<language>en</language>\n" .
6354                      # project owner is responsible for 'editorial' content
6355                      "<managingEditor>$owner</managingEditor>\n";
6356                if (defined $logo || defined $favicon) {
6357                        # prefer the logo to the favicon, since RSS
6358                        # doesn't allow both
6359                        my $img = esc_url($logo || $favicon);
6360                        print "<image>\n" .
6361                              "<url>$img</url>\n" .
6362                              "<title>$title</title>\n" .
6363                              "<link>$alt_url</link>\n" .
6364                              "</image>\n";
6365                }
6366                if (%latest_date) {
6367                        print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6368                        print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6369                }
6370                print "<generator>gitweb v.$version/$git_version</generator>\n";
6371        } elsif ($format eq 'atom') {
6372                print <<XML;
6373<feed xmlns="http://www.w3.org/2005/Atom">
6374XML
6375                print "<title>$title</title>\n" .
6376                      "<subtitle>$descr</subtitle>\n" .
6377                      '<link rel="alternate" type="text/html" href="' .
6378                      $alt_url . '" />' . "\n" .
6379                      '<link rel="self" type="' . $content_type . '" href="' .
6380                      $cgi->self_url() . '" />' . "\n" .
6381                      "<id>" . href(-full=>1) . "</id>\n" .
6382                      # use project owner for feed author
6383                      "<author><name>$owner</name></author>\n";
6384                if (defined $favicon) {
6385                        print "<icon>" . esc_url($favicon) . "</icon>\n";
6386                }
6387                if (defined $logo_url) {
6388                        # not twice as wide as tall: 72 x 27 pixels
6389                        print "<logo>" . esc_url($logo) . "</logo>\n";
6390                }
6391                if (! %latest_date) {
6392                        # dummy date to keep the feed valid until commits trickle in:
6393                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
6394                } else {
6395                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
6396                }
6397                print "<generator version='$version/$git_version'>gitweb</generator>\n";
6398        }
6399
6400        # contents
6401        for (my $i = 0; $i <= $#commitlist; $i++) {
6402                my %co = %{$commitlist[$i]};
6403                my $commit = $co{'id'};
6404                # we read 150, we always show 30 and the ones more recent than 48 hours
6405                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6406                        last;
6407                }
6408                my %cd = parse_date($co{'author_epoch'});
6409
6410                # get list of changed files
6411                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6412                        $co{'parent'} || "--root",
6413                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
6414                        or next;
6415                my @difftree = map { chomp; $_ } <$fd>;
6416                close $fd
6417                        or next;
6418
6419                # print element (entry, item)
6420                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6421                if ($format eq 'rss') {
6422                        print "<item>\n" .
6423                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
6424                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
6425                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6426                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6427                              "<link>$co_url</link>\n" .
6428                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
6429                              "<content:encoded>" .
6430                              "<![CDATA[\n";
6431                } elsif ($format eq 'atom') {
6432                        print "<entry>\n" .
6433                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6434                              "<updated>$cd{'iso-8601'}</updated>\n" .
6435                              "<author>\n" .
6436                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6437                        if ($co{'author_email'}) {
6438                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6439                        }
6440                        print "</author>\n" .
6441                              # use committer for contributor
6442                              "<contributor>\n" .
6443                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6444                        if ($co{'committer_email'}) {
6445                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6446                        }
6447                        print "</contributor>\n" .
6448                              "<published>$cd{'iso-8601'}</published>\n" .
6449                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6450                              "<id>$co_url</id>\n" .
6451                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6452                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6453                }
6454                my $comment = $co{'comment'};
6455                print "<pre>\n";
6456                foreach my $line (@$comment) {
6457                        $line = esc_html($line);
6458                        print "$line\n";
6459                }
6460                print "</pre><ul>\n";
6461                foreach my $difftree_line (@difftree) {
6462                        my %difftree = parse_difftree_raw_line($difftree_line);
6463                        next if !$difftree{'from_id'};
6464
6465                        my $file = $difftree{'file'} || $difftree{'to_file'};
6466
6467                        print "<li>" .
6468                              "[" .
6469                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6470                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6471                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6472                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
6473                                      -title => "diff"}, 'D');
6474                        if ($have_blame) {
6475                                print $cgi->a({-href => href(-full=>1, action=>"blame",
6476                                                             file_name=>$file, hash_base=>$commit),
6477                                              -title => "blame"}, 'B');
6478                        }
6479                        # if this is not a feed of a file history
6480                        if (!defined $file_name || $file_name ne $file) {
6481                                print $cgi->a({-href => href(-full=>1, action=>"history",
6482                                                             file_name=>$file, hash=>$commit),
6483                                              -title => "history"}, 'H');
6484                        }
6485                        $file = esc_path($file);
6486                        print "] ".
6487                              "$file</li>\n";
6488                }
6489                if ($format eq 'rss') {
6490                        print "</ul>]]>\n" .
6491                              "</content:encoded>\n" .
6492                              "</item>\n";
6493                } elsif ($format eq 'atom') {
6494                        print "</ul>\n</div>\n" .
6495                              "</content>\n" .
6496                              "</entry>\n";
6497                }
6498        }
6499
6500        # end of feed
6501        if ($format eq 'rss') {
6502                print "</channel>\n</rss>\n";
6503        } elsif ($format eq 'atom') {
6504                print "</feed>\n";
6505        }
6506}
6507
6508sub git_rss {
6509        git_feed('rss');
6510}
6511
6512sub git_atom {
6513        git_feed('atom');
6514}
6515
6516sub git_opml {
6517        my @list = git_get_projects_list();
6518
6519        print $cgi->header(
6520                -type => 'text/xml',
6521                -charset => 'utf-8',
6522                -content_disposition => 'inline; filename="opml.xml"');
6523
6524        print <<XML;
6525<?xml version="1.0" encoding="utf-8"?>
6526<opml version="1.0">
6527<head>
6528  <title>$site_name OPML Export</title>
6529</head>
6530<body>
6531<outline text="git RSS feeds">
6532XML
6533
6534        foreach my $pr (@list) {
6535                my %proj = %$pr;
6536                my $head = git_get_head_hash($proj{'path'});
6537                if (!defined $head) {
6538                        next;
6539                }
6540                $git_dir = "$projectroot/$proj{'path'}";
6541                my %co = parse_commit($head);
6542                if (!%co) {
6543                        next;
6544                }
6545
6546                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6547                my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6548                my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6549                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6550        }
6551        print <<XML;
6552</outline>
6553</body>
6554</opml>
6555XML
6556}