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