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