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