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