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