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