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