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