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