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