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