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