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