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