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