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