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