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