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