gitweb / gitweb.perlon commit gitweb: Add link to other blame implementation in blame views (87e573f)
   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        if ($format eq 'incremental') {
4888                $formats_nav .=
4889                        $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
4890                                "blame") . " (non-incremental)";
4891        } else {
4892                $formats_nav .=
4893                        $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
4894                                "blame") . " (incremental)";
4895        }
4896        $formats_nav .=
4897                " | " .
4898                $cgi->a({-href => href(action=>"history", -replay=>1)},
4899                        "history") .
4900                " | " .
4901                $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
4902                        "HEAD");
4903        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4904        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4905        git_print_page_path($file_name, $ftype, $hash_base);
4906
4907        # page body
4908        if ($format eq 'incremental') {
4909                print "<noscript>\n<div class=\"error\"><center><b>\n".
4910                      "This page requires JavaScript to run.\n Use ".
4911                      $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
4912                              'this page').
4913                      " instead.\n".
4914                      "</b></center></div>\n</noscript>\n";
4915
4916                print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
4917        }
4918
4919        print qq!<div class="page_body">\n!;
4920        print qq!<div id="progress_info">... / ...</div>\n!
4921                if ($format eq 'incremental');
4922        print qq!<table id="blame_table" class="blame" width="100%">\n!.
4923              #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
4924              qq!<thead>\n!.
4925              qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
4926              qq!</thead>\n!.
4927              qq!<tbody>\n!;
4928
4929        my @rev_color = qw(light dark);
4930        my $num_colors = scalar(@rev_color);
4931        my $current_color = 0;
4932
4933        if ($format eq 'incremental') {
4934                my $color_class = $rev_color[$current_color];
4935
4936                #contents of a file
4937                my $linenr = 0;
4938        LINE:
4939                while (my $line = <$fd>) {
4940                        chomp $line;
4941                        $linenr++;
4942
4943                        print qq!<tr id="l$linenr" class="$color_class">!.
4944                              qq!<td class="sha1"><a href=""> </a></td>!.
4945                              qq!<td class="linenr">!.
4946                              qq!<a class="linenr" href="">$linenr</a></td>!;
4947                        print qq!<td class="pre">! . esc_html($line) . "</td>\n";
4948                        print qq!</tr>\n!;
4949                }
4950
4951        } else { # porcelain, i.e. ordinary blame
4952                my %metainfo = (); # saves information about commits
4953
4954                # blame data
4955        LINE:
4956                while (my $line = <$fd>) {
4957                        chomp $line;
4958                        # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4959                        # no <lines in group> for subsequent lines in group of lines
4960                        my ($full_rev, $orig_lineno, $lineno, $group_size) =
4961                           ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4962                        if (!exists $metainfo{$full_rev}) {
4963                                $metainfo{$full_rev} = { 'nprevious' => 0 };
4964                        }
4965                        my $meta = $metainfo{$full_rev};
4966                        my $data;
4967                        while ($data = <$fd>) {
4968                                chomp $data;
4969                                last if ($data =~ s/^\t//); # contents of line
4970                                if ($data =~ /^(\S+)(?: (.*))?$/) {
4971                                        $meta->{$1} = $2 unless exists $meta->{$1};
4972                                }
4973                                if ($data =~ /^previous /) {
4974                                        $meta->{'nprevious'}++;
4975                                }
4976                        }
4977                        my $short_rev = substr($full_rev, 0, 8);
4978                        my $author = $meta->{'author'};
4979                        my %date =
4980                                parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4981                        my $date = $date{'iso-tz'};
4982                        if ($group_size) {
4983                                $current_color = ($current_color + 1) % $num_colors;
4984                        }
4985                        my $tr_class = $rev_color[$current_color];
4986                        $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4987                        $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4988                        $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4989                        print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4990                        if ($group_size) {
4991                                print "<td class=\"sha1\"";
4992                                print " title=\"". esc_html($author) . ", $date\"";
4993                                print " rowspan=\"$group_size\"" if ($group_size > 1);
4994                                print ">";
4995                                print $cgi->a({-href => href(action=>"commit",
4996                                                             hash=>$full_rev,
4997                                                             file_name=>$file_name)},
4998                                              esc_html($short_rev));
4999                                if ($group_size >= 2) {
5000                                        my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5001                                        if (@author_initials) {
5002                                                print "<br />" .
5003                                                      esc_html(join('', @author_initials));
5004                                                #           or join('.', ...)
5005                                        }
5006                                }
5007                                print "</td>\n";
5008                        }
5009                        # 'previous' <sha1 of parent commit> <filename at commit>
5010                        if (exists $meta->{'previous'} &&
5011                            $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5012                                $meta->{'parent'} = $1;
5013                                $meta->{'file_parent'} = unquote($2);
5014                        }
5015                        my $linenr_commit =
5016                                exists($meta->{'parent'}) ?
5017                                $meta->{'parent'} : $full_rev;
5018                        my $linenr_filename =
5019                                exists($meta->{'file_parent'}) ?
5020                                $meta->{'file_parent'} : unquote($meta->{'filename'});
5021                        my $blamed = href(action => 'blame',
5022                                          file_name => $linenr_filename,
5023                                          hash_base => $linenr_commit);
5024                        print "<td class=\"linenr\">";
5025                        print $cgi->a({ -href => "$blamed#l$orig_lineno",
5026                                        -class => "linenr" },
5027                                      esc_html($lineno));
5028                        print "</td>";
5029                        print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5030                        print "</tr>\n";
5031                } # end while
5032
5033        }
5034
5035        # footer
5036        print "</tbody>\n".
5037              "</table>\n"; # class="blame"
5038        print "</div>\n";   # class="blame_body"
5039        close $fd
5040                or print "Reading blob failed\n";
5041
5042        git_footer_html();
5043}
5044
5045sub git_blame {
5046        git_blame_common();
5047}
5048
5049sub git_blame_incremental {
5050        git_blame_common('incremental');
5051}
5052
5053sub git_blame_data {
5054        git_blame_common('data');
5055}
5056
5057sub git_tags {
5058        my $head = git_get_head_hash($project);
5059        git_header_html();
5060        git_print_page_nav('','', $head,undef,$head);
5061        git_print_header_div('summary', $project);
5062
5063        my @tagslist = git_get_tags_list();
5064        if (@tagslist) {
5065                git_tags_body(\@tagslist);
5066        }
5067        git_footer_html();
5068}
5069
5070sub git_heads {
5071        my $head = git_get_head_hash($project);
5072        git_header_html();
5073        git_print_page_nav('','', $head,undef,$head);
5074        git_print_header_div('summary', $project);
5075
5076        my @headslist = git_get_heads_list();
5077        if (@headslist) {
5078                git_heads_body(\@headslist, $head);
5079        }
5080        git_footer_html();
5081}
5082
5083sub git_blob_plain {
5084        my $type = shift;
5085        my $expires;
5086
5087        if (!defined $hash) {
5088                if (defined $file_name) {
5089                        my $base = $hash_base || git_get_head_hash($project);
5090                        $hash = git_get_hash_by_path($base, $file_name, "blob")
5091                                or die_error(404, "Cannot find file");
5092                } else {
5093                        die_error(400, "No file name defined");
5094                }
5095        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5096                # blobs defined by non-textual hash id's can be cached
5097                $expires = "+1d";
5098        }
5099
5100        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5101                or die_error(500, "Open git-cat-file blob '$hash' failed");
5102
5103        # content-type (can include charset)
5104        $type = blob_contenttype($fd, $file_name, $type);
5105
5106        # "save as" filename, even when no $file_name is given
5107        my $save_as = "$hash";
5108        if (defined $file_name) {
5109                $save_as = $file_name;
5110        } elsif ($type =~ m/^text\//) {
5111                $save_as .= '.txt';
5112        }
5113
5114        # With XSS prevention on, blobs of all types except a few known safe
5115        # ones are served with "Content-Disposition: attachment" to make sure
5116        # they don't run in our security domain.  For certain image types,
5117        # blob view writes an <img> tag referring to blob_plain view, and we
5118        # want to be sure not to break that by serving the image as an
5119        # attachment (though Firefox 3 doesn't seem to care).
5120        my $sandbox = $prevent_xss &&
5121                $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5122
5123        print $cgi->header(
5124                -type => $type,
5125                -expires => $expires,
5126                -content_disposition =>
5127                        ($sandbox ? 'attachment' : 'inline')
5128                        . '; filename="' . $save_as . '"');
5129        local $/ = undef;
5130        binmode STDOUT, ':raw';
5131        print <$fd>;
5132        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5133        close $fd;
5134}
5135
5136sub git_blob {
5137        my $expires;
5138
5139        if (!defined $hash) {
5140                if (defined $file_name) {
5141                        my $base = $hash_base || git_get_head_hash($project);
5142                        $hash = git_get_hash_by_path($base, $file_name, "blob")
5143                                or die_error(404, "Cannot find file");
5144                } else {
5145                        die_error(400, "No file name defined");
5146                }
5147        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5148                # blobs defined by non-textual hash id's can be cached
5149                $expires = "+1d";
5150        }
5151
5152        my $have_blame = gitweb_check_feature('blame');
5153        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5154                or die_error(500, "Couldn't cat $file_name, $hash");
5155        my $mimetype = blob_mimetype($fd, $file_name);
5156        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5157                close $fd;
5158                return git_blob_plain($mimetype);
5159        }
5160        # we can have blame only for text/* mimetype
5161        $have_blame &&= ($mimetype =~ m!^text/!);
5162
5163        git_header_html(undef, $expires);
5164        my $formats_nav = '';
5165        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5166                if (defined $file_name) {
5167                        if ($have_blame) {
5168                                $formats_nav .=
5169                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
5170                                                "blame") .
5171                                        " | ";
5172                        }
5173                        $formats_nav .=
5174                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5175                                        "history") .
5176                                " | " .
5177                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5178                                        "raw") .
5179                                " | " .
5180                                $cgi->a({-href => href(action=>"blob",
5181                                                       hash_base=>"HEAD", file_name=>$file_name)},
5182                                        "HEAD");
5183                } else {
5184                        $formats_nav .=
5185                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5186                                        "raw");
5187                }
5188                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5189                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5190        } else {
5191                print "<div class=\"page_nav\">\n" .
5192                      "<br/><br/></div>\n" .
5193                      "<div class=\"title\">$hash</div>\n";
5194        }
5195        git_print_page_path($file_name, "blob", $hash_base);
5196        print "<div class=\"page_body\">\n";
5197        if ($mimetype =~ m!^image/!) {
5198                print qq!<img type="$mimetype"!;
5199                if ($file_name) {
5200                        print qq! alt="$file_name" title="$file_name"!;
5201                }
5202                print qq! src="! .
5203                      href(action=>"blob_plain", hash=>$hash,
5204                           hash_base=>$hash_base, file_name=>$file_name) .
5205                      qq!" />\n!;
5206        } else {
5207                my $nr;
5208                while (my $line = <$fd>) {
5209                        chomp $line;
5210                        $nr++;
5211                        $line = untabify($line);
5212                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5213                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5214                }
5215        }
5216        close $fd
5217                or print "Reading blob failed.\n";
5218        print "</div>";
5219        git_footer_html();
5220}
5221
5222sub git_tree {
5223        if (!defined $hash_base) {
5224                $hash_base = "HEAD";
5225        }
5226        if (!defined $hash) {
5227                if (defined $file_name) {
5228                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5229                } else {
5230                        $hash = $hash_base;
5231                }
5232        }
5233        die_error(404, "No such tree") unless defined($hash);
5234
5235        my @entries = ();
5236        {
5237                local $/ = "\0";
5238                open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5239                        or die_error(500, "Open git-ls-tree failed");
5240                @entries = map { chomp; $_ } <$fd>;
5241                close $fd
5242                        or die_error(404, "Reading tree failed");
5243        }
5244
5245        my $refs = git_get_references();
5246        my $ref = format_ref_marker($refs, $hash_base);
5247        git_header_html();
5248        my $basedir = '';
5249        my $have_blame = gitweb_check_feature('blame');
5250        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5251                my @views_nav = ();
5252                if (defined $file_name) {
5253                        push @views_nav,
5254                                $cgi->a({-href => href(action=>"history", -replay=>1)},
5255                                        "history"),
5256                                $cgi->a({-href => href(action=>"tree",
5257                                                       hash_base=>"HEAD", file_name=>$file_name)},
5258                                        "HEAD"),
5259                }
5260                my $snapshot_links = format_snapshot_links($hash);
5261                if (defined $snapshot_links) {
5262                        # FIXME: Should be available when we have no hash base as well.
5263                        push @views_nav, $snapshot_links;
5264                }
5265                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5266                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5267        } else {
5268                undef $hash_base;
5269                print "<div class=\"page_nav\">\n";
5270                print "<br/><br/></div>\n";
5271                print "<div class=\"title\">$hash</div>\n";
5272        }
5273        if (defined $file_name) {
5274                $basedir = $file_name;
5275                if ($basedir ne '' && substr($basedir, -1) ne '/') {
5276                        $basedir .= '/';
5277                }
5278                git_print_page_path($file_name, 'tree', $hash_base);
5279        }
5280        print "<div class=\"page_body\">\n";
5281        print "<table class=\"tree\">\n";
5282        my $alternate = 1;
5283        # '..' (top directory) link if possible
5284        if (defined $hash_base &&
5285            defined $file_name && $file_name =~ m![^/]+$!) {
5286                if ($alternate) {
5287                        print "<tr class=\"dark\">\n";
5288                } else {
5289                        print "<tr class=\"light\">\n";
5290                }
5291                $alternate ^= 1;
5292
5293                my $up = $file_name;
5294                $up =~ s!/?[^/]+$!!;
5295                undef $up unless $up;
5296                # based on git_print_tree_entry
5297                print '<td class="mode">' . mode_str('040000') . "</td>\n";
5298                print '<td class="list">';
5299                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5300                                             file_name=>$up)},
5301                              "..");
5302                print "</td>\n";
5303                print "<td class=\"link\"></td>\n";
5304
5305                print "</tr>\n";
5306        }
5307        foreach my $line (@entries) {
5308                my %t = parse_ls_tree_line($line, -z => 1);
5309
5310                if ($alternate) {
5311                        print "<tr class=\"dark\">\n";
5312                } else {
5313                        print "<tr class=\"light\">\n";
5314                }
5315                $alternate ^= 1;
5316
5317                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5318
5319                print "</tr>\n";
5320        }
5321        print "</table>\n" .
5322              "</div>";
5323        git_footer_html();
5324}
5325
5326sub git_snapshot {
5327        my $format = $input_params{'snapshot_format'};
5328        if (!@snapshot_fmts) {
5329                die_error(403, "Snapshots not allowed");
5330        }
5331        # default to first supported snapshot format
5332        $format ||= $snapshot_fmts[0];
5333        if ($format !~ m/^[a-z0-9]+$/) {
5334                die_error(400, "Invalid snapshot format parameter");
5335        } elsif (!exists($known_snapshot_formats{$format})) {
5336                die_error(400, "Unknown snapshot format");
5337        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5338                die_error(403, "Unsupported snapshot format");
5339        }
5340
5341        if (!defined $hash) {
5342                $hash = git_get_head_hash($project);
5343        }
5344
5345        my $name = $project;
5346        $name =~ s,([^/])/*\.git$,$1,;
5347        $name = basename($name);
5348        my $filename = to_utf8($name);
5349        $name =~ s/\047/\047\\\047\047/g;
5350        my $cmd;
5351        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5352        $cmd = quote_command(
5353                git_cmd(), 'archive',
5354                "--format=$known_snapshot_formats{$format}{'format'}",
5355                "--prefix=$name/", $hash);
5356        if (exists $known_snapshot_formats{$format}{'compressor'}) {
5357                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5358        }
5359
5360        print $cgi->header(
5361                -type => $known_snapshot_formats{$format}{'type'},
5362                -content_disposition => 'inline; filename="' . "$filename" . '"',
5363                -status => '200 OK');
5364
5365        open my $fd, "-|", $cmd
5366                or die_error(500, "Execute git-archive failed");
5367        binmode STDOUT, ':raw';
5368        print <$fd>;
5369        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5370        close $fd;
5371}
5372
5373sub git_log {
5374        my $head = git_get_head_hash($project);
5375        if (!defined $hash) {
5376                $hash = $head;
5377        }
5378        if (!defined $page) {
5379                $page = 0;
5380        }
5381        my $refs = git_get_references();
5382
5383        my @commitlist = parse_commits($hash, 101, (100 * $page));
5384
5385        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5386
5387        my ($patch_max) = gitweb_get_feature('patches');
5388        if ($patch_max) {
5389                if ($patch_max < 0 || @commitlist <= $patch_max) {
5390                        $paging_nav .= " &sdot; " .
5391                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
5392                                        "patches");
5393                }
5394        }
5395
5396        git_header_html();
5397        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5398
5399        if (!@commitlist) {
5400                my %co = parse_commit($hash);
5401
5402                git_print_header_div('summary', $project);
5403                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5404        }
5405        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5406        for (my $i = 0; $i <= $to; $i++) {
5407                my %co = %{$commitlist[$i]};
5408                next if !%co;
5409                my $commit = $co{'id'};
5410                my $ref = format_ref_marker($refs, $commit);
5411                my %ad = parse_date($co{'author_epoch'});
5412                git_print_header_div('commit',
5413                               "<span class=\"age\">$co{'age_string'}</span>" .
5414                               esc_html($co{'title'}) . $ref,
5415                               $commit);
5416                print "<div class=\"title_text\">\n" .
5417                      "<div class=\"log_link\">\n" .
5418                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5419                      " | " .
5420                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5421                      " | " .
5422                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5423                      "<br/>\n" .
5424                      "</div>\n";
5425                      git_print_authorship(\%co, -tag => 'span');
5426                      print "<br/>\n</div>\n";
5427
5428                print "<div class=\"log_body\">\n";
5429                git_print_log($co{'comment'}, -final_empty_line=> 1);
5430                print "</div>\n";
5431        }
5432        if ($#commitlist >= 100) {
5433                print "<div class=\"page_nav\">\n";
5434                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5435                               -accesskey => "n", -title => "Alt-n"}, "next");
5436                print "</div>\n";
5437        }
5438        git_footer_html();
5439}
5440
5441sub git_commit {
5442        $hash ||= $hash_base || "HEAD";
5443        my %co = parse_commit($hash)
5444            or die_error(404, "Unknown commit object");
5445
5446        my $parent  = $co{'parent'};
5447        my $parents = $co{'parents'}; # listref
5448
5449        # we need to prepare $formats_nav before any parameter munging
5450        my $formats_nav;
5451        if (!defined $parent) {
5452                # --root commitdiff
5453                $formats_nav .= '(initial)';
5454        } elsif (@$parents == 1) {
5455                # single parent commit
5456                $formats_nav .=
5457                        '(parent: ' .
5458                        $cgi->a({-href => href(action=>"commit",
5459                                               hash=>$parent)},
5460                                esc_html(substr($parent, 0, 7))) .
5461                        ')';
5462        } else {
5463                # merge commit
5464                $formats_nav .=
5465                        '(merge: ' .
5466                        join(' ', map {
5467                                $cgi->a({-href => href(action=>"commit",
5468                                                       hash=>$_)},
5469                                        esc_html(substr($_, 0, 7)));
5470                        } @$parents ) .
5471                        ')';
5472        }
5473        if (gitweb_check_feature('patches')) {
5474                $formats_nav .= " | " .
5475                        $cgi->a({-href => href(action=>"patch", -replay=>1)},
5476                                "patch");
5477        }
5478
5479        if (!defined $parent) {
5480                $parent = "--root";
5481        }
5482        my @difftree;
5483        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5484                @diff_opts,
5485                (@$parents <= 1 ? $parent : '-c'),
5486                $hash, "--"
5487                or die_error(500, "Open git-diff-tree failed");
5488        @difftree = map { chomp; $_ } <$fd>;
5489        close $fd or die_error(404, "Reading git-diff-tree failed");
5490
5491        # non-textual hash id's can be cached
5492        my $expires;
5493        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5494                $expires = "+1d";
5495        }
5496        my $refs = git_get_references();
5497        my $ref = format_ref_marker($refs, $co{'id'});
5498
5499        git_header_html(undef, $expires);
5500        git_print_page_nav('commit', '',
5501                           $hash, $co{'tree'}, $hash,
5502                           $formats_nav);
5503
5504        if (defined $co{'parent'}) {
5505                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5506        } else {
5507                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5508        }
5509        print "<div class=\"title_text\">\n" .
5510              "<table class=\"object_header\">\n";
5511        git_print_authorship_rows(\%co);
5512        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5513        print "<tr>" .
5514              "<td>tree</td>" .
5515              "<td class=\"sha1\">" .
5516              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5517                       class => "list"}, $co{'tree'}) .
5518              "</td>" .
5519              "<td class=\"link\">" .
5520              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5521                      "tree");
5522        my $snapshot_links = format_snapshot_links($hash);
5523        if (defined $snapshot_links) {
5524                print " | " . $snapshot_links;
5525        }
5526        print "</td>" .
5527              "</tr>\n";
5528
5529        foreach my $par (@$parents) {
5530                print "<tr>" .
5531                      "<td>parent</td>" .
5532                      "<td class=\"sha1\">" .
5533                      $cgi->a({-href => href(action=>"commit", hash=>$par),
5534                               class => "list"}, $par) .
5535                      "</td>" .
5536                      "<td class=\"link\">" .
5537                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5538                      " | " .
5539                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5540                      "</td>" .
5541                      "</tr>\n";
5542        }
5543        print "</table>".
5544              "</div>\n";
5545
5546        print "<div class=\"page_body\">\n";
5547        git_print_log($co{'comment'});
5548        print "</div>\n";
5549
5550        git_difftree_body(\@difftree, $hash, @$parents);
5551
5552        git_footer_html();
5553}
5554
5555sub git_object {
5556        # object is defined by:
5557        # - hash or hash_base alone
5558        # - hash_base and file_name
5559        my $type;
5560
5561        # - hash or hash_base alone
5562        if ($hash || ($hash_base && !defined $file_name)) {
5563                my $object_id = $hash || $hash_base;
5564
5565                open my $fd, "-|", quote_command(
5566                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5567                        or die_error(404, "Object does not exist");
5568                $type = <$fd>;
5569                chomp $type;
5570                close $fd
5571                        or die_error(404, "Object does not exist");
5572
5573        # - hash_base and file_name
5574        } elsif ($hash_base && defined $file_name) {
5575                $file_name =~ s,/+$,,;
5576
5577                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5578                        or die_error(404, "Base object does not exist");
5579
5580                # here errors should not hapen
5581                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5582                        or die_error(500, "Open git-ls-tree failed");
5583                my $line = <$fd>;
5584                close $fd;
5585
5586                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5587                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5588                        die_error(404, "File or directory for given base does not exist");
5589                }
5590                $type = $2;
5591                $hash = $3;
5592        } else {
5593                die_error(400, "Not enough information to find object");
5594        }
5595
5596        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5597                                          hash=>$hash, hash_base=>$hash_base,
5598                                          file_name=>$file_name),
5599                             -status => '302 Found');
5600}
5601
5602sub git_blobdiff {
5603        my $format = shift || 'html';
5604
5605        my $fd;
5606        my @difftree;
5607        my %diffinfo;
5608        my $expires;
5609
5610        # preparing $fd and %diffinfo for git_patchset_body
5611        # new style URI
5612        if (defined $hash_base && defined $hash_parent_base) {
5613                if (defined $file_name) {
5614                        # read raw output
5615                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5616                                $hash_parent_base, $hash_base,
5617                                "--", (defined $file_parent ? $file_parent : ()), $file_name
5618                                or die_error(500, "Open git-diff-tree failed");
5619                        @difftree = map { chomp; $_ } <$fd>;
5620                        close $fd
5621                                or die_error(404, "Reading git-diff-tree failed");
5622                        @difftree
5623                                or die_error(404, "Blob diff not found");
5624
5625                } elsif (defined $hash &&
5626                         $hash =~ /[0-9a-fA-F]{40}/) {
5627                        # try to find filename from $hash
5628
5629                        # read filtered raw output
5630                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5631                                $hash_parent_base, $hash_base, "--"
5632                                or die_error(500, "Open git-diff-tree failed");
5633                        @difftree =
5634                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5635                                # $hash == to_id
5636                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5637                                map { chomp; $_ } <$fd>;
5638                        close $fd
5639                                or die_error(404, "Reading git-diff-tree failed");
5640                        @difftree
5641                                or die_error(404, "Blob diff not found");
5642
5643                } else {
5644                        die_error(400, "Missing one of the blob diff parameters");
5645                }
5646
5647                if (@difftree > 1) {
5648                        die_error(400, "Ambiguous blob diff specification");
5649                }
5650
5651                %diffinfo = parse_difftree_raw_line($difftree[0]);
5652                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5653                $file_name   ||= $diffinfo{'to_file'};
5654
5655                $hash_parent ||= $diffinfo{'from_id'};
5656                $hash        ||= $diffinfo{'to_id'};
5657
5658                # non-textual hash id's can be cached
5659                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5660                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5661                        $expires = '+1d';
5662                }
5663
5664                # open patch output
5665                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5666                        '-p', ($format eq 'html' ? "--full-index" : ()),
5667                        $hash_parent_base, $hash_base,
5668                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5669                        or die_error(500, "Open git-diff-tree failed");
5670        }
5671
5672        # old/legacy style URI -- not generated anymore since 1.4.3.
5673        if (!%diffinfo) {
5674                die_error('404 Not Found', "Missing one of the blob diff parameters")
5675        }
5676
5677        # header
5678        if ($format eq 'html') {
5679                my $formats_nav =
5680                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5681                                "raw");
5682                git_header_html(undef, $expires);
5683                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5684                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5685                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5686                } else {
5687                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5688                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5689                }
5690                if (defined $file_name) {
5691                        git_print_page_path($file_name, "blob", $hash_base);
5692                } else {
5693                        print "<div class=\"page_path\"></div>\n";
5694                }
5695
5696        } elsif ($format eq 'plain') {
5697                print $cgi->header(
5698                        -type => 'text/plain',
5699                        -charset => 'utf-8',
5700                        -expires => $expires,
5701                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5702
5703                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5704
5705        } else {
5706                die_error(400, "Unknown blobdiff format");
5707        }
5708
5709        # patch
5710        if ($format eq 'html') {
5711                print "<div class=\"page_body\">\n";
5712
5713                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5714                close $fd;
5715
5716                print "</div>\n"; # class="page_body"
5717                git_footer_html();
5718
5719        } else {
5720                while (my $line = <$fd>) {
5721                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5722                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5723
5724                        print $line;
5725
5726                        last if $line =~ m!^\+\+\+!;
5727                }
5728                local $/ = undef;
5729                print <$fd>;
5730                close $fd;
5731        }
5732}
5733
5734sub git_blobdiff_plain {
5735        git_blobdiff('plain');
5736}
5737
5738sub git_commitdiff {
5739        my %params = @_;
5740        my $format = $params{-format} || 'html';
5741
5742        my ($patch_max) = gitweb_get_feature('patches');
5743        if ($format eq 'patch') {
5744                die_error(403, "Patch view not allowed") unless $patch_max;
5745        }
5746
5747        $hash ||= $hash_base || "HEAD";
5748        my %co = parse_commit($hash)
5749            or die_error(404, "Unknown commit object");
5750
5751        # choose format for commitdiff for merge
5752        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5753                $hash_parent = '--cc';
5754        }
5755        # we need to prepare $formats_nav before almost any parameter munging
5756        my $formats_nav;
5757        if ($format eq 'html') {
5758                $formats_nav =
5759                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5760                                "raw");
5761                if ($patch_max) {
5762                        $formats_nav .= " | " .
5763                                $cgi->a({-href => href(action=>"patch", -replay=>1)},
5764                                        "patch");
5765                }
5766
5767                if (defined $hash_parent &&
5768                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5769                        # commitdiff with two commits given
5770                        my $hash_parent_short = $hash_parent;
5771                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5772                                $hash_parent_short = substr($hash_parent, 0, 7);
5773                        }
5774                        $formats_nav .=
5775                                ' (from';
5776                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5777                                if ($co{'parents'}[$i] eq $hash_parent) {
5778                                        $formats_nav .= ' parent ' . ($i+1);
5779                                        last;
5780                                }
5781                        }
5782                        $formats_nav .= ': ' .
5783                                $cgi->a({-href => href(action=>"commitdiff",
5784                                                       hash=>$hash_parent)},
5785                                        esc_html($hash_parent_short)) .
5786                                ')';
5787                } elsif (!$co{'parent'}) {
5788                        # --root commitdiff
5789                        $formats_nav .= ' (initial)';
5790                } elsif (scalar @{$co{'parents'}} == 1) {
5791                        # single parent commit
5792                        $formats_nav .=
5793                                ' (parent: ' .
5794                                $cgi->a({-href => href(action=>"commitdiff",
5795                                                       hash=>$co{'parent'})},
5796                                        esc_html(substr($co{'parent'}, 0, 7))) .
5797                                ')';
5798                } else {
5799                        # merge commit
5800                        if ($hash_parent eq '--cc') {
5801                                $formats_nav .= ' | ' .
5802                                        $cgi->a({-href => href(action=>"commitdiff",
5803                                                               hash=>$hash, hash_parent=>'-c')},
5804                                                'combined');
5805                        } else { # $hash_parent eq '-c'
5806                                $formats_nav .= ' | ' .
5807                                        $cgi->a({-href => href(action=>"commitdiff",
5808                                                               hash=>$hash, hash_parent=>'--cc')},
5809                                                'compact');
5810                        }
5811                        $formats_nav .=
5812                                ' (merge: ' .
5813                                join(' ', map {
5814                                        $cgi->a({-href => href(action=>"commitdiff",
5815                                                               hash=>$_)},
5816                                                esc_html(substr($_, 0, 7)));
5817                                } @{$co{'parents'}} ) .
5818                                ')';
5819                }
5820        }
5821
5822        my $hash_parent_param = $hash_parent;
5823        if (!defined $hash_parent_param) {
5824                # --cc for multiple parents, --root for parentless
5825                $hash_parent_param =
5826                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5827        }
5828
5829        # read commitdiff
5830        my $fd;
5831        my @difftree;
5832        if ($format eq 'html') {
5833                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5834                        "--no-commit-id", "--patch-with-raw", "--full-index",
5835                        $hash_parent_param, $hash, "--"
5836                        or die_error(500, "Open git-diff-tree failed");
5837
5838                while (my $line = <$fd>) {
5839                        chomp $line;
5840                        # empty line ends raw part of diff-tree output
5841                        last unless $line;
5842                        push @difftree, scalar parse_difftree_raw_line($line);
5843                }
5844
5845        } elsif ($format eq 'plain') {
5846                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5847                        '-p', $hash_parent_param, $hash, "--"
5848                        or die_error(500, "Open git-diff-tree failed");
5849        } elsif ($format eq 'patch') {
5850                # For commit ranges, we limit the output to the number of
5851                # patches specified in the 'patches' feature.
5852                # For single commits, we limit the output to a single patch,
5853                # diverging from the git-format-patch default.
5854                my @commit_spec = ();
5855                if ($hash_parent) {
5856                        if ($patch_max > 0) {
5857                                push @commit_spec, "-$patch_max";
5858                        }
5859                        push @commit_spec, '-n', "$hash_parent..$hash";
5860                } else {
5861                        if ($params{-single}) {
5862                                push @commit_spec, '-1';
5863                        } else {
5864                                if ($patch_max > 0) {
5865                                        push @commit_spec, "-$patch_max";
5866                                }
5867                                push @commit_spec, "-n";
5868                        }
5869                        push @commit_spec, '--root', $hash;
5870                }
5871                open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5872                        '--stdout', @commit_spec
5873                        or die_error(500, "Open git-format-patch failed");
5874        } else {
5875                die_error(400, "Unknown commitdiff format");
5876        }
5877
5878        # non-textual hash id's can be cached
5879        my $expires;
5880        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5881                $expires = "+1d";
5882        }
5883
5884        # write commit message
5885        if ($format eq 'html') {
5886                my $refs = git_get_references();
5887                my $ref = format_ref_marker($refs, $co{'id'});
5888
5889                git_header_html(undef, $expires);
5890                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5891                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5892                print "<div class=\"title_text\">\n" .
5893                      "<table class=\"object_header\">\n";
5894                git_print_authorship_rows(\%co);
5895                print "</table>".
5896                      "</div>\n";
5897                print "<div class=\"page_body\">\n";
5898                if (@{$co{'comment'}} > 1) {
5899                        print "<div class=\"log\">\n";
5900                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5901                        print "</div>\n"; # class="log"
5902                }
5903
5904        } elsif ($format eq 'plain') {
5905                my $refs = git_get_references("tags");
5906                my $tagname = git_get_rev_name_tags($hash);
5907                my $filename = basename($project) . "-$hash.patch";
5908
5909                print $cgi->header(
5910                        -type => 'text/plain',
5911                        -charset => 'utf-8',
5912                        -expires => $expires,
5913                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5914                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5915                print "From: " . to_utf8($co{'author'}) . "\n";
5916                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5917                print "Subject: " . to_utf8($co{'title'}) . "\n";
5918
5919                print "X-Git-Tag: $tagname\n" if $tagname;
5920                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5921
5922                foreach my $line (@{$co{'comment'}}) {
5923                        print to_utf8($line) . "\n";
5924                }
5925                print "---\n\n";
5926        } elsif ($format eq 'patch') {
5927                my $filename = basename($project) . "-$hash.patch";
5928
5929                print $cgi->header(
5930                        -type => 'text/plain',
5931                        -charset => 'utf-8',
5932                        -expires => $expires,
5933                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5934        }
5935
5936        # write patch
5937        if ($format eq 'html') {
5938                my $use_parents = !defined $hash_parent ||
5939                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5940                git_difftree_body(\@difftree, $hash,
5941                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5942                print "<br/>\n";
5943
5944                git_patchset_body($fd, \@difftree, $hash,
5945                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5946                close $fd;
5947                print "</div>\n"; # class="page_body"
5948                git_footer_html();
5949
5950        } elsif ($format eq 'plain') {
5951                local $/ = undef;
5952                print <$fd>;
5953                close $fd
5954                        or print "Reading git-diff-tree failed\n";
5955        } elsif ($format eq 'patch') {
5956                local $/ = undef;
5957                print <$fd>;
5958                close $fd
5959                        or print "Reading git-format-patch failed\n";
5960        }
5961}
5962
5963sub git_commitdiff_plain {
5964        git_commitdiff(-format => 'plain');
5965}
5966
5967# format-patch-style patches
5968sub git_patch {
5969        git_commitdiff(-format => 'patch', -single=> 1);
5970}
5971
5972sub git_patches {
5973        git_commitdiff(-format => 'patch');
5974}
5975
5976sub git_history {
5977        if (!defined $hash_base) {
5978                $hash_base = git_get_head_hash($project);
5979        }
5980        if (!defined $page) {
5981                $page = 0;
5982        }
5983        my $ftype;
5984        my %co = parse_commit($hash_base)
5985            or die_error(404, "Unknown commit object");
5986
5987        my $refs = git_get_references();
5988        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5989
5990        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5991                                       $file_name, "--full-history")
5992            or die_error(404, "No such file or directory on given branch");
5993
5994        if (!defined $hash && defined $file_name) {
5995                # some commits could have deleted file in question,
5996                # and not have it in tree, but one of them has to have it
5997                for (my $i = 0; $i <= @commitlist; $i++) {
5998                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5999                        last if defined $hash;
6000                }
6001        }
6002        if (defined $hash) {
6003                $ftype = git_get_type($hash);
6004        }
6005        if (!defined $ftype) {
6006                die_error(500, "Unknown type of object");
6007        }
6008
6009        my $paging_nav = '';
6010        if ($page > 0) {
6011                $paging_nav .=
6012                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
6013                                               file_name=>$file_name)},
6014                                "first");
6015                $paging_nav .= " &sdot; " .
6016                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
6017                                 -accesskey => "p", -title => "Alt-p"}, "prev");
6018        } else {
6019                $paging_nav .= "first";
6020                $paging_nav .= " &sdot; prev";
6021        }
6022        my $next_link = '';
6023        if ($#commitlist >= 100) {
6024                $next_link =
6025                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
6026                                 -accesskey => "n", -title => "Alt-n"}, "next");
6027                $paging_nav .= " &sdot; $next_link";
6028        } else {
6029                $paging_nav .= " &sdot; next";
6030        }
6031
6032        git_header_html();
6033        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
6034        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6035        git_print_page_path($file_name, $ftype, $hash_base);
6036
6037        git_history_body(\@commitlist, 0, 99,
6038                         $refs, $hash_base, $ftype, $next_link);
6039
6040        git_footer_html();
6041}
6042
6043sub git_search {
6044        gitweb_check_feature('search') or die_error(403, "Search is disabled");
6045        if (!defined $searchtext) {
6046                die_error(400, "Text field is empty");
6047        }
6048        if (!defined $hash) {
6049                $hash = git_get_head_hash($project);
6050        }
6051        my %co = parse_commit($hash);
6052        if (!%co) {
6053                die_error(404, "Unknown commit object");
6054        }
6055        if (!defined $page) {
6056                $page = 0;
6057        }
6058
6059        $searchtype ||= 'commit';
6060        if ($searchtype eq 'pickaxe') {
6061                # pickaxe may take all resources of your box and run for several minutes
6062                # with every query - so decide by yourself how public you make this feature
6063                gitweb_check_feature('pickaxe')
6064                    or die_error(403, "Pickaxe is disabled");
6065        }
6066        if ($searchtype eq 'grep') {
6067                gitweb_check_feature('grep')
6068                    or die_error(403, "Grep is disabled");
6069        }
6070
6071        git_header_html();
6072
6073        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6074                my $greptype;
6075                if ($searchtype eq 'commit') {
6076                        $greptype = "--grep=";
6077                } elsif ($searchtype eq 'author') {
6078                        $greptype = "--author=";
6079                } elsif ($searchtype eq 'committer') {
6080                        $greptype = "--committer=";
6081                }
6082                $greptype .= $searchtext;
6083                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6084                                               $greptype, '--regexp-ignore-case',
6085                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6086
6087                my $paging_nav = '';
6088                if ($page > 0) {
6089                        $paging_nav .=
6090                                $cgi->a({-href => href(action=>"search", hash=>$hash,
6091                                                       searchtext=>$searchtext,
6092                                                       searchtype=>$searchtype)},
6093                                        "first");
6094                        $paging_nav .= " &sdot; " .
6095                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
6096                                         -accesskey => "p", -title => "Alt-p"}, "prev");
6097                } else {
6098                        $paging_nav .= "first";
6099                        $paging_nav .= " &sdot; prev";
6100                }
6101                my $next_link = '';
6102                if ($#commitlist >= 100) {
6103                        $next_link =
6104                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
6105                                         -accesskey => "n", -title => "Alt-n"}, "next");
6106                        $paging_nav .= " &sdot; $next_link";
6107                } else {
6108                        $paging_nav .= " &sdot; next";
6109                }
6110
6111                if ($#commitlist >= 100) {
6112                }
6113
6114                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6115                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6116                git_search_grep_body(\@commitlist, 0, 99, $next_link);
6117        }
6118
6119        if ($searchtype eq 'pickaxe') {
6120                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6121                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6122
6123                print "<table class=\"pickaxe search\">\n";
6124                my $alternate = 1;
6125                local $/ = "\n";
6126                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6127                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6128                        ($search_use_regexp ? '--pickaxe-regex' : ());
6129                undef %co;
6130                my @files;
6131                while (my $line = <$fd>) {
6132                        chomp $line;
6133                        next unless $line;
6134
6135                        my %set = parse_difftree_raw_line($line);
6136                        if (defined $set{'commit'}) {
6137                                # finish previous commit
6138                                if (%co) {
6139                                        print "</td>\n" .
6140                                              "<td class=\"link\">" .
6141                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6142                                              " | " .
6143                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6144                                        print "</td>\n" .
6145                                              "</tr>\n";
6146                                }
6147
6148                                if ($alternate) {
6149                                        print "<tr class=\"dark\">\n";
6150                                } else {
6151                                        print "<tr class=\"light\">\n";
6152                                }
6153                                $alternate ^= 1;
6154                                %co = parse_commit($set{'commit'});
6155                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6156                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6157                                      "<td><i>$author</i></td>\n" .
6158                                      "<td>" .
6159                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6160                                              -class => "list subject"},
6161                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6162                        } elsif (defined $set{'to_id'}) {
6163                                next if ($set{'to_id'} =~ m/^0{40}$/);
6164
6165                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6166                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6167                                              -class => "list"},
6168                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6169                                      "<br/>\n";
6170                        }
6171                }
6172                close $fd;
6173
6174                # finish last commit (warning: repetition!)
6175                if (%co) {
6176                        print "</td>\n" .
6177                              "<td class=\"link\">" .
6178                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6179                              " | " .
6180                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6181                        print "</td>\n" .
6182                              "</tr>\n";
6183                }
6184
6185                print "</table>\n";
6186        }
6187
6188        if ($searchtype eq 'grep') {
6189                git_print_page_nav('','', $hash,$co{'tree'},$hash);
6190                git_print_header_div('commit', esc_html($co{'title'}), $hash);
6191
6192                print "<table class=\"grep_search\">\n";
6193                my $alternate = 1;
6194                my $matches = 0;
6195                local $/ = "\n";
6196                open my $fd, "-|", git_cmd(), 'grep', '-n',
6197                        $search_use_regexp ? ('-E', '-i') : '-F',
6198                        $searchtext, $co{'tree'};
6199                my $lastfile = '';
6200                while (my $line = <$fd>) {
6201                        chomp $line;
6202                        my ($file, $lno, $ltext, $binary);
6203                        last if ($matches++ > 1000);
6204                        if ($line =~ /^Binary file (.+) matches$/) {
6205                                $file = $1;
6206                                $binary = 1;
6207                        } else {
6208                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6209                        }
6210                        if ($file ne $lastfile) {
6211                                $lastfile and print "</td></tr>\n";
6212                                if ($alternate++) {
6213                                        print "<tr class=\"dark\">\n";
6214                                } else {
6215                                        print "<tr class=\"light\">\n";
6216                                }
6217                                print "<td class=\"list\">".
6218                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6219                                                               file_name=>"$file"),
6220                                                -class => "list"}, esc_path($file));
6221                                print "</td><td>\n";
6222                                $lastfile = $file;
6223                        }
6224                        if ($binary) {
6225                                print "<div class=\"binary\">Binary file</div>\n";
6226                        } else {
6227                                $ltext = untabify($ltext);
6228                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6229                                        $ltext = esc_html($1, -nbsp=>1);
6230                                        $ltext .= '<span class="match">';
6231                                        $ltext .= esc_html($2, -nbsp=>1);
6232                                        $ltext .= '</span>';
6233                                        $ltext .= esc_html($3, -nbsp=>1);
6234                                } else {
6235                                        $ltext = esc_html($ltext, -nbsp=>1);
6236                                }
6237                                print "<div class=\"pre\">" .
6238                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6239                                                               file_name=>"$file").'#l'.$lno,
6240                                                -class => "linenr"}, sprintf('%4i', $lno))
6241                                        . ' ' .  $ltext . "</div>\n";
6242                        }
6243                }
6244                if ($lastfile) {
6245                        print "</td></tr>\n";
6246                        if ($matches > 1000) {
6247                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6248                        }
6249                } else {
6250                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
6251                }
6252                close $fd;
6253
6254                print "</table>\n";
6255        }
6256        git_footer_html();
6257}
6258
6259sub git_search_help {
6260        git_header_html();
6261        git_print_page_nav('','', $hash,$hash,$hash);
6262        print <<EOT;
6263<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6264regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6265the pattern entered is recognized as the POSIX extended
6266<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6267insensitive).</p>
6268<dl>
6269<dt><b>commit</b></dt>
6270<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6271EOT
6272        my $have_grep = gitweb_check_feature('grep');
6273        if ($have_grep) {
6274                print <<EOT;
6275<dt><b>grep</b></dt>
6276<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6277    a different one) are searched for the given pattern. On large trees, this search can take
6278a while and put some strain on the server, so please use it with some consideration. Note that
6279due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6280case-sensitive.</dd>
6281EOT
6282        }
6283        print <<EOT;
6284<dt><b>author</b></dt>
6285<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6286<dt><b>committer</b></dt>
6287<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6288EOT
6289        my $have_pickaxe = gitweb_check_feature('pickaxe');
6290        if ($have_pickaxe) {
6291                print <<EOT;
6292<dt><b>pickaxe</b></dt>
6293<dd>All commits that caused the string to appear or disappear from any file (changes that
6294added, removed or "modified" the string) will be listed. This search can take a while and
6295takes a lot of strain on the server, so please use it wisely. Note that since you may be
6296interested even in changes just changing the case as well, this search is case sensitive.</dd>
6297EOT
6298        }
6299        print "</dl>\n";
6300        git_footer_html();
6301}
6302
6303sub git_shortlog {
6304        my $head = git_get_head_hash($project);
6305        if (!defined $hash) {
6306                $hash = $head;
6307        }
6308        if (!defined $page) {
6309                $page = 0;
6310        }
6311        my $refs = git_get_references();
6312
6313        my $commit_hash = $hash;
6314        if (defined $hash_parent) {
6315                $commit_hash = "$hash_parent..$hash";
6316        }
6317        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6318
6319        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6320        my $next_link = '';
6321        if ($#commitlist >= 100) {
6322                $next_link =
6323                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
6324                                 -accesskey => "n", -title => "Alt-n"}, "next");
6325        }
6326        my $patch_max = gitweb_check_feature('patches');
6327        if ($patch_max) {
6328                if ($patch_max < 0 || @commitlist <= $patch_max) {
6329                        $paging_nav .= " &sdot; " .
6330                                $cgi->a({-href => href(action=>"patches", -replay=>1)},
6331                                        "patches");
6332                }
6333        }
6334
6335        git_header_html();
6336        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6337        git_print_header_div('summary', $project);
6338
6339        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6340
6341        git_footer_html();
6342}
6343
6344## ......................................................................
6345## feeds (RSS, Atom; OPML)
6346
6347sub git_feed {
6348        my $format = shift || 'atom';
6349        my $have_blame = gitweb_check_feature('blame');
6350
6351        # Atom: http://www.atomenabled.org/developers/syndication/
6352        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6353        if ($format ne 'rss' && $format ne 'atom') {
6354                die_error(400, "Unknown web feed format");
6355        }
6356
6357        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6358        my $head = $hash || 'HEAD';
6359        my @commitlist = parse_commits($head, 150, 0, $file_name);
6360
6361        my %latest_commit;
6362        my %latest_date;
6363        my $content_type = "application/$format+xml";
6364        if (defined $cgi->http('HTTP_ACCEPT') &&
6365                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6366                # browser (feed reader) prefers text/xml
6367                $content_type = 'text/xml';
6368        }
6369        if (defined($commitlist[0])) {
6370                %latest_commit = %{$commitlist[0]};
6371                my $latest_epoch = $latest_commit{'committer_epoch'};
6372                %latest_date   = parse_date($latest_epoch);
6373                my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6374                if (defined $if_modified) {
6375                        my $since;
6376                        if (eval { require HTTP::Date; 1; }) {
6377                                $since = HTTP::Date::str2time($if_modified);
6378                        } elsif (eval { require Time::ParseDate; 1; }) {
6379                                $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6380                        }
6381                        if (defined $since && $latest_epoch <= $since) {
6382                                print $cgi->header(
6383                                        -type => $content_type,
6384                                        -charset => 'utf-8',
6385                                        -last_modified => $latest_date{'rfc2822'},
6386                                        -status => '304 Not Modified');
6387                                return;
6388                        }
6389                }
6390                print $cgi->header(
6391                        -type => $content_type,
6392                        -charset => 'utf-8',
6393                        -last_modified => $latest_date{'rfc2822'});
6394        } else {
6395                print $cgi->header(
6396                        -type => $content_type,
6397                        -charset => 'utf-8');
6398        }
6399
6400        # Optimization: skip generating the body if client asks only
6401        # for Last-Modified date.
6402        return if ($cgi->request_method() eq 'HEAD');
6403
6404        # header variables
6405        my $title = "$site_name - $project/$action";
6406        my $feed_type = 'log';
6407        if (defined $hash) {
6408                $title .= " - '$hash'";
6409                $feed_type = 'branch log';
6410                if (defined $file_name) {
6411                        $title .= " :: $file_name";
6412                        $feed_type = 'history';
6413                }
6414        } elsif (defined $file_name) {
6415                $title .= " - $file_name";
6416                $feed_type = 'history';
6417        }
6418        $title .= " $feed_type";
6419        my $descr = git_get_project_description($project);
6420        if (defined $descr) {
6421                $descr = esc_html($descr);
6422        } else {
6423                $descr = "$project " .
6424                         ($format eq 'rss' ? 'RSS' : 'Atom') .
6425                         " feed";
6426        }
6427        my $owner = git_get_project_owner($project);
6428        $owner = esc_html($owner);
6429
6430        #header
6431        my $alt_url;
6432        if (defined $file_name) {
6433                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6434        } elsif (defined $hash) {
6435                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6436        } else {
6437                $alt_url = href(-full=>1, action=>"summary");
6438        }
6439        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6440        if ($format eq 'rss') {
6441                print <<XML;
6442<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6443<channel>
6444XML
6445                print "<title>$title</title>\n" .
6446                      "<link>$alt_url</link>\n" .
6447                      "<description>$descr</description>\n" .
6448                      "<language>en</language>\n" .
6449                      # project owner is responsible for 'editorial' content
6450                      "<managingEditor>$owner</managingEditor>\n";
6451                if (defined $logo || defined $favicon) {
6452                        # prefer the logo to the favicon, since RSS
6453                        # doesn't allow both
6454                        my $img = esc_url($logo || $favicon);
6455                        print "<image>\n" .
6456                              "<url>$img</url>\n" .
6457                              "<title>$title</title>\n" .
6458                              "<link>$alt_url</link>\n" .
6459                              "</image>\n";
6460                }
6461                if (%latest_date) {
6462                        print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6463                        print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6464                }
6465                print "<generator>gitweb v.$version/$git_version</generator>\n";
6466        } elsif ($format eq 'atom') {
6467                print <<XML;
6468<feed xmlns="http://www.w3.org/2005/Atom">
6469XML
6470                print "<title>$title</title>\n" .
6471                      "<subtitle>$descr</subtitle>\n" .
6472                      '<link rel="alternate" type="text/html" href="' .
6473                      $alt_url . '" />' . "\n" .
6474                      '<link rel="self" type="' . $content_type . '" href="' .
6475                      $cgi->self_url() . '" />' . "\n" .
6476                      "<id>" . href(-full=>1) . "</id>\n" .
6477                      # use project owner for feed author
6478                      "<author><name>$owner</name></author>\n";
6479                if (defined $favicon) {
6480                        print "<icon>" . esc_url($favicon) . "</icon>\n";
6481                }
6482                if (defined $logo_url) {
6483                        # not twice as wide as tall: 72 x 27 pixels
6484                        print "<logo>" . esc_url($logo) . "</logo>\n";
6485                }
6486                if (! %latest_date) {
6487                        # dummy date to keep the feed valid until commits trickle in:
6488                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
6489                } else {
6490                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
6491                }
6492                print "<generator version='$version/$git_version'>gitweb</generator>\n";
6493        }
6494
6495        # contents
6496        for (my $i = 0; $i <= $#commitlist; $i++) {
6497                my %co = %{$commitlist[$i]};
6498                my $commit = $co{'id'};
6499                # we read 150, we always show 30 and the ones more recent than 48 hours
6500                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6501                        last;
6502                }
6503                my %cd = parse_date($co{'author_epoch'});
6504
6505                # get list of changed files
6506                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6507                        $co{'parent'} || "--root",
6508                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
6509                        or next;
6510                my @difftree = map { chomp; $_ } <$fd>;
6511                close $fd
6512                        or next;
6513
6514                # print element (entry, item)
6515                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6516                if ($format eq 'rss') {
6517                        print "<item>\n" .
6518                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
6519                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
6520                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6521                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6522                              "<link>$co_url</link>\n" .
6523                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
6524                              "<content:encoded>" .
6525                              "<![CDATA[\n";
6526                } elsif ($format eq 'atom') {
6527                        print "<entry>\n" .
6528                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6529                              "<updated>$cd{'iso-8601'}</updated>\n" .
6530                              "<author>\n" .
6531                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6532                        if ($co{'author_email'}) {
6533                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6534                        }
6535                        print "</author>\n" .
6536                              # use committer for contributor
6537                              "<contributor>\n" .
6538                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6539                        if ($co{'committer_email'}) {
6540                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6541                        }
6542                        print "</contributor>\n" .
6543                              "<published>$cd{'iso-8601'}</published>\n" .
6544                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6545                              "<id>$co_url</id>\n" .
6546                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6547                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6548                }
6549                my $comment = $co{'comment'};
6550                print "<pre>\n";
6551                foreach my $line (@$comment) {
6552                        $line = esc_html($line);
6553                        print "$line\n";
6554                }
6555                print "</pre><ul>\n";
6556                foreach my $difftree_line (@difftree) {
6557                        my %difftree = parse_difftree_raw_line($difftree_line);
6558                        next if !$difftree{'from_id'};
6559
6560                        my $file = $difftree{'file'} || $difftree{'to_file'};
6561
6562                        print "<li>" .
6563                              "[" .
6564                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6565                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6566                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6567                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
6568                                      -title => "diff"}, 'D');
6569                        if ($have_blame) {
6570                                print $cgi->a({-href => href(-full=>1, action=>"blame",
6571                                                             file_name=>$file, hash_base=>$commit),
6572                                              -title => "blame"}, 'B');
6573                        }
6574                        # if this is not a feed of a file history
6575                        if (!defined $file_name || $file_name ne $file) {
6576                                print $cgi->a({-href => href(-full=>1, action=>"history",
6577                                                             file_name=>$file, hash=>$commit),
6578                                              -title => "history"}, 'H');
6579                        }
6580                        $file = esc_path($file);
6581                        print "] ".
6582                              "$file</li>\n";
6583                }
6584                if ($format eq 'rss') {
6585                        print "</ul>]]>\n" .
6586                              "</content:encoded>\n" .
6587                              "</item>\n";
6588                } elsif ($format eq 'atom') {
6589                        print "</ul>\n</div>\n" .
6590                              "</content>\n" .
6591                              "</entry>\n";
6592                }
6593        }
6594
6595        # end of feed
6596        if ($format eq 'rss') {
6597                print "</channel>\n</rss>\n";
6598        } elsif ($format eq 'atom') {
6599                print "</feed>\n";
6600        }
6601}
6602
6603sub git_rss {
6604        git_feed('rss');
6605}
6606
6607sub git_atom {
6608        git_feed('atom');
6609}
6610
6611sub git_opml {
6612        my @list = git_get_projects_list();
6613
6614        print $cgi->header(
6615                -type => 'text/xml',
6616                -charset => 'utf-8',
6617                -content_disposition => 'inline; filename="opml.xml"');
6618
6619        print <<XML;
6620<?xml version="1.0" encoding="utf-8"?>
6621<opml version="1.0">
6622<head>
6623  <title>$site_name OPML Export</title>
6624</head>
6625<body>
6626<outline text="git RSS feeds">
6627XML
6628
6629        foreach my $pr (@list) {
6630                my %proj = %$pr;
6631                my $head = git_get_head_hash($proj{'path'});
6632                if (!defined $head) {
6633                        next;
6634                }
6635                $git_dir = "$projectroot/$proj{'path'}";
6636                my %co = parse_commit($head);
6637                if (!%co) {
6638                        next;
6639                }
6640
6641                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6642                my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6643                my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6644                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6645        }
6646        print <<XML;
6647</outline>
6648</body>
6649</opml>
6650XML
6651}