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