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