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