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