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