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