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