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