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