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