99fdb13f1f8c02696a0e1ed46ec8581c7d27af97
   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_export_ok("$projectroot/$filter/$subdir")) {
1893                                        push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1894                                        $File::Find::prune = 1;
1895                                }
1896                        },
1897                }, "$dir");
1898
1899        } elsif (-f $projects_list) {
1900                # read from file(url-encoded):
1901                # 'git%2Fgit.git Linus+Torvalds'
1902                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1903                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1904                my %paths;
1905                open my ($fd), $projects_list or return;
1906        PROJECT:
1907                while (my $line = <$fd>) {
1908                        chomp $line;
1909                        my ($path, $owner) = split ' ', $line;
1910                        $path = unescape($path);
1911                        $owner = unescape($owner);
1912                        if (!defined $path) {
1913                                next;
1914                        }
1915                        if ($filter ne '') {
1916                                # looking for forks;
1917                                my $pfx = substr($path, 0, length($filter));
1918                                if ($pfx ne $filter) {
1919                                        next PROJECT;
1920                                }
1921                                my $sfx = substr($path, length($filter));
1922                                if ($sfx !~ /^\/.*\.git$/) {
1923                                        next PROJECT;
1924                                }
1925                        } elsif ($check_forks) {
1926                        PATH:
1927                                foreach my $filter (keys %paths) {
1928                                        # looking for forks;
1929                                        my $pfx = substr($path, 0, length($filter));
1930                                        if ($pfx ne $filter) {
1931                                                next PATH;
1932                                        }
1933                                        my $sfx = substr($path, length($filter));
1934                                        if ($sfx !~ /^\/.*\.git$/) {
1935                                                next PATH;
1936                                        }
1937                                        # is a fork, don't include it in
1938                                        # the list
1939                                        next PROJECT;
1940                                }
1941                        }
1942                        if (check_export_ok("$projectroot/$path")) {
1943                                my $pr = {
1944                                        path => $path,
1945                                        owner => to_utf8($owner),
1946                                };
1947                                push @list, $pr;
1948                                (my $forks_path = $path) =~ s/\.git$//;
1949                                $paths{$forks_path}++;
1950                        }
1951                }
1952                close $fd;
1953        }
1954        return @list;
1955}
1956
1957our $gitweb_project_owner = undef;
1958sub git_get_project_list_from_file {
1959
1960        return if (defined $gitweb_project_owner);
1961
1962        $gitweb_project_owner = {};
1963        # read from file (url-encoded):
1964        # 'git%2Fgit.git Linus+Torvalds'
1965        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1966        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1967        if (-f $projects_list) {
1968                open (my $fd , $projects_list);
1969                while (my $line = <$fd>) {
1970                        chomp $line;
1971                        my ($pr, $ow) = split ' ', $line;
1972                        $pr = unescape($pr);
1973                        $ow = unescape($ow);
1974                        $gitweb_project_owner->{$pr} = to_utf8($ow);
1975                }
1976                close $fd;
1977        }
1978}
1979
1980sub git_get_project_owner {
1981        my $project = shift;
1982        my $owner;
1983
1984        return undef unless $project;
1985        $git_dir = "$projectroot/$project";
1986
1987        if (!defined $gitweb_project_owner) {
1988                git_get_project_list_from_file();
1989        }
1990
1991        if (exists $gitweb_project_owner->{$project}) {
1992                $owner = $gitweb_project_owner->{$project};
1993        }
1994        if (!defined $owner){
1995                $owner = git_get_project_config('owner');
1996        }
1997        if (!defined $owner) {
1998                $owner = get_file_owner("$git_dir");
1999        }
2000
2001        return $owner;
2002}
2003
2004sub git_get_last_activity {
2005        my ($path) = @_;
2006        my $fd;
2007
2008        $git_dir = "$projectroot/$path";
2009        open($fd, "-|", git_cmd(), 'for-each-ref',
2010             '--format=%(committer)',
2011             '--sort=-committerdate',
2012             '--count=1',
2013             'refs/heads') or return;
2014        my $most_recent = <$fd>;
2015        close $fd or return;
2016        if (defined $most_recent &&
2017            $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2018                my $timestamp = $1;
2019                my $age = time - $timestamp;
2020                return ($age, age_string($age));
2021        }
2022        return (undef, undef);
2023}
2024
2025sub git_get_references {
2026        my $type = shift || "";
2027        my %refs;
2028        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2029        # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2030        open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2031                ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2032                or return;
2033
2034        while (my $line = <$fd>) {
2035                chomp $line;
2036                if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2037                        if (defined $refs{$1}) {
2038                                push @{$refs{$1}}, $2;
2039                        } else {
2040                                $refs{$1} = [ $2 ];
2041                        }
2042                }
2043        }
2044        close $fd or return;
2045        return \%refs;
2046}
2047
2048sub git_get_rev_name_tags {
2049        my $hash = shift || return undef;
2050
2051        open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2052                or return;
2053        my $name_rev = <$fd>;
2054        close $fd;
2055
2056        if ($name_rev =~ m|^$hash tags/(.*)$|) {
2057                return $1;
2058        } else {
2059                # catches also '$hash undefined' output
2060                return undef;
2061        }
2062}
2063
2064## ----------------------------------------------------------------------
2065## parse to hash functions
2066
2067sub parse_date {
2068        my $epoch = shift;
2069        my $tz = shift || "-0000";
2070
2071        my %date;
2072        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2073        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2074        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2075        $date{'hour'} = $hour;
2076        $date{'minute'} = $min;
2077        $date{'mday'} = $mday;
2078        $date{'day'} = $days[$wday];
2079        $date{'month'} = $months[$mon];
2080        $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2081                             $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2082        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2083                             $mday, $months[$mon], $hour ,$min;
2084        $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2085                             1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2086
2087        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2088        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2089        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2090        $date{'hour_local'} = $hour;
2091        $date{'minute_local'} = $min;
2092        $date{'tz_local'} = $tz;
2093        $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2094                                  1900+$year, $mon+1, $mday,
2095                                  $hour, $min, $sec, $tz);
2096        return %date;
2097}
2098
2099sub parse_tag {
2100        my $tag_id = shift;
2101        my %tag;
2102        my @comment;
2103
2104        open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2105        $tag{'id'} = $tag_id;
2106        while (my $line = <$fd>) {
2107                chomp $line;
2108                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2109                        $tag{'object'} = $1;
2110                } elsif ($line =~ m/^type (.+)$/) {
2111                        $tag{'type'} = $1;
2112                } elsif ($line =~ m/^tag (.+)$/) {
2113                        $tag{'name'} = $1;
2114                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2115                        $tag{'author'} = $1;
2116                        $tag{'epoch'} = $2;
2117                        $tag{'tz'} = $3;
2118                } elsif ($line =~ m/--BEGIN/) {
2119                        push @comment, $line;
2120                        last;
2121                } elsif ($line eq "") {
2122                        last;
2123                }
2124        }
2125        push @comment, <$fd>;
2126        $tag{'comment'} = \@comment;
2127        close $fd or return;
2128        if (!defined $tag{'name'}) {
2129                return
2130        };
2131        return %tag
2132}
2133
2134sub parse_commit_text {
2135        my ($commit_text, $withparents) = @_;
2136        my @commit_lines = split '\n', $commit_text;
2137        my %co;
2138
2139        pop @commit_lines; # Remove '\0'
2140
2141        if (! @commit_lines) {
2142                return;
2143        }
2144
2145        my $header = shift @commit_lines;
2146        if ($header !~ m/^[0-9a-fA-F]{40}/) {
2147                return;
2148        }
2149        ($co{'id'}, my @parents) = split ' ', $header;
2150        while (my $line = shift @commit_lines) {
2151                last if $line eq "\n";
2152                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2153                        $co{'tree'} = $1;
2154                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2155                        push @parents, $1;
2156                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2157                        $co{'author'} = $1;
2158                        $co{'author_epoch'} = $2;
2159                        $co{'author_tz'} = $3;
2160                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2161                                $co{'author_name'}  = $1;
2162                                $co{'author_email'} = $2;
2163                        } else {
2164                                $co{'author_name'} = $co{'author'};
2165                        }
2166                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2167                        $co{'committer'} = $1;
2168                        $co{'committer_epoch'} = $2;
2169                        $co{'committer_tz'} = $3;
2170                        $co{'committer_name'} = $co{'committer'};
2171                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2172                                $co{'committer_name'}  = $1;
2173                                $co{'committer_email'} = $2;
2174                        } else {
2175                                $co{'committer_name'} = $co{'committer'};
2176                        }
2177                }
2178        }
2179        if (!defined $co{'tree'}) {
2180                return;
2181        };
2182        $co{'parents'} = \@parents;
2183        $co{'parent'} = $parents[0];
2184
2185        foreach my $title (@commit_lines) {
2186                $title =~ s/^    //;
2187                if ($title ne "") {
2188                        $co{'title'} = chop_str($title, 80, 5);
2189                        # remove leading stuff of merges to make the interesting part visible
2190                        if (length($title) > 50) {
2191                                $title =~ s/^Automatic //;
2192                                $title =~ s/^merge (of|with) /Merge ... /i;
2193                                if (length($title) > 50) {
2194                                        $title =~ s/(http|rsync):\/\///;
2195                                }
2196                                if (length($title) > 50) {
2197                                        $title =~ s/(master|www|rsync)\.//;
2198                                }
2199                                if (length($title) > 50) {
2200                                        $title =~ s/kernel.org:?//;
2201                                }
2202                                if (length($title) > 50) {
2203                                        $title =~ s/\/pub\/scm//;
2204                                }
2205                        }
2206                        $co{'title_short'} = chop_str($title, 50, 5);
2207                        last;
2208                }
2209        }
2210        if (! defined $co{'title'} || $co{'title'} eq "") {
2211                $co{'title'} = $co{'title_short'} = '(no commit message)';
2212        }
2213        # remove added spaces
2214        foreach my $line (@commit_lines) {
2215                $line =~ s/^    //;
2216        }
2217        $co{'comment'} = \@commit_lines;
2218
2219        my $age = time - $co{'committer_epoch'};
2220        $co{'age'} = $age;
2221        $co{'age_string'} = age_string($age);
2222        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2223        if ($age > 60*60*24*7*2) {
2224                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2225                $co{'age_string_age'} = $co{'age_string'};
2226        } else {
2227                $co{'age_string_date'} = $co{'age_string'};
2228                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2229        }
2230        return %co;
2231}
2232
2233sub parse_commit {
2234        my ($commit_id) = @_;
2235        my %co;
2236
2237        local $/ = "\0";
2238
2239        open my $fd, "-|", git_cmd(), "rev-list",
2240                "--parents",
2241                "--header",
2242                "--max-count=1",
2243                $commit_id,
2244                "--",
2245                or die_error(500, "Open git-rev-list failed");
2246        %co = parse_commit_text(<$fd>, 1);
2247        close $fd;
2248
2249        return %co;
2250}
2251
2252sub parse_commits {
2253        my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2254        my @cos;
2255
2256        $maxcount ||= 1;
2257        $skip ||= 0;
2258
2259        local $/ = "\0";
2260
2261        open my $fd, "-|", git_cmd(), "rev-list",
2262                "--header",
2263                @args,
2264                ("--max-count=" . $maxcount),
2265                ("--skip=" . $skip),
2266                @extra_options,
2267                $commit_id,
2268                "--",
2269                ($filename ? ($filename) : ())
2270                or die_error(500, "Open git-rev-list failed");
2271        while (my $line = <$fd>) {
2272                my %co = parse_commit_text($line);
2273                push @cos, \%co;
2274        }
2275        close $fd;
2276
2277        return wantarray ? @cos : \@cos;
2278}
2279
2280# parse line of git-diff-tree "raw" output
2281sub parse_difftree_raw_line {
2282        my $line = shift;
2283        my %res;
2284
2285        # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2286        # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2287        if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2288                $res{'from_mode'} = $1;
2289                $res{'to_mode'} = $2;
2290                $res{'from_id'} = $3;
2291                $res{'to_id'} = $4;
2292                $res{'status'} = $5;
2293                $res{'similarity'} = $6;
2294                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2295                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2296                } else {
2297                        $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2298                }
2299        }
2300        # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2301        # combined diff (for merge commit)
2302        elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2303                $res{'nparents'}  = length($1);
2304                $res{'from_mode'} = [ split(' ', $2) ];
2305                $res{'to_mode'} = pop @{$res{'from_mode'}};
2306                $res{'from_id'} = [ split(' ', $3) ];
2307                $res{'to_id'} = pop @{$res{'from_id'}};
2308                $res{'status'} = [ split('', $4) ];
2309                $res{'to_file'} = unquote($5);
2310        }
2311        # 'c512b523472485aef4fff9e57b229d9d243c967f'
2312        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2313                $res{'commit'} = $1;
2314        }
2315
2316        return wantarray ? %res : \%res;
2317}
2318
2319# wrapper: return parsed line of git-diff-tree "raw" output
2320# (the argument might be raw line, or parsed info)
2321sub parsed_difftree_line {
2322        my $line_or_ref = shift;
2323
2324        if (ref($line_or_ref) eq "HASH") {
2325                # pre-parsed (or generated by hand)
2326                return $line_or_ref;
2327        } else {
2328                return parse_difftree_raw_line($line_or_ref);
2329        }
2330}
2331
2332# parse line of git-ls-tree output
2333sub parse_ls_tree_line ($;%) {
2334        my $line = shift;
2335        my %opts = @_;
2336        my %res;
2337
2338        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2339        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2340
2341        $res{'mode'} = $1;
2342        $res{'type'} = $2;
2343        $res{'hash'} = $3;
2344        if ($opts{'-z'}) {
2345                $res{'name'} = $4;
2346        } else {
2347                $res{'name'} = unquote($4);
2348        }
2349
2350        return wantarray ? %res : \%res;
2351}
2352
2353# generates _two_ hashes, references to which are passed as 2 and 3 argument
2354sub parse_from_to_diffinfo {
2355        my ($diffinfo, $from, $to, @parents) = @_;
2356
2357        if ($diffinfo->{'nparents'}) {
2358                # combined diff
2359                $from->{'file'} = [];
2360                $from->{'href'} = [];
2361                fill_from_file_info($diffinfo, @parents)
2362                        unless exists $diffinfo->{'from_file'};
2363                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2364                        $from->{'file'}[$i] =
2365                                defined $diffinfo->{'from_file'}[$i] ?
2366                                        $diffinfo->{'from_file'}[$i] :
2367                                        $diffinfo->{'to_file'};
2368                        if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2369                                $from->{'href'}[$i] = href(action=>"blob",
2370                                                           hash_base=>$parents[$i],
2371                                                           hash=>$diffinfo->{'from_id'}[$i],
2372                                                           file_name=>$from->{'file'}[$i]);
2373                        } else {
2374                                $from->{'href'}[$i] = undef;
2375                        }
2376                }
2377        } else {
2378                # ordinary (not combined) diff
2379                $from->{'file'} = $diffinfo->{'from_file'};
2380                if ($diffinfo->{'status'} ne "A") { # not new (added) file
2381                        $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2382                                               hash=>$diffinfo->{'from_id'},
2383                                               file_name=>$from->{'file'});
2384                } else {
2385                        delete $from->{'href'};
2386                }
2387        }
2388
2389        $to->{'file'} = $diffinfo->{'to_file'};
2390        if (!is_deleted($diffinfo)) { # file exists in result
2391                $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2392                                     hash=>$diffinfo->{'to_id'},
2393                                     file_name=>$to->{'file'});
2394        } else {
2395                delete $to->{'href'};
2396        }
2397}
2398
2399## ......................................................................
2400## parse to array of hashes functions
2401
2402sub git_get_heads_list {
2403        my $limit = shift;
2404        my @headslist;
2405
2406        open my $fd, '-|', git_cmd(), 'for-each-ref',
2407                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2408                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2409                'refs/heads'
2410                or return;
2411        while (my $line = <$fd>) {
2412                my %ref_item;
2413
2414                chomp $line;
2415                my ($refinfo, $committerinfo) = split(/\0/, $line);
2416                my ($hash, $name, $title) = split(' ', $refinfo, 3);
2417                my ($committer, $epoch, $tz) =
2418                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2419                $ref_item{'fullname'}  = $name;
2420                $name =~ s!^refs/heads/!!;
2421
2422                $ref_item{'name'}  = $name;
2423                $ref_item{'id'}    = $hash;
2424                $ref_item{'title'} = $title || '(no commit message)';
2425                $ref_item{'epoch'} = $epoch;
2426                if ($epoch) {
2427                        $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2428                } else {
2429                        $ref_item{'age'} = "unknown";
2430                }
2431
2432                push @headslist, \%ref_item;
2433        }
2434        close $fd;
2435
2436        return wantarray ? @headslist : \@headslist;
2437}
2438
2439sub git_get_tags_list {
2440        my $limit = shift;
2441        my @tagslist;
2442
2443        open my $fd, '-|', git_cmd(), 'for-each-ref',
2444                ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2445                '--format=%(objectname) %(objecttype) %(refname) '.
2446                '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2447                'refs/tags'
2448                or return;
2449        while (my $line = <$fd>) {
2450                my %ref_item;
2451
2452                chomp $line;
2453                my ($refinfo, $creatorinfo) = split(/\0/, $line);
2454                my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2455                my ($creator, $epoch, $tz) =
2456                        ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2457                $ref_item{'fullname'} = $name;
2458                $name =~ s!^refs/tags/!!;
2459
2460                $ref_item{'type'} = $type;
2461                $ref_item{'id'} = $id;
2462                $ref_item{'name'} = $name;
2463                if ($type eq "tag") {
2464                        $ref_item{'subject'} = $title;
2465                        $ref_item{'reftype'} = $reftype;
2466                        $ref_item{'refid'}   = $refid;
2467                } else {
2468                        $ref_item{'reftype'} = $type;
2469                        $ref_item{'refid'}   = $id;
2470                }
2471
2472                if ($type eq "tag" || $type eq "commit") {
2473                        $ref_item{'epoch'} = $epoch;
2474                        if ($epoch) {
2475                                $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2476                        } else {
2477                                $ref_item{'age'} = "unknown";
2478                        }
2479                }
2480
2481                push @tagslist, \%ref_item;
2482        }
2483        close $fd;
2484
2485        return wantarray ? @tagslist : \@tagslist;
2486}
2487
2488## ----------------------------------------------------------------------
2489## filesystem-related functions
2490
2491sub get_file_owner {
2492        my $path = shift;
2493
2494        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2495        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2496        if (!defined $gcos) {
2497                return undef;
2498        }
2499        my $owner = $gcos;
2500        $owner =~ s/[,;].*$//;
2501        return to_utf8($owner);
2502}
2503
2504## ......................................................................
2505## mimetype related functions
2506
2507sub mimetype_guess_file {
2508        my $filename = shift;
2509        my $mimemap = shift;
2510        -r $mimemap or return undef;
2511
2512        my %mimemap;
2513        open(MIME, $mimemap) or return undef;
2514        while (<MIME>) {
2515                next if m/^#/; # skip comments
2516                my ($mime, $exts) = split(/\t+/);
2517                if (defined $exts) {
2518                        my @exts = split(/\s+/, $exts);
2519                        foreach my $ext (@exts) {
2520                                $mimemap{$ext} = $mime;
2521                        }
2522                }
2523        }
2524        close(MIME);
2525
2526        $filename =~ /\.([^.]*)$/;
2527        return $mimemap{$1};
2528}
2529
2530sub mimetype_guess {
2531        my $filename = shift;
2532        my $mime;
2533        $filename =~ /\./ or return undef;
2534
2535        if ($mimetypes_file) {
2536                my $file = $mimetypes_file;
2537                if ($file !~ m!^/!) { # if it is relative path
2538                        # it is relative to project
2539                        $file = "$projectroot/$project/$file";
2540                }
2541                $mime = mimetype_guess_file($filename, $file);
2542        }
2543        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2544        return $mime;
2545}
2546
2547sub blob_mimetype {
2548        my $fd = shift;
2549        my $filename = shift;
2550
2551        if ($filename) {
2552                my $mime = mimetype_guess($filename);
2553                $mime and return $mime;
2554        }
2555
2556        # just in case
2557        return $default_blob_plain_mimetype unless $fd;
2558
2559        if (-T $fd) {
2560                return 'text/plain';
2561        } elsif (! $filename) {
2562                return 'application/octet-stream';
2563        } elsif ($filename =~ m/\.png$/i) {
2564                return 'image/png';
2565        } elsif ($filename =~ m/\.gif$/i) {
2566                return 'image/gif';
2567        } elsif ($filename =~ m/\.jpe?g$/i) {
2568                return 'image/jpeg';
2569        } else {
2570                return 'application/octet-stream';
2571        }
2572}
2573
2574sub blob_contenttype {
2575        my ($fd, $file_name, $type) = @_;
2576
2577        $type ||= blob_mimetype($fd, $file_name);
2578        if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2579                $type .= "; charset=$default_text_plain_charset";
2580        }
2581
2582        return $type;
2583}
2584
2585## ======================================================================
2586## functions printing HTML: header, footer, error page
2587
2588sub git_header_html {
2589        my $status = shift || "200 OK";
2590        my $expires = shift;
2591
2592        my $title = "$site_name";
2593        if (defined $project) {
2594                $title .= " - " . to_utf8($project);
2595                if (defined $action) {
2596                        $title .= "/$action";
2597                        if (defined $file_name) {
2598                                $title .= " - " . esc_path($file_name);
2599                                if ($action eq "tree" && $file_name !~ m|/$|) {
2600                                        $title .= "/";
2601                                }
2602                        }
2603                }
2604        }
2605        my $content_type;
2606        # require explicit support from the UA if we are to send the page as
2607        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2608        # we have to do this because MSIE sometimes globs '*/*', pretending to
2609        # support xhtml+xml but choking when it gets what it asked for.
2610        if (defined $cgi->http('HTTP_ACCEPT') &&
2611            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2612            $cgi->Accept('application/xhtml+xml') != 0) {
2613                $content_type = 'application/xhtml+xml';
2614        } else {
2615                $content_type = 'text/html';
2616        }
2617        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2618                           -status=> $status, -expires => $expires);
2619        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2620        print <<EOF;
2621<?xml version="1.0" encoding="utf-8"?>
2622<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2623<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2624<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2625<!-- git core binaries version $git_version -->
2626<head>
2627<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2628<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2629<meta name="robots" content="index, nofollow"/>
2630<title>$title</title>
2631EOF
2632# print out each stylesheet that exist
2633        if (defined $stylesheet) {
2634#provides backwards capability for those people who define style sheet in a config file
2635                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2636        } else {
2637                foreach my $stylesheet (@stylesheets) {
2638                        next unless $stylesheet;
2639                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2640                }
2641        }
2642        if (defined $project) {
2643                my %href_params = get_feed_info();
2644                if (!exists $href_params{'-title'}) {
2645                        $href_params{'-title'} = 'log';
2646                }
2647
2648                foreach my $format qw(RSS Atom) {
2649                        my $type = lc($format);
2650                        my %link_attr = (
2651                                '-rel' => 'alternate',
2652                                '-title' => "$project - $href_params{'-title'} - $format feed",
2653                                '-type' => "application/$type+xml"
2654                        );
2655
2656                        $href_params{'action'} = $type;
2657                        $link_attr{'-href'} = href(%href_params);
2658                        print "<link ".
2659                              "rel=\"$link_attr{'-rel'}\" ".
2660                              "title=\"$link_attr{'-title'}\" ".
2661                              "href=\"$link_attr{'-href'}\" ".
2662                              "type=\"$link_attr{'-type'}\" ".
2663                              "/>\n";
2664
2665                        $href_params{'extra_options'} = '--no-merges';
2666                        $link_attr{'-href'} = href(%href_params);
2667                        $link_attr{'-title'} .= ' (no merges)';
2668                        print "<link ".
2669                              "rel=\"$link_attr{'-rel'}\" ".
2670                              "title=\"$link_attr{'-title'}\" ".
2671                              "href=\"$link_attr{'-href'}\" ".
2672                              "type=\"$link_attr{'-type'}\" ".
2673                              "/>\n";
2674                }
2675
2676        } else {
2677                printf('<link rel="alternate" title="%s projects list" '.
2678                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
2679                       $site_name, href(project=>undef, action=>"project_index"));
2680                printf('<link rel="alternate" title="%s projects feeds" '.
2681                       'href="%s" type="text/x-opml" />'."\n",
2682                       $site_name, href(project=>undef, action=>"opml"));
2683        }
2684        if (defined $favicon) {
2685                print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2686        }
2687
2688        print "</head>\n" .
2689              "<body>\n";
2690
2691        if (-f $site_header) {
2692                open (my $fd, $site_header);
2693                print <$fd>;
2694                close $fd;
2695        }
2696
2697        print "<div class=\"page_header\">\n" .
2698              $cgi->a({-href => esc_url($logo_url),
2699                       -title => $logo_label},
2700                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2701        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2702        if (defined $project) {
2703                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2704                if (defined $action) {
2705                        print " / $action";
2706                }
2707                print "\n";
2708        }
2709        print "</div>\n";
2710
2711        my ($have_search) = gitweb_check_feature('search');
2712        if (defined $project && $have_search) {
2713                if (!defined $searchtext) {
2714                        $searchtext = "";
2715                }
2716                my $search_hash;
2717                if (defined $hash_base) {
2718                        $search_hash = $hash_base;
2719                } elsif (defined $hash) {
2720                        $search_hash = $hash;
2721                } else {
2722                        $search_hash = "HEAD";
2723                }
2724                my $action = $my_uri;
2725                my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2726                if ($use_pathinfo) {
2727                        $action .= "/".esc_url($project);
2728                }
2729                print $cgi->startform(-method => "get", -action => $action) .
2730                      "<div class=\"search\">\n" .
2731                      (!$use_pathinfo &&
2732                      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2733                      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2734                      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2735                      $cgi->popup_menu(-name => 'st', -default => 'commit',
2736                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2737                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2738                      " search:\n",
2739                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2740                      "<span title=\"Extended regular expression\">" .
2741                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2742                                     -checked => $search_use_regexp) .
2743                      "</span>" .
2744                      "</div>" .
2745                      $cgi->end_form() . "\n";
2746        }
2747}
2748
2749sub git_footer_html {
2750        my $feed_class = 'rss_logo';
2751
2752        print "<div class=\"page_footer\">\n";
2753        if (defined $project) {
2754                my $descr = git_get_project_description($project);
2755                if (defined $descr) {
2756                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2757                }
2758
2759                my %href_params = get_feed_info();
2760                if (!%href_params) {
2761                        $feed_class .= ' generic';
2762                }
2763                $href_params{'-title'} ||= 'log';
2764
2765                foreach my $format qw(RSS Atom) {
2766                        $href_params{'action'} = lc($format);
2767                        print $cgi->a({-href => href(%href_params),
2768                                      -title => "$href_params{'-title'} $format feed",
2769                                      -class => $feed_class}, $format)."\n";
2770                }
2771
2772        } else {
2773                print $cgi->a({-href => href(project=>undef, action=>"opml"),
2774                              -class => $feed_class}, "OPML") . " ";
2775                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2776                              -class => $feed_class}, "TXT") . "\n";
2777        }
2778        print "</div>\n"; # class="page_footer"
2779
2780        if (-f $site_footer) {
2781                open (my $fd, $site_footer);
2782                print <$fd>;
2783                close $fd;
2784        }
2785
2786        print "</body>\n" .
2787              "</html>";
2788}
2789
2790# die_error(<http_status_code>, <error_message>)
2791# Example: die_error(404, 'Hash not found')
2792# By convention, use the following status codes (as defined in RFC 2616):
2793# 400: Invalid or missing CGI parameters, or
2794#      requested object exists but has wrong type.
2795# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2796#      this server or project.
2797# 404: Requested object/revision/project doesn't exist.
2798# 500: The server isn't configured properly, or
2799#      an internal error occurred (e.g. failed assertions caused by bugs), or
2800#      an unknown error occurred (e.g. the git binary died unexpectedly).
2801sub die_error {
2802        my $status = shift || 500;
2803        my $error = shift || "Internal server error";
2804
2805        my %http_responses = (400 => '400 Bad Request',
2806                              403 => '403 Forbidden',
2807                              404 => '404 Not Found',
2808                              500 => '500 Internal Server Error');
2809        git_header_html($http_responses{$status});
2810        print <<EOF;
2811<div class="page_body">
2812<br /><br />
2813$status - $error
2814<br />
2815</div>
2816EOF
2817        git_footer_html();
2818        exit;
2819}
2820
2821## ----------------------------------------------------------------------
2822## functions printing or outputting HTML: navigation
2823
2824sub git_print_page_nav {
2825        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2826        $extra = '' if !defined $extra; # pager or formats
2827
2828        my @navs = qw(summary shortlog log commit commitdiff tree);
2829        if ($suppress) {
2830                @navs = grep { $_ ne $suppress } @navs;
2831        }
2832
2833        my %arg = map { $_ => {action=>$_} } @navs;
2834        if (defined $head) {
2835                for (qw(commit commitdiff)) {
2836                        $arg{$_}{'hash'} = $head;
2837                }
2838                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2839                        for (qw(shortlog log)) {
2840                                $arg{$_}{'hash'} = $head;
2841                        }
2842                }
2843        }
2844        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2845        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2846
2847        print "<div class=\"page_nav\">\n" .
2848                (join " | ",
2849                 map { $_ eq $current ?
2850                       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2851                 } @navs);
2852        print "<br/>\n$extra<br/>\n" .
2853              "</div>\n";
2854}
2855
2856sub format_paging_nav {
2857        my ($action, $hash, $head, $page, $has_next_link) = @_;
2858        my $paging_nav;
2859
2860
2861        if ($hash ne $head || $page) {
2862                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2863        } else {
2864                $paging_nav .= "HEAD";
2865        }
2866
2867        if ($page > 0) {
2868                $paging_nav .= " &sdot; " .
2869                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
2870                                 -accesskey => "p", -title => "Alt-p"}, "prev");
2871        } else {
2872                $paging_nav .= " &sdot; prev";
2873        }
2874
2875        if ($has_next_link) {
2876                $paging_nav .= " &sdot; " .
2877                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
2878                                 -accesskey => "n", -title => "Alt-n"}, "next");
2879        } else {
2880                $paging_nav .= " &sdot; next";
2881        }
2882
2883        return $paging_nav;
2884}
2885
2886## ......................................................................
2887## functions printing or outputting HTML: div
2888
2889sub git_print_header_div {
2890        my ($action, $title, $hash, $hash_base) = @_;
2891        my %args = ();
2892
2893        $args{'action'} = $action;
2894        $args{'hash'} = $hash if $hash;
2895        $args{'hash_base'} = $hash_base if $hash_base;
2896
2897        print "<div class=\"header\">\n" .
2898              $cgi->a({-href => href(%args), -class => "title"},
2899              $title ? $title : $action) .
2900              "\n</div>\n";
2901}
2902
2903#sub git_print_authorship (\%) {
2904sub git_print_authorship {
2905        my $co = shift;
2906
2907        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2908        print "<div class=\"author_date\">" .
2909              esc_html($co->{'author_name'}) .
2910              " [$ad{'rfc2822'}";
2911        if ($ad{'hour_local'} < 6) {
2912                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2913                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2914        } else {
2915                printf(" (%02d:%02d %s)",
2916                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2917        }
2918        print "]</div>\n";
2919}
2920
2921sub git_print_page_path {
2922        my $name = shift;
2923        my $type = shift;
2924        my $hb = shift;
2925
2926
2927        print "<div class=\"page_path\">";
2928        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2929                      -title => 'tree root'}, to_utf8("[$project]"));
2930        print " / ";
2931        if (defined $name) {
2932                my @dirname = split '/', $name;
2933                my $basename = pop @dirname;
2934                my $fullname = '';
2935
2936                foreach my $dir (@dirname) {
2937                        $fullname .= ($fullname ? '/' : '') . $dir;
2938                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2939                                                     hash_base=>$hb),
2940                                      -title => $fullname}, esc_path($dir));
2941                        print " / ";
2942                }
2943                if (defined $type && $type eq 'blob') {
2944                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2945                                                     hash_base=>$hb),
2946                                      -title => $name}, esc_path($basename));
2947                } elsif (defined $type && $type eq 'tree') {
2948                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2949                                                     hash_base=>$hb),
2950                                      -title => $name}, esc_path($basename));
2951                        print " / ";
2952                } else {
2953                        print esc_path($basename);
2954                }
2955        }
2956        print "<br/></div>\n";
2957}
2958
2959# sub git_print_log (\@;%) {
2960sub git_print_log ($;%) {
2961        my $log = shift;
2962        my %opts = @_;
2963
2964        if ($opts{'-remove_title'}) {
2965                # remove title, i.e. first line of log
2966                shift @$log;
2967        }
2968        # remove leading empty lines
2969        while (defined $log->[0] && $log->[0] eq "") {
2970                shift @$log;
2971        }
2972
2973        # print log
2974        my $signoff = 0;
2975        my $empty = 0;
2976        foreach my $line (@$log) {
2977                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2978                        $signoff = 1;
2979                        $empty = 0;
2980                        if (! $opts{'-remove_signoff'}) {
2981                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2982                                next;
2983                        } else {
2984                                # remove signoff lines
2985                                next;
2986                        }
2987                } else {
2988                        $signoff = 0;
2989                }
2990
2991                # print only one empty line
2992                # do not print empty line after signoff
2993                if ($line eq "") {
2994                        next if ($empty || $signoff);
2995                        $empty = 1;
2996                } else {
2997                        $empty = 0;
2998                }
2999
3000                print format_log_line_html($line) . "<br/>\n";
3001        }
3002
3003        if ($opts{'-final_empty_line'}) {
3004                # end with single empty line
3005                print "<br/>\n" unless $empty;
3006        }
3007}
3008
3009# return link target (what link points to)
3010sub git_get_link_target {
3011        my $hash = shift;
3012        my $link_target;
3013
3014        # read link
3015        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3016                or return;
3017        {
3018                local $/;
3019                $link_target = <$fd>;
3020        }
3021        close $fd
3022                or return;
3023
3024        return $link_target;
3025}
3026
3027# given link target, and the directory (basedir) the link is in,
3028# return target of link relative to top directory (top tree);
3029# return undef if it is not possible (including absolute links).
3030sub normalize_link_target {
3031        my ($link_target, $basedir, $hash_base) = @_;
3032
3033        # we can normalize symlink target only if $hash_base is provided
3034        return unless $hash_base;
3035
3036        # absolute symlinks (beginning with '/') cannot be normalized
3037        return if (substr($link_target, 0, 1) eq '/');
3038
3039        # normalize link target to path from top (root) tree (dir)
3040        my $path;
3041        if ($basedir) {
3042                $path = $basedir . '/' . $link_target;
3043        } else {
3044                # we are in top (root) tree (dir)
3045                $path = $link_target;
3046        }
3047
3048        # remove //, /./, and /../
3049        my @path_parts;
3050        foreach my $part (split('/', $path)) {
3051                # discard '.' and ''
3052                next if (!$part || $part eq '.');
3053                # handle '..'
3054                if ($part eq '..') {
3055                        if (@path_parts) {
3056                                pop @path_parts;
3057                        } else {
3058                                # link leads outside repository (outside top dir)
3059                                return;
3060                        }
3061                } else {
3062                        push @path_parts, $part;
3063                }
3064        }
3065        $path = join('/', @path_parts);
3066
3067        return $path;
3068}
3069
3070# print tree entry (row of git_tree), but without encompassing <tr> element
3071sub git_print_tree_entry {
3072        my ($t, $basedir, $hash_base, $have_blame) = @_;
3073
3074        my %base_key = ();
3075        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3076
3077        # The format of a table row is: mode list link.  Where mode is
3078        # the mode of the entry, list is the name of the entry, an href,
3079        # and link is the action links of the entry.
3080
3081        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3082        if ($t->{'type'} eq "blob") {
3083                print "<td class=\"list\">" .
3084                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3085                                               file_name=>"$basedir$t->{'name'}", %base_key),
3086                                -class => "list"}, esc_path($t->{'name'}));
3087                if (S_ISLNK(oct $t->{'mode'})) {
3088                        my $link_target = git_get_link_target($t->{'hash'});
3089                        if ($link_target) {
3090                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3091                                if (defined $norm_target) {
3092                                        print " -> " .
3093                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3094                                                                     file_name=>$norm_target),
3095                                                       -title => $norm_target}, esc_path($link_target));
3096                                } else {
3097                                        print " -> " . esc_path($link_target);
3098                                }
3099                        }
3100                }
3101                print "</td>\n";
3102                print "<td class=\"link\">";
3103                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3104                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3105                              "blob");
3106                if ($have_blame) {
3107                        print " | " .
3108                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3109                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3110                                      "blame");
3111                }
3112                if (defined $hash_base) {
3113                        print " | " .
3114                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3115                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3116                                      "history");
3117                }
3118                print " | " .
3119                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3120                                               file_name=>"$basedir$t->{'name'}")},
3121                                "raw");
3122                print "</td>\n";
3123
3124        } elsif ($t->{'type'} eq "tree") {
3125                print "<td class=\"list\">";
3126                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3127                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3128                              esc_path($t->{'name'}));
3129                print "</td>\n";
3130                print "<td class=\"link\">";
3131                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3132                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3133                              "tree");
3134                if (defined $hash_base) {
3135                        print " | " .
3136                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3137                                                     file_name=>"$basedir$t->{'name'}")},
3138                                      "history");
3139                }
3140                print "</td>\n";
3141        } else {
3142                # unknown object: we can only present history for it
3143                # (this includes 'commit' object, i.e. submodule support)
3144                print "<td class=\"list\">" .
3145                      esc_path($t->{'name'}) .
3146                      "</td>\n";
3147                print "<td class=\"link\">";
3148                if (defined $hash_base) {
3149                        print $cgi->a({-href => href(action=>"history",
3150                                                     hash_base=>$hash_base,
3151                                                     file_name=>"$basedir$t->{'name'}")},
3152                                      "history");
3153                }
3154                print "</td>\n";
3155        }
3156}
3157
3158## ......................................................................
3159## functions printing large fragments of HTML
3160
3161# get pre-image filenames for merge (combined) diff
3162sub fill_from_file_info {
3163        my ($diff, @parents) = @_;
3164
3165        $diff->{'from_file'} = [ ];
3166        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3167        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3168                if ($diff->{'status'}[$i] eq 'R' ||
3169                    $diff->{'status'}[$i] eq 'C') {
3170                        $diff->{'from_file'}[$i] =
3171                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3172                }
3173        }
3174
3175        return $diff;
3176}
3177
3178# is current raw difftree line of file deletion
3179sub is_deleted {
3180        my $diffinfo = shift;
3181
3182        return $diffinfo->{'to_id'} eq ('0' x 40);
3183}
3184
3185# does patch correspond to [previous] difftree raw line
3186# $diffinfo  - hashref of parsed raw diff format
3187# $patchinfo - hashref of parsed patch diff format
3188#              (the same keys as in $diffinfo)
3189sub is_patch_split {
3190        my ($diffinfo, $patchinfo) = @_;
3191
3192        return defined $diffinfo && defined $patchinfo
3193                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3194}
3195
3196
3197sub git_difftree_body {
3198        my ($difftree, $hash, @parents) = @_;
3199        my ($parent) = $parents[0];
3200        my ($have_blame) = gitweb_check_feature('blame');
3201        print "<div class=\"list_head\">\n";
3202        if ($#{$difftree} > 10) {
3203                print(($#{$difftree} + 1) . " files changed:\n");
3204        }
3205        print "</div>\n";
3206
3207        print "<table class=\"" .
3208              (@parents > 1 ? "combined " : "") .
3209              "diff_tree\">\n";
3210
3211        # header only for combined diff in 'commitdiff' view
3212        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3213        if ($has_header) {
3214                # table header
3215                print "<thead><tr>\n" .
3216                       "<th></th><th></th>\n"; # filename, patchN link
3217                for (my $i = 0; $i < @parents; $i++) {
3218                        my $par = $parents[$i];
3219                        print "<th>" .
3220                              $cgi->a({-href => href(action=>"commitdiff",
3221                                                     hash=>$hash, hash_parent=>$par),
3222                                       -title => 'commitdiff to parent number ' .
3223                                                  ($i+1) . ': ' . substr($par,0,7)},
3224                                      $i+1) .
3225                              "&nbsp;</th>\n";
3226                }
3227                print "</tr></thead>\n<tbody>\n";
3228        }
3229
3230        my $alternate = 1;
3231        my $patchno = 0;
3232        foreach my $line (@{$difftree}) {
3233                my $diff = parsed_difftree_line($line);
3234
3235                if ($alternate) {
3236                        print "<tr class=\"dark\">\n";
3237                } else {
3238                        print "<tr class=\"light\">\n";
3239                }
3240                $alternate ^= 1;
3241
3242                if (exists $diff->{'nparents'}) { # combined diff
3243
3244                        fill_from_file_info($diff, @parents)
3245                                unless exists $diff->{'from_file'};
3246
3247                        if (!is_deleted($diff)) {
3248                                # file exists in the result (child) commit
3249                                print "<td>" .
3250                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3251                                                             file_name=>$diff->{'to_file'},
3252                                                             hash_base=>$hash),
3253                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3254                                      "</td>\n";
3255                        } else {
3256                                print "<td>" .
3257                                      esc_path($diff->{'to_file'}) .
3258                                      "</td>\n";
3259                        }
3260
3261                        if ($action eq 'commitdiff') {
3262                                # link to patch
3263                                $patchno++;
3264                                print "<td class=\"link\">" .
3265                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3266                                      " | " .
3267                                      "</td>\n";
3268                        }
3269
3270                        my $has_history = 0;
3271                        my $not_deleted = 0;
3272                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3273                                my $hash_parent = $parents[$i];
3274                                my $from_hash = $diff->{'from_id'}[$i];
3275                                my $from_path = $diff->{'from_file'}[$i];
3276                                my $status = $diff->{'status'}[$i];
3277
3278                                $has_history ||= ($status ne 'A');
3279                                $not_deleted ||= ($status ne 'D');
3280
3281                                if ($status eq 'A') {
3282                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3283                                } elsif ($status eq 'D') {
3284                                        print "<td class=\"link\">" .
3285                                              $cgi->a({-href => href(action=>"blob",
3286                                                                     hash_base=>$hash,
3287                                                                     hash=>$from_hash,
3288                                                                     file_name=>$from_path)},
3289                                                      "blob" . ($i+1)) .
3290                                              " | </td>\n";
3291                                } else {
3292                                        if ($diff->{'to_id'} eq $from_hash) {
3293                                                print "<td class=\"link nochange\">";
3294                                        } else {
3295                                                print "<td class=\"link\">";
3296                                        }
3297                                        print $cgi->a({-href => href(action=>"blobdiff",
3298                                                                     hash=>$diff->{'to_id'},
3299                                                                     hash_parent=>$from_hash,
3300                                                                     hash_base=>$hash,
3301                                                                     hash_parent_base=>$hash_parent,
3302                                                                     file_name=>$diff->{'to_file'},
3303                                                                     file_parent=>$from_path)},
3304                                                      "diff" . ($i+1)) .
3305                                              " | </td>\n";
3306                                }
3307                        }
3308
3309                        print "<td class=\"link\">";
3310                        if ($not_deleted) {
3311                                print $cgi->a({-href => href(action=>"blob",
3312                                                             hash=>$diff->{'to_id'},
3313                                                             file_name=>$diff->{'to_file'},
3314                                                             hash_base=>$hash)},
3315                                              "blob");
3316                                print " | " if ($has_history);
3317                        }
3318                        if ($has_history) {
3319                                print $cgi->a({-href => href(action=>"history",
3320                                                             file_name=>$diff->{'to_file'},
3321                                                             hash_base=>$hash)},
3322                                              "history");
3323                        }
3324                        print "</td>\n";
3325
3326                        print "</tr>\n";
3327                        next; # instead of 'else' clause, to avoid extra indent
3328                }
3329                # else ordinary diff
3330
3331                my ($to_mode_oct, $to_mode_str, $to_file_type);
3332                my ($from_mode_oct, $from_mode_str, $from_file_type);
3333                if ($diff->{'to_mode'} ne ('0' x 6)) {
3334                        $to_mode_oct = oct $diff->{'to_mode'};
3335                        if (S_ISREG($to_mode_oct)) { # only for regular file
3336                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3337                        }
3338                        $to_file_type = file_type($diff->{'to_mode'});
3339                }
3340                if ($diff->{'from_mode'} ne ('0' x 6)) {
3341                        $from_mode_oct = oct $diff->{'from_mode'};
3342                        if (S_ISREG($to_mode_oct)) { # only for regular file
3343                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3344                        }
3345                        $from_file_type = file_type($diff->{'from_mode'});
3346                }
3347
3348                if ($diff->{'status'} eq "A") { # created
3349                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3350                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3351                        $mode_chng   .= "]</span>";
3352                        print "<td>";
3353                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3354                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3355                                      -class => "list"}, esc_path($diff->{'file'}));
3356                        print "</td>\n";
3357                        print "<td>$mode_chng</td>\n";
3358                        print "<td class=\"link\">";
3359                        if ($action eq 'commitdiff') {
3360                                # link to patch
3361                                $patchno++;
3362                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3363                                print " | ";
3364                        }
3365                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3366                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3367                                      "blob");
3368                        print "</td>\n";
3369
3370                } elsif ($diff->{'status'} eq "D") { # deleted
3371                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3372                        print "<td>";
3373                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3374                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3375                                       -class => "list"}, esc_path($diff->{'file'}));
3376                        print "</td>\n";
3377                        print "<td>$mode_chng</td>\n";
3378                        print "<td class=\"link\">";
3379                        if ($action eq 'commitdiff') {
3380                                # link to patch
3381                                $patchno++;
3382                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3383                                print " | ";
3384                        }
3385                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3386                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3387                                      "blob") . " | ";
3388                        if ($have_blame) {
3389                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3390                                                             file_name=>$diff->{'file'})},
3391                                              "blame") . " | ";
3392                        }
3393                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3394                                                     file_name=>$diff->{'file'})},
3395                                      "history");
3396                        print "</td>\n";
3397
3398                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3399                        my $mode_chnge = "";
3400                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3401                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3402                                if ($from_file_type ne $to_file_type) {
3403                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3404                                }
3405                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3406                                        if ($from_mode_str && $to_mode_str) {
3407                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3408                                        } elsif ($to_mode_str) {
3409                                                $mode_chnge .= " mode: $to_mode_str";
3410                                        }
3411                                }
3412                                $mode_chnge .= "]</span>\n";
3413                        }
3414                        print "<td>";
3415                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3416                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3417                                      -class => "list"}, esc_path($diff->{'file'}));
3418                        print "</td>\n";
3419                        print "<td>$mode_chnge</td>\n";
3420                        print "<td class=\"link\">";
3421                        if ($action eq 'commitdiff') {
3422                                # link to patch
3423                                $patchno++;
3424                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3425                                      " | ";
3426                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3427                                # "commit" view and modified file (not onlu mode changed)
3428                                print $cgi->a({-href => href(action=>"blobdiff",
3429                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3430                                                             hash_base=>$hash, hash_parent_base=>$parent,
3431                                                             file_name=>$diff->{'file'})},
3432                                              "diff") .
3433                                      " | ";
3434                        }
3435                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3436                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3437                                       "blob") . " | ";
3438                        if ($have_blame) {
3439                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3440                                                             file_name=>$diff->{'file'})},
3441                                              "blame") . " | ";
3442                        }
3443                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3444                                                     file_name=>$diff->{'file'})},
3445                                      "history");
3446                        print "</td>\n";
3447
3448                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3449                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3450                        my $nstatus = $status_name{$diff->{'status'}};
3451                        my $mode_chng = "";
3452                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3453                                # mode also for directories, so we cannot use $to_mode_str
3454                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3455                        }
3456                        print "<td>" .
3457                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3458                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3459                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3460                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3461                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3462                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3463                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3464                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3465                              "<td class=\"link\">";
3466                        if ($action eq 'commitdiff') {
3467                                # link to patch
3468                                $patchno++;
3469                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3470                                      " | ";
3471                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3472                                # "commit" view and modified file (not only pure rename or copy)
3473                                print $cgi->a({-href => href(action=>"blobdiff",
3474                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3475                                                             hash_base=>$hash, hash_parent_base=>$parent,
3476                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3477                                              "diff") .
3478                                      " | ";
3479                        }
3480                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3481                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3482                                      "blob") . " | ";
3483                        if ($have_blame) {
3484                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3485                                                             file_name=>$diff->{'to_file'})},
3486                                              "blame") . " | ";
3487                        }
3488                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3489                                                    file_name=>$diff->{'to_file'})},
3490                                      "history");
3491                        print "</td>\n";
3492
3493                } # we should not encounter Unmerged (U) or Unknown (X) status
3494                print "</tr>\n";
3495        }
3496        print "</tbody>" if $has_header;
3497        print "</table>\n";
3498}
3499
3500sub git_patchset_body {
3501        my ($fd, $difftree, $hash, @hash_parents) = @_;
3502        my ($hash_parent) = $hash_parents[0];
3503
3504        my $is_combined = (@hash_parents > 1);
3505        my $patch_idx = 0;
3506        my $patch_number = 0;
3507        my $patch_line;
3508        my $diffinfo;
3509        my $to_name;
3510        my (%from, %to);
3511
3512        print "<div class=\"patchset\">\n";
3513
3514        # skip to first patch
3515        while ($patch_line = <$fd>) {
3516                chomp $patch_line;
3517
3518                last if ($patch_line =~ m/^diff /);
3519        }
3520
3521 PATCH:
3522        while ($patch_line) {
3523
3524                # parse "git diff" header line
3525                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3526                        # $1 is from_name, which we do not use
3527                        $to_name = unquote($2);
3528                        $to_name =~ s!^b/!!;
3529                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3530                        # $1 is 'cc' or 'combined', which we do not use
3531                        $to_name = unquote($2);
3532                } else {
3533                        $to_name = undef;
3534                }
3535
3536                # check if current patch belong to current raw line
3537                # and parse raw git-diff line if needed
3538                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3539                        # this is continuation of a split patch
3540                        print "<div class=\"patch cont\">\n";
3541                } else {
3542                        # advance raw git-diff output if needed
3543                        $patch_idx++ if defined $diffinfo;
3544
3545                        # read and prepare patch information
3546                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3547
3548                        # compact combined diff output can have some patches skipped
3549                        # find which patch (using pathname of result) we are at now;
3550                        if ($is_combined) {
3551                                while ($to_name ne $diffinfo->{'to_file'}) {
3552                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3553                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3554                                              "</div>\n";  # class="patch"
3555
3556                                        $patch_idx++;
3557                                        $patch_number++;
3558
3559                                        last if $patch_idx > $#$difftree;
3560                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3561                                }
3562                        }
3563
3564                        # modifies %from, %to hashes
3565                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3566
3567                        # this is first patch for raw difftree line with $patch_idx index
3568                        # we index @$difftree array from 0, but number patches from 1
3569                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3570                }
3571
3572                # git diff header
3573                #assert($patch_line =~ m/^diff /) if DEBUG;
3574                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3575                $patch_number++;
3576                # print "git diff" header
3577                print format_git_diff_header_line($patch_line, $diffinfo,
3578                                                  \%from, \%to);
3579
3580                # print extended diff header
3581                print "<div class=\"diff extended_header\">\n";
3582        EXTENDED_HEADER:
3583                while ($patch_line = <$fd>) {
3584                        chomp $patch_line;
3585
3586                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3587
3588                        print format_extended_diff_header_line($patch_line, $diffinfo,
3589                                                               \%from, \%to);
3590                }
3591                print "</div>\n"; # class="diff extended_header"
3592
3593                # from-file/to-file diff header
3594                if (! $patch_line) {
3595                        print "</div>\n"; # class="patch"
3596                        last PATCH;
3597                }
3598                next PATCH if ($patch_line =~ m/^diff /);
3599                #assert($patch_line =~ m/^---/) if DEBUG;
3600
3601                my $last_patch_line = $patch_line;
3602                $patch_line = <$fd>;
3603                chomp $patch_line;
3604                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3605
3606                print format_diff_from_to_header($last_patch_line, $patch_line,
3607                                                 $diffinfo, \%from, \%to,
3608                                                 @hash_parents);
3609
3610                # the patch itself
3611        LINE:
3612                while ($patch_line = <$fd>) {
3613                        chomp $patch_line;
3614
3615                        next PATCH if ($patch_line =~ m/^diff /);
3616
3617                        print format_diff_line($patch_line, \%from, \%to);
3618                }
3619
3620        } continue {
3621                print "</div>\n"; # class="patch"
3622        }
3623
3624        # for compact combined (--cc) format, with chunk and patch simpliciaction
3625        # patchset might be empty, but there might be unprocessed raw lines
3626        for (++$patch_idx if $patch_number > 0;
3627             $patch_idx < @$difftree;
3628             ++$patch_idx) {
3629                # read and prepare patch information
3630                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3631
3632                # generate anchor for "patch" links in difftree / whatchanged part
3633                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3634                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3635                      "</div>\n";  # class="patch"
3636
3637                $patch_number++;
3638        }
3639
3640        if ($patch_number == 0) {
3641                if (@hash_parents > 1) {
3642                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3643                } else {
3644                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3645                }
3646        }
3647
3648        print "</div>\n"; # class="patchset"
3649}
3650
3651# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3652
3653# fills project list info (age, description, owner, forks) for each
3654# project in the list, removing invalid projects from returned list
3655# NOTE: modifies $projlist, but does not remove entries from it
3656sub fill_project_list_info {
3657        my ($projlist, $check_forks) = @_;
3658        my @projects;
3659
3660        my $show_ctags = gitweb_check_feature('ctags');
3661 PROJECT:
3662        foreach my $pr (@$projlist) {
3663                my (@activity) = git_get_last_activity($pr->{'path'});
3664                unless (@activity) {
3665                        next PROJECT;
3666                }
3667                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3668                if (!defined $pr->{'descr'}) {
3669                        my $descr = git_get_project_description($pr->{'path'}) || "";
3670                        $descr = to_utf8($descr);
3671                        $pr->{'descr_long'} = $descr;
3672                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3673                }
3674                if (!defined $pr->{'owner'}) {
3675                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3676                }
3677                if ($check_forks) {
3678                        my $pname = $pr->{'path'};
3679                        if (($pname =~ s/\.git$//) &&
3680                            ($pname !~ /\/$/) &&
3681                            (-d "$projectroot/$pname")) {
3682                                $pr->{'forks'} = "-d $projectroot/$pname";
3683                        }       else {
3684                                $pr->{'forks'} = 0;
3685                        }
3686                }
3687                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3688                push @projects, $pr;
3689        }
3690
3691        return @projects;
3692}
3693
3694# print 'sort by' <th> element, either sorting by $key if $name eq $order
3695# (changing $list), or generating 'sort by $name' replay link otherwise
3696sub print_sort_th {
3697        my ($str_sort, $name, $order, $key, $header, $list) = @_;
3698        $key    ||= $name;
3699        $header ||= ucfirst($name);
3700
3701        if ($order eq $name) {
3702                if ($str_sort) {
3703                        @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3704                } else {
3705                        @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3706                }
3707                print "<th>$header</th>\n";
3708        } else {
3709                print "<th>" .
3710                      $cgi->a({-href => href(-replay=>1, order=>$name),
3711                               -class => "header"}, $header) .
3712                      "</th>\n";
3713        }
3714}
3715
3716sub print_sort_th_str {
3717        print_sort_th(1, @_);
3718}
3719
3720sub print_sort_th_num {
3721        print_sort_th(0, @_);
3722}
3723
3724sub git_project_list_body {
3725        # actually uses global variable $project
3726        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3727
3728        my ($check_forks) = gitweb_check_feature('forks');
3729        my @projects = fill_project_list_info($projlist, $check_forks);
3730
3731        $order ||= $default_projects_order;
3732        $from = 0 unless defined $from;
3733        $to = $#projects if (!defined $to || $#projects < $to);
3734
3735        my $show_ctags = gitweb_check_feature('ctags');
3736        if ($show_ctags) {
3737                my %ctags;
3738                foreach my $p (@projects) {
3739                        foreach my $ct (keys %{$p->{'ctags'}}) {
3740                                $ctags{$ct} += $p->{'ctags'}->{$ct};
3741                        }
3742                }
3743                my $cloud = git_populate_project_tagcloud(\%ctags);
3744                print git_show_project_tagcloud($cloud, 64);
3745        }
3746
3747        print "<table class=\"project_list\">\n";
3748        unless ($no_header) {
3749                print "<tr>\n";
3750                if ($check_forks) {
3751                        print "<th></th>\n";
3752                }
3753                print_sort_th_str('project', $order, 'path',
3754                                  'Project', \@projects);
3755                print_sort_th_str('descr', $order, 'descr_long',
3756                                  'Description', \@projects);
3757                print_sort_th_str('owner', $order, 'owner',
3758                                  'Owner', \@projects);
3759                print_sort_th_num('age', $order, 'age',
3760                                  'Last Change', \@projects);
3761                print "<th></th>\n" . # for links
3762                      "</tr>\n";
3763        }
3764        my $alternate = 1;
3765        my $tagfilter = $cgi->param('by_tag');
3766        for (my $i = $from; $i <= $to; $i++) {
3767                my $pr = $projects[$i];
3768
3769                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3770                # Weed out forks
3771                if ($check_forks) {
3772                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3773                        $forkbase="^$forkbase" if $forkbase;
3774                        next if not $tagfilter and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3775                }
3776
3777                if ($alternate) {
3778                        print "<tr class=\"dark\">\n";
3779                } else {
3780                        print "<tr class=\"light\">\n";
3781                }
3782                $alternate ^= 1;
3783                if ($check_forks) {
3784                        print "<td>";
3785                        if ($pr->{'forks'}) {
3786                                print "<!-- $pr->{'forks'} -->\n";
3787                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3788                        }
3789                        print "</td>\n";
3790                }
3791                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3792                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3793                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3794                                        -class => "list", -title => $pr->{'descr_long'}},
3795                                        esc_html($pr->{'descr'})) . "</td>\n" .
3796                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3797                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3798                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3799                      "<td class=\"link\">" .
3800                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3801                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3802                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3803                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3804                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3805                      "</td>\n" .
3806                      "</tr>\n";
3807        }
3808        if (defined $extra) {
3809                print "<tr>\n";
3810                if ($check_forks) {
3811                        print "<td></td>\n";
3812                }
3813                print "<td colspan=\"5\">$extra</td>\n" .
3814                      "</tr>\n";
3815        }
3816        print "</table>\n";
3817}
3818
3819sub git_shortlog_body {
3820        # uses global variable $project
3821        my ($commitlist, $from, $to, $refs, $extra) = @_;
3822
3823        $from = 0 unless defined $from;
3824        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3825
3826        print "<table class=\"shortlog\">\n";
3827        my $alternate = 1;
3828        for (my $i = $from; $i <= $to; $i++) {
3829                my %co = %{$commitlist->[$i]};
3830                my $commit = $co{'id'};
3831                my $ref = format_ref_marker($refs, $commit);
3832                if ($alternate) {
3833                        print "<tr class=\"dark\">\n";
3834                } else {
3835                        print "<tr class=\"light\">\n";
3836                }
3837                $alternate ^= 1;
3838                my $author = chop_and_escape_str($co{'author_name'}, 10);
3839                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3840                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3841                      "<td><i>" . $author . "</i></td>\n" .
3842                      "<td>";
3843                print format_subject_html($co{'title'}, $co{'title_short'},
3844                                          href(action=>"commit", hash=>$commit), $ref);
3845                print "</td>\n" .
3846                      "<td class=\"link\">" .
3847                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3848                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3849                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3850                my $snapshot_links = format_snapshot_links($commit);
3851                if (defined $snapshot_links) {
3852                        print " | " . $snapshot_links;
3853                }
3854                print "</td>\n" .
3855                      "</tr>\n";
3856        }
3857        if (defined $extra) {
3858                print "<tr>\n" .
3859                      "<td colspan=\"4\">$extra</td>\n" .
3860                      "</tr>\n";
3861        }
3862        print "</table>\n";
3863}
3864
3865sub git_history_body {
3866        # Warning: assumes constant type (blob or tree) during history
3867        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3868
3869        $from = 0 unless defined $from;
3870        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3871
3872        print "<table class=\"history\">\n";
3873        my $alternate = 1;
3874        for (my $i = $from; $i <= $to; $i++) {
3875                my %co = %{$commitlist->[$i]};
3876                if (!%co) {
3877                        next;
3878                }
3879                my $commit = $co{'id'};
3880
3881                my $ref = format_ref_marker($refs, $commit);
3882
3883                if ($alternate) {
3884                        print "<tr class=\"dark\">\n";
3885                } else {
3886                        print "<tr class=\"light\">\n";
3887                }
3888                $alternate ^= 1;
3889        # shortlog uses      chop_str($co{'author_name'}, 10)
3890                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3891                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3892                      "<td><i>" . $author . "</i></td>\n" .
3893                      "<td>";
3894                # originally git_history used chop_str($co{'title'}, 50)
3895                print format_subject_html($co{'title'}, $co{'title_short'},
3896                                          href(action=>"commit", hash=>$commit), $ref);
3897                print "</td>\n" .
3898                      "<td class=\"link\">" .
3899                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3900                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3901
3902                if ($ftype eq 'blob') {
3903                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3904                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3905                        if (defined $blob_current && defined $blob_parent &&
3906                                        $blob_current ne $blob_parent) {
3907                                print " | " .
3908                                        $cgi->a({-href => href(action=>"blobdiff",
3909                                                               hash=>$blob_current, hash_parent=>$blob_parent,
3910                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
3911                                                               file_name=>$file_name)},
3912                                                "diff to current");
3913                        }
3914                }
3915                print "</td>\n" .
3916                      "</tr>\n";
3917        }
3918        if (defined $extra) {
3919                print "<tr>\n" .
3920                      "<td colspan=\"4\">$extra</td>\n" .
3921                      "</tr>\n";
3922        }
3923        print "</table>\n";
3924}
3925
3926sub git_tags_body {
3927        # uses global variable $project
3928        my ($taglist, $from, $to, $extra) = @_;
3929        $from = 0 unless defined $from;
3930        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3931
3932        print "<table class=\"tags\">\n";
3933        my $alternate = 1;
3934        for (my $i = $from; $i <= $to; $i++) {
3935                my $entry = $taglist->[$i];
3936                my %tag = %$entry;
3937                my $comment = $tag{'subject'};
3938                my $comment_short;
3939                if (defined $comment) {
3940                        $comment_short = chop_str($comment, 30, 5);
3941                }
3942                if ($alternate) {
3943                        print "<tr class=\"dark\">\n";
3944                } else {
3945                        print "<tr class=\"light\">\n";
3946                }
3947                $alternate ^= 1;
3948                if (defined $tag{'age'}) {
3949                        print "<td><i>$tag{'age'}</i></td>\n";
3950                } else {
3951                        print "<td></td>\n";
3952                }
3953                print "<td>" .
3954                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3955                               -class => "list name"}, esc_html($tag{'name'})) .
3956                      "</td>\n" .
3957                      "<td>";
3958                if (defined $comment) {
3959                        print format_subject_html($comment, $comment_short,
3960                                                  href(action=>"tag", hash=>$tag{'id'}));
3961                }
3962                print "</td>\n" .
3963                      "<td class=\"selflink\">";
3964                if ($tag{'type'} eq "tag") {
3965                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3966                } else {
3967                        print "&nbsp;";
3968                }
3969                print "</td>\n" .
3970                      "<td class=\"link\">" . " | " .
3971                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3972                if ($tag{'reftype'} eq "commit") {
3973                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3974                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3975                } elsif ($tag{'reftype'} eq "blob") {
3976                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3977                }
3978                print "</td>\n" .
3979                      "</tr>";
3980        }
3981        if (defined $extra) {
3982                print "<tr>\n" .
3983                      "<td colspan=\"5\">$extra</td>\n" .
3984                      "</tr>\n";
3985        }
3986        print "</table>\n";
3987}
3988
3989sub git_heads_body {
3990        # uses global variable $project
3991        my ($headlist, $head, $from, $to, $extra) = @_;
3992        $from = 0 unless defined $from;
3993        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3994
3995        print "<table class=\"heads\">\n";
3996        my $alternate = 1;
3997        for (my $i = $from; $i <= $to; $i++) {
3998                my $entry = $headlist->[$i];
3999                my %ref = %$entry;
4000                my $curr = $ref{'id'} eq $head;
4001                if ($alternate) {
4002                        print "<tr class=\"dark\">\n";
4003                } else {
4004                        print "<tr class=\"light\">\n";
4005                }
4006                $alternate ^= 1;
4007                print "<td><i>$ref{'age'}</i></td>\n" .
4008                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4009                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4010                               -class => "list name"},esc_html($ref{'name'})) .
4011                      "</td>\n" .
4012                      "<td class=\"link\">" .
4013                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4014                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4015                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4016                      "</td>\n" .
4017                      "</tr>";
4018        }
4019        if (defined $extra) {
4020                print "<tr>\n" .
4021                      "<td colspan=\"3\">$extra</td>\n" .
4022                      "</tr>\n";
4023        }
4024        print "</table>\n";
4025}
4026
4027sub git_search_grep_body {
4028        my ($commitlist, $from, $to, $extra) = @_;
4029        $from = 0 unless defined $from;
4030        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4031
4032        print "<table class=\"commit_search\">\n";
4033        my $alternate = 1;
4034        for (my $i = $from; $i <= $to; $i++) {
4035                my %co = %{$commitlist->[$i]};
4036                if (!%co) {
4037                        next;
4038                }
4039                my $commit = $co{'id'};
4040                if ($alternate) {
4041                        print "<tr class=\"dark\">\n";
4042                } else {
4043                        print "<tr class=\"light\">\n";
4044                }
4045                $alternate ^= 1;
4046                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4047                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4048                      "<td><i>" . $author . "</i></td>\n" .
4049                      "<td>" .
4050                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4051                               -class => "list subject"},
4052                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4053                my $comment = $co{'comment'};
4054                foreach my $line (@$comment) {
4055                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4056                                my ($lead, $match, $trail) = ($1, $2, $3);
4057                                $match = chop_str($match, 70, 5, 'center');
4058                                my $contextlen = int((80 - length($match))/2);
4059                                $contextlen = 30 if ($contextlen > 30);
4060                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4061                                $trail = chop_str($trail, $contextlen, 10, 'right');
4062
4063                                $lead  = esc_html($lead);
4064                                $match = esc_html($match);
4065                                $trail = esc_html($trail);
4066
4067                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4068                        }
4069                }
4070                print "</td>\n" .
4071                      "<td class=\"link\">" .
4072                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4073                      " | " .
4074                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4075                      " | " .
4076                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4077                print "</td>\n" .
4078                      "</tr>\n";
4079        }
4080        if (defined $extra) {
4081                print "<tr>\n" .
4082                      "<td colspan=\"3\">$extra</td>\n" .
4083                      "</tr>\n";
4084        }
4085        print "</table>\n";
4086}
4087
4088## ======================================================================
4089## ======================================================================
4090## actions
4091
4092sub git_project_list {
4093        my $order = $cgi->param('o');
4094        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4095                die_error(400, "Unknown order parameter");
4096        }
4097
4098        my @list = git_get_projects_list();
4099        if (!@list) {
4100                die_error(404, "No projects found");
4101        }
4102
4103        git_header_html();
4104        if (-f $home_text) {
4105                print "<div class=\"index_include\">\n";
4106                open (my $fd, $home_text);
4107                print <$fd>;
4108                close $fd;
4109                print "</div>\n";
4110        }
4111        git_project_list_body(\@list, $order);
4112        git_footer_html();
4113}
4114
4115sub git_forks {
4116        my $order = $cgi->param('o');
4117        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4118                die_error(400, "Unknown order parameter");
4119        }
4120
4121        my @list = git_get_projects_list($project);
4122        if (!@list) {
4123                die_error(404, "No forks found");
4124        }
4125
4126        git_header_html();
4127        git_print_page_nav('','');
4128        git_print_header_div('summary', "$project forks");
4129        git_project_list_body(\@list, $order);
4130        git_footer_html();
4131}
4132
4133sub git_project_index {
4134        my @projects = git_get_projects_list($project);
4135
4136        print $cgi->header(
4137                -type => 'text/plain',
4138                -charset => 'utf-8',
4139                -content_disposition => 'inline; filename="index.aux"');
4140
4141        foreach my $pr (@projects) {
4142                if (!exists $pr->{'owner'}) {
4143                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4144                }
4145
4146                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4147                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4148                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4149                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4150                $path  =~ s/ /\+/g;
4151                $owner =~ s/ /\+/g;
4152
4153                print "$path $owner\n";
4154        }
4155}
4156
4157sub git_summary {
4158        my $descr = git_get_project_description($project) || "none";
4159        my %co = parse_commit("HEAD");
4160        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4161        my $head = $co{'id'};
4162
4163        my $owner = git_get_project_owner($project);
4164
4165        my $refs = git_get_references();
4166        # These get_*_list functions return one more to allow us to see if
4167        # there are more ...
4168        my @taglist  = git_get_tags_list(16);
4169        my @headlist = git_get_heads_list(16);
4170        my @forklist;
4171        my ($check_forks) = gitweb_check_feature('forks');
4172
4173        if ($check_forks) {
4174                @forklist = git_get_projects_list($project);
4175        }
4176
4177        git_header_html();
4178        git_print_page_nav('summary','', $head);
4179
4180        print "<div class=\"title\">&nbsp;</div>\n";
4181        print "<table class=\"projects_list\">\n" .
4182              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4183              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4184        if (defined $cd{'rfc2822'}) {
4185                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4186        }
4187
4188        # use per project git URL list in $projectroot/$project/cloneurl
4189        # or make project git URL from git base URL and project name
4190        my $url_tag = "URL";
4191        my @url_list = git_get_project_url_list($project);
4192        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4193        foreach my $git_url (@url_list) {
4194                next unless $git_url;
4195                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4196                $url_tag = "";
4197        }
4198
4199        # Tag cloud
4200        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4201        if ($show_ctags) {
4202                my $ctags = git_get_project_ctags($project);
4203                my $cloud = git_populate_project_tagcloud($ctags);
4204                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4205                print "</td>\n<td>" unless %$ctags;
4206                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4207                print "</td>\n<td>" if %$ctags;
4208                print git_show_project_tagcloud($cloud, 48);
4209                print "</td></tr>";
4210        }
4211
4212        print "</table>\n";
4213
4214        if (-s "$projectroot/$project/README.html") {
4215                if (open my $fd, "$projectroot/$project/README.html") {
4216                        print "<div class=\"title\">readme</div>\n" .
4217                              "<div class=\"readme\">\n";
4218                        print $_ while (<$fd>);
4219                        print "\n</div>\n"; # class="readme"
4220                        close $fd;
4221                }
4222        }
4223
4224        # we need to request one more than 16 (0..15) to check if
4225        # those 16 are all
4226        my @commitlist = $head ? parse_commits($head, 17) : ();
4227        if (@commitlist) {
4228                git_print_header_div('shortlog');
4229                git_shortlog_body(\@commitlist, 0, 15, $refs,
4230                                  $#commitlist <=  15 ? undef :
4231                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4232        }
4233
4234        if (@taglist) {
4235                git_print_header_div('tags');
4236                git_tags_body(\@taglist, 0, 15,
4237                              $#taglist <=  15 ? undef :
4238                              $cgi->a({-href => href(action=>"tags")}, "..."));
4239        }
4240
4241        if (@headlist) {
4242                git_print_header_div('heads');
4243                git_heads_body(\@headlist, $head, 0, 15,
4244                               $#headlist <= 15 ? undef :
4245                               $cgi->a({-href => href(action=>"heads")}, "..."));
4246        }
4247
4248        if (@forklist) {
4249                git_print_header_div('forks');
4250                git_project_list_body(\@forklist, undef, 0, 15,
4251                                      $#forklist <= 15 ? undef :
4252                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4253                                      'noheader');
4254        }
4255
4256        git_footer_html();
4257}
4258
4259sub git_tag {
4260        my $head = git_get_head_hash($project);
4261        git_header_html();
4262        git_print_page_nav('','', $head,undef,$head);
4263        my %tag = parse_tag($hash);
4264
4265        if (! %tag) {
4266                die_error(404, "Unknown tag object");
4267        }
4268
4269        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4270        print "<div class=\"title_text\">\n" .
4271              "<table class=\"object_header\">\n" .
4272              "<tr>\n" .
4273              "<td>object</td>\n" .
4274              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4275                               $tag{'object'}) . "</td>\n" .
4276              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4277                                              $tag{'type'}) . "</td>\n" .
4278              "</tr>\n";
4279        if (defined($tag{'author'})) {
4280                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4281                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4282                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4283                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4284                        "</td></tr>\n";
4285        }
4286        print "</table>\n\n" .
4287              "</div>\n";
4288        print "<div class=\"page_body\">";
4289        my $comment = $tag{'comment'};
4290        foreach my $line (@$comment) {
4291                chomp $line;
4292                print esc_html($line, -nbsp=>1) . "<br/>\n";
4293        }
4294        print "</div>\n";
4295        git_footer_html();
4296}
4297
4298sub git_blame {
4299        my $fd;
4300        my $ftype;
4301
4302        gitweb_check_feature('blame')
4303            or die_error(403, "Blame view not allowed");
4304
4305        die_error(400, "No file name given") unless $file_name;
4306        $hash_base ||= git_get_head_hash($project);
4307        die_error(404, "Couldn't find base commit") unless ($hash_base);
4308        my %co = parse_commit($hash_base)
4309                or die_error(404, "Commit not found");
4310        if (!defined $hash) {
4311                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4312                        or die_error(404, "Error looking up file");
4313        }
4314        $ftype = git_get_type($hash);
4315        if ($ftype !~ "blob") {
4316                die_error(400, "Object is not a blob");
4317        }
4318        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4319              $file_name, $hash_base)
4320                or die_error(500, "Open git-blame failed");
4321        git_header_html();
4322        my $formats_nav =
4323                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4324                        "blob") .
4325                " | " .
4326                $cgi->a({-href => href(action=>"history", -replay=>1)},
4327                        "history") .
4328                " | " .
4329                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4330                        "HEAD");
4331        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4332        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4333        git_print_page_path($file_name, $ftype, $hash_base);
4334        my @rev_color = (qw(light2 dark2));
4335        my $num_colors = scalar(@rev_color);
4336        my $current_color = 0;
4337        my $last_rev;
4338        print <<HTML;
4339<div class="page_body">
4340<table class="blame">
4341<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4342HTML
4343        my %metainfo = ();
4344        while (1) {
4345                $_ = <$fd>;
4346                last unless defined $_;
4347                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4348                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4349                if (!exists $metainfo{$full_rev}) {
4350                        $metainfo{$full_rev} = {};
4351                }
4352                my $meta = $metainfo{$full_rev};
4353                while (<$fd>) {
4354                        last if (s/^\t//);
4355                        if (/^(\S+) (.*)$/) {
4356                                $meta->{$1} = $2;
4357                        }
4358                }
4359                my $data = $_;
4360                chomp $data;
4361                my $rev = substr($full_rev, 0, 8);
4362                my $author = $meta->{'author'};
4363                my %date = parse_date($meta->{'author-time'},
4364                                      $meta->{'author-tz'});
4365                my $date = $date{'iso-tz'};
4366                if ($group_size) {
4367                        $current_color = ++$current_color % $num_colors;
4368                }
4369                print "<tr class=\"$rev_color[$current_color]\">\n";
4370                if ($group_size) {
4371                        print "<td class=\"sha1\"";
4372                        print " title=\"". esc_html($author) . ", $date\"";
4373                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4374                        print ">";
4375                        print $cgi->a({-href => href(action=>"commit",
4376                                                     hash=>$full_rev,
4377                                                     file_name=>$file_name)},
4378                                      esc_html($rev));
4379                        print "</td>\n";
4380                }
4381                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4382                        or die_error(500, "Open git-rev-parse failed");
4383                my $parent_commit = <$dd>;
4384                close $dd;
4385                chomp($parent_commit);
4386                my $blamed = href(action => 'blame',
4387                                  file_name => $meta->{'filename'},
4388                                  hash_base => $parent_commit);
4389                print "<td class=\"linenr\">";
4390                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4391                                -id => "l$lineno",
4392                                -class => "linenr" },
4393                              esc_html($lineno));
4394                print "</td>";
4395                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4396                print "</tr>\n";
4397        }
4398        print "</table>\n";
4399        print "</div>";
4400        close $fd
4401                or print "Reading blob failed\n";
4402        git_footer_html();
4403}
4404
4405sub git_tags {
4406        my $head = git_get_head_hash($project);
4407        git_header_html();
4408        git_print_page_nav('','', $head,undef,$head);
4409        git_print_header_div('summary', $project);
4410
4411        my @tagslist = git_get_tags_list();
4412        if (@tagslist) {
4413                git_tags_body(\@tagslist);
4414        }
4415        git_footer_html();
4416}
4417
4418sub git_heads {
4419        my $head = git_get_head_hash($project);
4420        git_header_html();
4421        git_print_page_nav('','', $head,undef,$head);
4422        git_print_header_div('summary', $project);
4423
4424        my @headslist = git_get_heads_list();
4425        if (@headslist) {
4426                git_heads_body(\@headslist, $head);
4427        }
4428        git_footer_html();
4429}
4430
4431sub git_blob_plain {
4432        my $type = shift;
4433        my $expires;
4434
4435        if (!defined $hash) {
4436                if (defined $file_name) {
4437                        my $base = $hash_base || git_get_head_hash($project);
4438                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4439                                or die_error(404, "Cannot find file");
4440                } else {
4441                        die_error(400, "No file name defined");
4442                }
4443        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4444                # blobs defined by non-textual hash id's can be cached
4445                $expires = "+1d";
4446        }
4447
4448        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4449                or die_error(500, "Open git-cat-file blob '$hash' failed");
4450
4451        # content-type (can include charset)
4452        $type = blob_contenttype($fd, $file_name, $type);
4453
4454        # "save as" filename, even when no $file_name is given
4455        my $save_as = "$hash";
4456        if (defined $file_name) {
4457                $save_as = $file_name;
4458        } elsif ($type =~ m/^text\//) {
4459                $save_as .= '.txt';
4460        }
4461
4462        print $cgi->header(
4463                -type => $type,
4464                -expires => $expires,
4465                -content_disposition => 'inline; filename="' . $save_as . '"');
4466        undef $/;
4467        binmode STDOUT, ':raw';
4468        print <$fd>;
4469        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4470        $/ = "\n";
4471        close $fd;
4472}
4473
4474sub git_blob {
4475        my $expires;
4476
4477        if (!defined $hash) {
4478                if (defined $file_name) {
4479                        my $base = $hash_base || git_get_head_hash($project);
4480                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4481                                or die_error(404, "Cannot find file");
4482                } else {
4483                        die_error(400, "No file name defined");
4484                }
4485        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4486                # blobs defined by non-textual hash id's can be cached
4487                $expires = "+1d";
4488        }
4489
4490        my ($have_blame) = gitweb_check_feature('blame');
4491        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4492                or die_error(500, "Couldn't cat $file_name, $hash");
4493        my $mimetype = blob_mimetype($fd, $file_name);
4494        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4495                close $fd;
4496                return git_blob_plain($mimetype);
4497        }
4498        # we can have blame only for text/* mimetype
4499        $have_blame &&= ($mimetype =~ m!^text/!);
4500
4501        git_header_html(undef, $expires);
4502        my $formats_nav = '';
4503        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4504                if (defined $file_name) {
4505                        if ($have_blame) {
4506                                $formats_nav .=
4507                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4508                                                "blame") .
4509                                        " | ";
4510                        }
4511                        $formats_nav .=
4512                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4513                                        "history") .
4514                                " | " .
4515                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4516                                        "raw") .
4517                                " | " .
4518                                $cgi->a({-href => href(action=>"blob",
4519                                                       hash_base=>"HEAD", file_name=>$file_name)},
4520                                        "HEAD");
4521                } else {
4522                        $formats_nav .=
4523                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4524                                        "raw");
4525                }
4526                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4527                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4528        } else {
4529                print "<div class=\"page_nav\">\n" .
4530                      "<br/><br/></div>\n" .
4531                      "<div class=\"title\">$hash</div>\n";
4532        }
4533        git_print_page_path($file_name, "blob", $hash_base);
4534        print "<div class=\"page_body\">\n";
4535        if ($mimetype =~ m!^image/!) {
4536                print qq!<img type="$mimetype"!;
4537                if ($file_name) {
4538                        print qq! alt="$file_name" title="$file_name"!;
4539                }
4540                print qq! src="! .
4541                      href(action=>"blob_plain", hash=>$hash,
4542                           hash_base=>$hash_base, file_name=>$file_name) .
4543                      qq!" />\n!;
4544        } else {
4545                my $nr;
4546                while (my $line = <$fd>) {
4547                        chomp $line;
4548                        $nr++;
4549                        $line = untabify($line);
4550                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4551                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4552                }
4553        }
4554        close $fd
4555                or print "Reading blob failed.\n";
4556        print "</div>";
4557        git_footer_html();
4558}
4559
4560sub git_tree {
4561        if (!defined $hash_base) {
4562                $hash_base = "HEAD";
4563        }
4564        if (!defined $hash) {
4565                if (defined $file_name) {
4566                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4567                } else {
4568                        $hash = $hash_base;
4569                }
4570        }
4571        die_error(404, "No such tree") unless defined($hash);
4572        $/ = "\0";
4573        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4574                or die_error(500, "Open git-ls-tree failed");
4575        my @entries = map { chomp; $_ } <$fd>;
4576        close $fd or die_error(404, "Reading tree failed");
4577        $/ = "\n";
4578
4579        my $refs = git_get_references();
4580        my $ref = format_ref_marker($refs, $hash_base);
4581        git_header_html();
4582        my $basedir = '';
4583        my ($have_blame) = gitweb_check_feature('blame');
4584        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4585                my @views_nav = ();
4586                if (defined $file_name) {
4587                        push @views_nav,
4588                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4589                                        "history"),
4590                                $cgi->a({-href => href(action=>"tree",
4591                                                       hash_base=>"HEAD", file_name=>$file_name)},
4592                                        "HEAD"),
4593                }
4594                my $snapshot_links = format_snapshot_links($hash);
4595                if (defined $snapshot_links) {
4596                        # FIXME: Should be available when we have no hash base as well.
4597                        push @views_nav, $snapshot_links;
4598                }
4599                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4600                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4601        } else {
4602                undef $hash_base;
4603                print "<div class=\"page_nav\">\n";
4604                print "<br/><br/></div>\n";
4605                print "<div class=\"title\">$hash</div>\n";
4606        }
4607        if (defined $file_name) {
4608                $basedir = $file_name;
4609                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4610                        $basedir .= '/';
4611                }
4612                git_print_page_path($file_name, 'tree', $hash_base);
4613        }
4614        print "<div class=\"page_body\">\n";
4615        print "<table class=\"tree\">\n";
4616        my $alternate = 1;
4617        # '..' (top directory) link if possible
4618        if (defined $hash_base &&
4619            defined $file_name && $file_name =~ m![^/]+$!) {
4620                if ($alternate) {
4621                        print "<tr class=\"dark\">\n";
4622                } else {
4623                        print "<tr class=\"light\">\n";
4624                }
4625                $alternate ^= 1;
4626
4627                my $up = $file_name;
4628                $up =~ s!/?[^/]+$!!;
4629                undef $up unless $up;
4630                # based on git_print_tree_entry
4631                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4632                print '<td class="list">';
4633                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4634                                             file_name=>$up)},
4635                              "..");
4636                print "</td>\n";
4637                print "<td class=\"link\"></td>\n";
4638
4639                print "</tr>\n";
4640        }
4641        foreach my $line (@entries) {
4642                my %t = parse_ls_tree_line($line, -z => 1);
4643
4644                if ($alternate) {
4645                        print "<tr class=\"dark\">\n";
4646                } else {
4647                        print "<tr class=\"light\">\n";
4648                }
4649                $alternate ^= 1;
4650
4651                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4652
4653                print "</tr>\n";
4654        }
4655        print "</table>\n" .
4656              "</div>";
4657        git_footer_html();
4658}
4659
4660sub git_snapshot {
4661        my @supported_fmts = gitweb_check_feature('snapshot');
4662        @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4663
4664        my $format = $cgi->param('sf');
4665        if (!@supported_fmts) {
4666                die_error(403, "Snapshots not allowed");
4667        }
4668        # default to first supported snapshot format
4669        $format ||= $supported_fmts[0];
4670        if ($format !~ m/^[a-z0-9]+$/) {
4671                die_error(400, "Invalid snapshot format parameter");
4672        } elsif (!exists($known_snapshot_formats{$format})) {
4673                die_error(400, "Unknown snapshot format");
4674        } elsif (!grep($_ eq $format, @supported_fmts)) {
4675                die_error(403, "Unsupported snapshot format");
4676        }
4677
4678        if (!defined $hash) {
4679                $hash = git_get_head_hash($project);
4680        }
4681
4682        my $name = $project;
4683        $name =~ s,([^/])/*\.git$,$1,;
4684        $name = basename($name);
4685        my $filename = to_utf8($name);
4686        $name =~ s/\047/\047\\\047\047/g;
4687        my $cmd;
4688        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4689        $cmd = quote_command(
4690                git_cmd(), 'archive',
4691                "--format=$known_snapshot_formats{$format}{'format'}",
4692                "--prefix=$name/", $hash);
4693        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4694                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4695        }
4696
4697        print $cgi->header(
4698                -type => $known_snapshot_formats{$format}{'type'},
4699                -content_disposition => 'inline; filename="' . "$filename" . '"',
4700                -status => '200 OK');
4701
4702        open my $fd, "-|", $cmd
4703                or die_error(500, "Execute git-archive failed");
4704        binmode STDOUT, ':raw';
4705        print <$fd>;
4706        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4707        close $fd;
4708}
4709
4710sub git_log {
4711        my $head = git_get_head_hash($project);
4712        if (!defined $hash) {
4713                $hash = $head;
4714        }
4715        if (!defined $page) {
4716                $page = 0;
4717        }
4718        my $refs = git_get_references();
4719
4720        my @commitlist = parse_commits($hash, 101, (100 * $page));
4721
4722        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4723
4724        git_header_html();
4725        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4726
4727        if (!@commitlist) {
4728                my %co = parse_commit($hash);
4729
4730                git_print_header_div('summary', $project);
4731                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4732        }
4733        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4734        for (my $i = 0; $i <= $to; $i++) {
4735                my %co = %{$commitlist[$i]};
4736                next if !%co;
4737                my $commit = $co{'id'};
4738                my $ref = format_ref_marker($refs, $commit);
4739                my %ad = parse_date($co{'author_epoch'});
4740                git_print_header_div('commit',
4741                               "<span class=\"age\">$co{'age_string'}</span>" .
4742                               esc_html($co{'title'}) . $ref,
4743                               $commit);
4744                print "<div class=\"title_text\">\n" .
4745                      "<div class=\"log_link\">\n" .
4746                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4747                      " | " .
4748                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4749                      " | " .
4750                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4751                      "<br/>\n" .
4752                      "</div>\n" .
4753                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4754                      "</div>\n";
4755
4756                print "<div class=\"log_body\">\n";
4757                git_print_log($co{'comment'}, -final_empty_line=> 1);
4758                print "</div>\n";
4759        }
4760        if ($#commitlist >= 100) {
4761                print "<div class=\"page_nav\">\n";
4762                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4763                               -accesskey => "n", -title => "Alt-n"}, "next");
4764                print "</div>\n";
4765        }
4766        git_footer_html();
4767}
4768
4769sub git_commit {
4770        $hash ||= $hash_base || "HEAD";
4771        my %co = parse_commit($hash)
4772            or die_error(404, "Unknown commit object");
4773        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4774        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4775
4776        my $parent  = $co{'parent'};
4777        my $parents = $co{'parents'}; # listref
4778
4779        # we need to prepare $formats_nav before any parameter munging
4780        my $formats_nav;
4781        if (!defined $parent) {
4782                # --root commitdiff
4783                $formats_nav .= '(initial)';
4784        } elsif (@$parents == 1) {
4785                # single parent commit
4786                $formats_nav .=
4787                        '(parent: ' .
4788                        $cgi->a({-href => href(action=>"commit",
4789                                               hash=>$parent)},
4790                                esc_html(substr($parent, 0, 7))) .
4791                        ')';
4792        } else {
4793                # merge commit
4794                $formats_nav .=
4795                        '(merge: ' .
4796                        join(' ', map {
4797                                $cgi->a({-href => href(action=>"commit",
4798                                                       hash=>$_)},
4799                                        esc_html(substr($_, 0, 7)));
4800                        } @$parents ) .
4801                        ')';
4802        }
4803
4804        if (!defined $parent) {
4805                $parent = "--root";
4806        }
4807        my @difftree;
4808        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4809                @diff_opts,
4810                (@$parents <= 1 ? $parent : '-c'),
4811                $hash, "--"
4812                or die_error(500, "Open git-diff-tree failed");
4813        @difftree = map { chomp; $_ } <$fd>;
4814        close $fd or die_error(404, "Reading git-diff-tree failed");
4815
4816        # non-textual hash id's can be cached
4817        my $expires;
4818        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4819                $expires = "+1d";
4820        }
4821        my $refs = git_get_references();
4822        my $ref = format_ref_marker($refs, $co{'id'});
4823
4824        git_header_html(undef, $expires);
4825        git_print_page_nav('commit', '',
4826                           $hash, $co{'tree'}, $hash,
4827                           $formats_nav);
4828
4829        if (defined $co{'parent'}) {
4830                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4831        } else {
4832                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4833        }
4834        print "<div class=\"title_text\">\n" .
4835              "<table class=\"object_header\">\n";
4836        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4837              "<tr>" .
4838              "<td></td><td> $ad{'rfc2822'}";
4839        if ($ad{'hour_local'} < 6) {
4840                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4841                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4842        } else {
4843                printf(" (%02d:%02d %s)",
4844                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4845        }
4846        print "</td>" .
4847              "</tr>\n";
4848        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4849        print "<tr><td></td><td> $cd{'rfc2822'}" .
4850              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4851              "</td></tr>\n";
4852        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4853        print "<tr>" .
4854              "<td>tree</td>" .
4855              "<td class=\"sha1\">" .
4856              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4857                       class => "list"}, $co{'tree'}) .
4858              "</td>" .
4859              "<td class=\"link\">" .
4860              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4861                      "tree");
4862        my $snapshot_links = format_snapshot_links($hash);
4863        if (defined $snapshot_links) {
4864                print " | " . $snapshot_links;
4865        }
4866        print "</td>" .
4867              "</tr>\n";
4868
4869        foreach my $par (@$parents) {
4870                print "<tr>" .
4871                      "<td>parent</td>" .
4872                      "<td class=\"sha1\">" .
4873                      $cgi->a({-href => href(action=>"commit", hash=>$par),
4874                               class => "list"}, $par) .
4875                      "</td>" .
4876                      "<td class=\"link\">" .
4877                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4878                      " | " .
4879                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4880                      "</td>" .
4881                      "</tr>\n";
4882        }
4883        print "</table>".
4884              "</div>\n";
4885
4886        print "<div class=\"page_body\">\n";
4887        git_print_log($co{'comment'});
4888        print "</div>\n";
4889
4890        git_difftree_body(\@difftree, $hash, @$parents);
4891
4892        git_footer_html();
4893}
4894
4895sub git_object {
4896        # object is defined by:
4897        # - hash or hash_base alone
4898        # - hash_base and file_name
4899        my $type;
4900
4901        # - hash or hash_base alone
4902        if ($hash || ($hash_base && !defined $file_name)) {
4903                my $object_id = $hash || $hash_base;
4904
4905                open my $fd, "-|", quote_command(
4906                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4907                        or die_error(404, "Object does not exist");
4908                $type = <$fd>;
4909                chomp $type;
4910                close $fd
4911                        or die_error(404, "Object does not exist");
4912
4913        # - hash_base and file_name
4914        } elsif ($hash_base && defined $file_name) {
4915                $file_name =~ s,/+$,,;
4916
4917                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4918                        or die_error(404, "Base object does not exist");
4919
4920                # here errors should not hapen
4921                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4922                        or die_error(500, "Open git-ls-tree failed");
4923                my $line = <$fd>;
4924                close $fd;
4925
4926                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4927                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4928                        die_error(404, "File or directory for given base does not exist");
4929                }
4930                $type = $2;
4931                $hash = $3;
4932        } else {
4933                die_error(400, "Not enough information to find object");
4934        }
4935
4936        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4937                                          hash=>$hash, hash_base=>$hash_base,
4938                                          file_name=>$file_name),
4939                             -status => '302 Found');
4940}
4941
4942sub git_blobdiff {
4943        my $format = shift || 'html';
4944
4945        my $fd;
4946        my @difftree;
4947        my %diffinfo;
4948        my $expires;
4949
4950        # preparing $fd and %diffinfo for git_patchset_body
4951        # new style URI
4952        if (defined $hash_base && defined $hash_parent_base) {
4953                if (defined $file_name) {
4954                        # read raw output
4955                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4956                                $hash_parent_base, $hash_base,
4957                                "--", (defined $file_parent ? $file_parent : ()), $file_name
4958                                or die_error(500, "Open git-diff-tree failed");
4959                        @difftree = map { chomp; $_ } <$fd>;
4960                        close $fd
4961                                or die_error(404, "Reading git-diff-tree failed");
4962                        @difftree
4963                                or die_error(404, "Blob diff not found");
4964
4965                } elsif (defined $hash &&
4966                         $hash =~ /[0-9a-fA-F]{40}/) {
4967                        # try to find filename from $hash
4968
4969                        # read filtered raw output
4970                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4971                                $hash_parent_base, $hash_base, "--"
4972                                or die_error(500, "Open git-diff-tree failed");
4973                        @difftree =
4974                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4975                                # $hash == to_id
4976                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4977                                map { chomp; $_ } <$fd>;
4978                        close $fd
4979                                or die_error(404, "Reading git-diff-tree failed");
4980                        @difftree
4981                                or die_error(404, "Blob diff not found");
4982
4983                } else {
4984                        die_error(400, "Missing one of the blob diff parameters");
4985                }
4986
4987                if (@difftree > 1) {
4988                        die_error(400, "Ambiguous blob diff specification");
4989                }
4990
4991                %diffinfo = parse_difftree_raw_line($difftree[0]);
4992                $file_parent ||= $diffinfo{'from_file'} || $file_name;
4993                $file_name   ||= $diffinfo{'to_file'};
4994
4995                $hash_parent ||= $diffinfo{'from_id'};
4996                $hash        ||= $diffinfo{'to_id'};
4997
4998                # non-textual hash id's can be cached
4999                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5000                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5001                        $expires = '+1d';
5002                }
5003
5004                # open patch output
5005                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5006                        '-p', ($format eq 'html' ? "--full-index" : ()),
5007                        $hash_parent_base, $hash_base,
5008                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5009                        or die_error(500, "Open git-diff-tree failed");
5010        }
5011
5012        # old/legacy style URI
5013        if (!%diffinfo && # if new style URI failed
5014            defined $hash && defined $hash_parent) {
5015                # fake git-diff-tree raw output
5016                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5017                $diffinfo{'from_id'} = $hash_parent;
5018                $diffinfo{'to_id'}   = $hash;
5019                if (defined $file_name) {
5020                        if (defined $file_parent) {
5021                                $diffinfo{'status'} = '2';
5022                                $diffinfo{'from_file'} = $file_parent;
5023                                $diffinfo{'to_file'}   = $file_name;
5024                        } else { # assume not renamed
5025                                $diffinfo{'status'} = '1';
5026                                $diffinfo{'from_file'} = $file_name;
5027                                $diffinfo{'to_file'}   = $file_name;
5028                        }
5029                } else { # no filename given
5030                        $diffinfo{'status'} = '2';
5031                        $diffinfo{'from_file'} = $hash_parent;
5032                        $diffinfo{'to_file'}   = $hash;
5033                }
5034
5035                # non-textual hash id's can be cached
5036                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5037                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5038                        $expires = '+1d';
5039                }
5040
5041                # open patch output
5042                open $fd, "-|", git_cmd(), "diff", @diff_opts,
5043                        '-p', ($format eq 'html' ? "--full-index" : ()),
5044                        $hash_parent, $hash, "--"
5045                        or die_error(500, "Open git-diff failed");
5046        } else  {
5047                die_error(400, "Missing one of the blob diff parameters")
5048                        unless %diffinfo;
5049        }
5050
5051        # header
5052        if ($format eq 'html') {
5053                my $formats_nav =
5054                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5055                                "raw");
5056                git_header_html(undef, $expires);
5057                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5058                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5059                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5060                } else {
5061                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5062                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5063                }
5064                if (defined $file_name) {
5065                        git_print_page_path($file_name, "blob", $hash_base);
5066                } else {
5067                        print "<div class=\"page_path\"></div>\n";
5068                }
5069
5070        } elsif ($format eq 'plain') {
5071                print $cgi->header(
5072                        -type => 'text/plain',
5073                        -charset => 'utf-8',
5074                        -expires => $expires,
5075                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5076
5077                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5078
5079        } else {
5080                die_error(400, "Unknown blobdiff format");
5081        }
5082
5083        # patch
5084        if ($format eq 'html') {
5085                print "<div class=\"page_body\">\n";
5086
5087                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5088                close $fd;
5089
5090                print "</div>\n"; # class="page_body"
5091                git_footer_html();
5092
5093        } else {
5094                while (my $line = <$fd>) {
5095                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5096                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5097
5098                        print $line;
5099
5100                        last if $line =~ m!^\+\+\+!;
5101                }
5102                local $/ = undef;
5103                print <$fd>;
5104                close $fd;
5105        }
5106}
5107
5108sub git_blobdiff_plain {
5109        git_blobdiff('plain');
5110}
5111
5112sub git_commitdiff {
5113        my $format = shift || 'html';
5114        $hash ||= $hash_base || "HEAD";
5115        my %co = parse_commit($hash)
5116            or die_error(404, "Unknown commit object");
5117
5118        # choose format for commitdiff for merge
5119        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5120                $hash_parent = '--cc';
5121        }
5122        # we need to prepare $formats_nav before almost any parameter munging
5123        my $formats_nav;
5124        if ($format eq 'html') {
5125                $formats_nav =
5126                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5127                                "raw");
5128
5129                if (defined $hash_parent &&
5130                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5131                        # commitdiff with two commits given
5132                        my $hash_parent_short = $hash_parent;
5133                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5134                                $hash_parent_short = substr($hash_parent, 0, 7);
5135                        }
5136                        $formats_nav .=
5137                                ' (from';
5138                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5139                                if ($co{'parents'}[$i] eq $hash_parent) {
5140                                        $formats_nav .= ' parent ' . ($i+1);
5141                                        last;
5142                                }
5143                        }
5144                        $formats_nav .= ': ' .
5145                                $cgi->a({-href => href(action=>"commitdiff",
5146                                                       hash=>$hash_parent)},
5147                                        esc_html($hash_parent_short)) .
5148                                ')';
5149                } elsif (!$co{'parent'}) {
5150                        # --root commitdiff
5151                        $formats_nav .= ' (initial)';
5152                } elsif (scalar @{$co{'parents'}} == 1) {
5153                        # single parent commit
5154                        $formats_nav .=
5155                                ' (parent: ' .
5156                                $cgi->a({-href => href(action=>"commitdiff",
5157                                                       hash=>$co{'parent'})},
5158                                        esc_html(substr($co{'parent'}, 0, 7))) .
5159                                ')';
5160                } else {
5161                        # merge commit
5162                        if ($hash_parent eq '--cc') {
5163                                $formats_nav .= ' | ' .
5164                                        $cgi->a({-href => href(action=>"commitdiff",
5165                                                               hash=>$hash, hash_parent=>'-c')},
5166                                                'combined');
5167                        } else { # $hash_parent eq '-c'
5168                                $formats_nav .= ' | ' .
5169                                        $cgi->a({-href => href(action=>"commitdiff",
5170                                                               hash=>$hash, hash_parent=>'--cc')},
5171                                                'compact');
5172                        }
5173                        $formats_nav .=
5174                                ' (merge: ' .
5175                                join(' ', map {
5176                                        $cgi->a({-href => href(action=>"commitdiff",
5177                                                               hash=>$_)},
5178                                                esc_html(substr($_, 0, 7)));
5179                                } @{$co{'parents'}} ) .
5180                                ')';
5181                }
5182        }
5183
5184        my $hash_parent_param = $hash_parent;
5185        if (!defined $hash_parent_param) {
5186                # --cc for multiple parents, --root for parentless
5187                $hash_parent_param =
5188                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5189        }
5190
5191        # read commitdiff
5192        my $fd;
5193        my @difftree;
5194        if ($format eq 'html') {
5195                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5196                        "--no-commit-id", "--patch-with-raw", "--full-index",
5197                        $hash_parent_param, $hash, "--"
5198                        or die_error(500, "Open git-diff-tree failed");
5199
5200                while (my $line = <$fd>) {
5201                        chomp $line;
5202                        # empty line ends raw part of diff-tree output
5203                        last unless $line;
5204                        push @difftree, scalar parse_difftree_raw_line($line);
5205                }
5206
5207        } elsif ($format eq 'plain') {
5208                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5209                        '-p', $hash_parent_param, $hash, "--"
5210                        or die_error(500, "Open git-diff-tree failed");
5211
5212        } else {
5213                die_error(400, "Unknown commitdiff format");
5214        }
5215
5216        # non-textual hash id's can be cached
5217        my $expires;
5218        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5219                $expires = "+1d";
5220        }
5221
5222        # write commit message
5223        if ($format eq 'html') {
5224                my $refs = git_get_references();
5225                my $ref = format_ref_marker($refs, $co{'id'});
5226
5227                git_header_html(undef, $expires);
5228                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5229                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5230                git_print_authorship(\%co);
5231                print "<div class=\"page_body\">\n";
5232                if (@{$co{'comment'}} > 1) {
5233                        print "<div class=\"log\">\n";
5234                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5235                        print "</div>\n"; # class="log"
5236                }
5237
5238        } elsif ($format eq 'plain') {
5239                my $refs = git_get_references("tags");
5240                my $tagname = git_get_rev_name_tags($hash);
5241                my $filename = basename($project) . "-$hash.patch";
5242
5243                print $cgi->header(
5244                        -type => 'text/plain',
5245                        -charset => 'utf-8',
5246                        -expires => $expires,
5247                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5248                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5249                print "From: " . to_utf8($co{'author'}) . "\n";
5250                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5251                print "Subject: " . to_utf8($co{'title'}) . "\n";
5252
5253                print "X-Git-Tag: $tagname\n" if $tagname;
5254                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5255
5256                foreach my $line (@{$co{'comment'}}) {
5257                        print to_utf8($line) . "\n";
5258                }
5259                print "---\n\n";
5260        }
5261
5262        # write patch
5263        if ($format eq 'html') {
5264                my $use_parents = !defined $hash_parent ||
5265                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5266                git_difftree_body(\@difftree, $hash,
5267                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5268                print "<br/>\n";
5269
5270                git_patchset_body($fd, \@difftree, $hash,
5271                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5272                close $fd;
5273                print "</div>\n"; # class="page_body"
5274                git_footer_html();
5275
5276        } elsif ($format eq 'plain') {
5277                local $/ = undef;
5278                print <$fd>;
5279                close $fd
5280                        or print "Reading git-diff-tree failed\n";
5281        }
5282}
5283
5284sub git_commitdiff_plain {
5285        git_commitdiff('plain');
5286}
5287
5288sub git_history {
5289        if (!defined $hash_base) {
5290                $hash_base = git_get_head_hash($project);
5291        }
5292        if (!defined $page) {
5293                $page = 0;
5294        }
5295        my $ftype;
5296        my %co = parse_commit($hash_base)
5297            or die_error(404, "Unknown commit object");
5298
5299        my $refs = git_get_references();
5300        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5301
5302        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5303                                       $file_name, "--full-history")
5304            or die_error(404, "No such file or directory on given branch");
5305
5306        if (!defined $hash && defined $file_name) {
5307                # some commits could have deleted file in question,
5308                # and not have it in tree, but one of them has to have it
5309                for (my $i = 0; $i <= @commitlist; $i++) {
5310                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5311                        last if defined $hash;
5312                }
5313        }
5314        if (defined $hash) {
5315                $ftype = git_get_type($hash);
5316        }
5317        if (!defined $ftype) {
5318                die_error(500, "Unknown type of object");
5319        }
5320
5321        my $paging_nav = '';
5322        if ($page > 0) {
5323                $paging_nav .=
5324                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5325                                               file_name=>$file_name)},
5326                                "first");
5327                $paging_nav .= " &sdot; " .
5328                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5329                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5330        } else {
5331                $paging_nav .= "first";
5332                $paging_nav .= " &sdot; prev";
5333        }
5334        my $next_link = '';
5335        if ($#commitlist >= 100) {
5336                $next_link =
5337                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5338                                 -accesskey => "n", -title => "Alt-n"}, "next");
5339                $paging_nav .= " &sdot; $next_link";
5340        } else {
5341                $paging_nav .= " &sdot; next";
5342        }
5343
5344        git_header_html();
5345        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5346        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5347        git_print_page_path($file_name, $ftype, $hash_base);
5348
5349        git_history_body(\@commitlist, 0, 99,
5350                         $refs, $hash_base, $ftype, $next_link);
5351
5352        git_footer_html();
5353}
5354
5355sub git_search {
5356        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5357        if (!defined $searchtext) {
5358                die_error(400, "Text field is empty");
5359        }
5360        if (!defined $hash) {
5361                $hash = git_get_head_hash($project);
5362        }
5363        my %co = parse_commit($hash);
5364        if (!%co) {
5365                die_error(404, "Unknown commit object");
5366        }
5367        if (!defined $page) {
5368                $page = 0;
5369        }
5370
5371        $searchtype ||= 'commit';
5372        if ($searchtype eq 'pickaxe') {
5373                # pickaxe may take all resources of your box and run for several minutes
5374                # with every query - so decide by yourself how public you make this feature
5375                gitweb_check_feature('pickaxe')
5376                    or die_error(403, "Pickaxe is disabled");
5377        }
5378        if ($searchtype eq 'grep') {
5379                gitweb_check_feature('grep')
5380                    or die_error(403, "Grep is disabled");
5381        }
5382
5383        git_header_html();
5384
5385        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5386                my $greptype;
5387                if ($searchtype eq 'commit') {
5388                        $greptype = "--grep=";
5389                } elsif ($searchtype eq 'author') {
5390                        $greptype = "--author=";
5391                } elsif ($searchtype eq 'committer') {
5392                        $greptype = "--committer=";
5393                }
5394                $greptype .= $searchtext;
5395                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5396                                               $greptype, '--regexp-ignore-case',
5397                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5398
5399                my $paging_nav = '';
5400                if ($page > 0) {
5401                        $paging_nav .=
5402                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5403                                                       searchtext=>$searchtext,
5404                                                       searchtype=>$searchtype)},
5405                                        "first");
5406                        $paging_nav .= " &sdot; " .
5407                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5408                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5409                } else {
5410                        $paging_nav .= "first";
5411                        $paging_nav .= " &sdot; prev";
5412                }
5413                my $next_link = '';
5414                if ($#commitlist >= 100) {
5415                        $next_link =
5416                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5417                                         -accesskey => "n", -title => "Alt-n"}, "next");
5418                        $paging_nav .= " &sdot; $next_link";
5419                } else {
5420                        $paging_nav .= " &sdot; next";
5421                }
5422
5423                if ($#commitlist >= 100) {
5424                }
5425
5426                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5427                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5428                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5429        }
5430
5431        if ($searchtype eq 'pickaxe') {
5432                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5433                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5434
5435                print "<table class=\"pickaxe search\">\n";
5436                my $alternate = 1;
5437                $/ = "\n";
5438                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5439                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5440                        ($search_use_regexp ? '--pickaxe-regex' : ());
5441                undef %co;
5442                my @files;
5443                while (my $line = <$fd>) {
5444                        chomp $line;
5445                        next unless $line;
5446
5447                        my %set = parse_difftree_raw_line($line);
5448                        if (defined $set{'commit'}) {
5449                                # finish previous commit
5450                                if (%co) {
5451                                        print "</td>\n" .
5452                                              "<td class=\"link\">" .
5453                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5454                                              " | " .
5455                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5456                                        print "</td>\n" .
5457                                              "</tr>\n";
5458                                }
5459
5460                                if ($alternate) {
5461                                        print "<tr class=\"dark\">\n";
5462                                } else {
5463                                        print "<tr class=\"light\">\n";
5464                                }
5465                                $alternate ^= 1;
5466                                %co = parse_commit($set{'commit'});
5467                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5468                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5469                                      "<td><i>$author</i></td>\n" .
5470                                      "<td>" .
5471                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5472                                              -class => "list subject"},
5473                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5474                        } elsif (defined $set{'to_id'}) {
5475                                next if ($set{'to_id'} =~ m/^0{40}$/);
5476
5477                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5478                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5479                                              -class => "list"},
5480                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5481                                      "<br/>\n";
5482                        }
5483                }
5484                close $fd;
5485
5486                # finish last commit (warning: repetition!)
5487                if (%co) {
5488                        print "</td>\n" .
5489                              "<td class=\"link\">" .
5490                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5491                              " | " .
5492                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5493                        print "</td>\n" .
5494                              "</tr>\n";
5495                }
5496
5497                print "</table>\n";
5498        }
5499
5500        if ($searchtype eq 'grep') {
5501                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5502                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5503
5504                print "<table class=\"grep_search\">\n";
5505                my $alternate = 1;
5506                my $matches = 0;
5507                $/ = "\n";
5508                open my $fd, "-|", git_cmd(), 'grep', '-n',
5509                        $search_use_regexp ? ('-E', '-i') : '-F',
5510                        $searchtext, $co{'tree'};
5511                my $lastfile = '';
5512                while (my $line = <$fd>) {
5513                        chomp $line;
5514                        my ($file, $lno, $ltext, $binary);
5515                        last if ($matches++ > 1000);
5516                        if ($line =~ /^Binary file (.+) matches$/) {
5517                                $file = $1;
5518                                $binary = 1;
5519                        } else {
5520                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5521                        }
5522                        if ($file ne $lastfile) {
5523                                $lastfile and print "</td></tr>\n";
5524                                if ($alternate++) {
5525                                        print "<tr class=\"dark\">\n";
5526                                } else {
5527                                        print "<tr class=\"light\">\n";
5528                                }
5529                                print "<td class=\"list\">".
5530                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5531                                                               file_name=>"$file"),
5532                                                -class => "list"}, esc_path($file));
5533                                print "</td><td>\n";
5534                                $lastfile = $file;
5535                        }
5536                        if ($binary) {
5537                                print "<div class=\"binary\">Binary file</div>\n";
5538                        } else {
5539                                $ltext = untabify($ltext);
5540                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5541                                        $ltext = esc_html($1, -nbsp=>1);
5542                                        $ltext .= '<span class="match">';
5543                                        $ltext .= esc_html($2, -nbsp=>1);
5544                                        $ltext .= '</span>';
5545                                        $ltext .= esc_html($3, -nbsp=>1);
5546                                } else {
5547                                        $ltext = esc_html($ltext, -nbsp=>1);
5548                                }
5549                                print "<div class=\"pre\">" .
5550                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5551                                                               file_name=>"$file").'#l'.$lno,
5552                                                -class => "linenr"}, sprintf('%4i', $lno))
5553                                        . ' ' .  $ltext . "</div>\n";
5554                        }
5555                }
5556                if ($lastfile) {
5557                        print "</td></tr>\n";
5558                        if ($matches > 1000) {
5559                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5560                        }
5561                } else {
5562                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5563                }
5564                close $fd;
5565
5566                print "</table>\n";
5567        }
5568        git_footer_html();
5569}
5570
5571sub git_search_help {
5572        git_header_html();
5573        git_print_page_nav('','', $hash,$hash,$hash);
5574        print <<EOT;
5575<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5576regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5577the pattern entered is recognized as the POSIX extended
5578<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5579insensitive).</p>
5580<dl>
5581<dt><b>commit</b></dt>
5582<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5583EOT
5584        my ($have_grep) = gitweb_check_feature('grep');
5585        if ($have_grep) {
5586                print <<EOT;
5587<dt><b>grep</b></dt>
5588<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5589    a different one) are searched for the given pattern. On large trees, this search can take
5590a while and put some strain on the server, so please use it with some consideration. Note that
5591due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5592case-sensitive.</dd>
5593EOT
5594        }
5595        print <<EOT;
5596<dt><b>author</b></dt>
5597<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5598<dt><b>committer</b></dt>
5599<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5600EOT
5601        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5602        if ($have_pickaxe) {
5603                print <<EOT;
5604<dt><b>pickaxe</b></dt>
5605<dd>All commits that caused the string to appear or disappear from any file (changes that
5606added, removed or "modified" the string) will be listed. This search can take a while and
5607takes a lot of strain on the server, so please use it wisely. Note that since you may be
5608interested even in changes just changing the case as well, this search is case sensitive.</dd>
5609EOT
5610        }
5611        print "</dl>\n";
5612        git_footer_html();
5613}
5614
5615sub git_shortlog {
5616        my $head = git_get_head_hash($project);
5617        if (!defined $hash) {
5618                $hash = $head;
5619        }
5620        if (!defined $page) {
5621                $page = 0;
5622        }
5623        my $refs = git_get_references();
5624
5625        my $commit_hash = $hash;
5626        if (defined $hash_parent) {
5627                $commit_hash = "$hash_parent..$hash";
5628        }
5629        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5630
5631        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5632        my $next_link = '';
5633        if ($#commitlist >= 100) {
5634                $next_link =
5635                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5636                                 -accesskey => "n", -title => "Alt-n"}, "next");
5637        }
5638
5639        git_header_html();
5640        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5641        git_print_header_div('summary', $project);
5642
5643        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5644
5645        git_footer_html();
5646}
5647
5648## ......................................................................
5649## feeds (RSS, Atom; OPML)
5650
5651sub git_feed {
5652        my $format = shift || 'atom';
5653        my ($have_blame) = gitweb_check_feature('blame');
5654
5655        # Atom: http://www.atomenabled.org/developers/syndication/
5656        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5657        if ($format ne 'rss' && $format ne 'atom') {
5658                die_error(400, "Unknown web feed format");
5659        }
5660
5661        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5662        my $head = $hash || 'HEAD';
5663        my @commitlist = parse_commits($head, 150, 0, $file_name);
5664
5665        my %latest_commit;
5666        my %latest_date;
5667        my $content_type = "application/$format+xml";
5668        if (defined $cgi->http('HTTP_ACCEPT') &&
5669                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5670                # browser (feed reader) prefers text/xml
5671                $content_type = 'text/xml';
5672        }
5673        if (defined($commitlist[0])) {
5674                %latest_commit = %{$commitlist[0]};
5675                %latest_date   = parse_date($latest_commit{'author_epoch'});
5676                print $cgi->header(
5677                        -type => $content_type,
5678                        -charset => 'utf-8',
5679                        -last_modified => $latest_date{'rfc2822'});
5680        } else {
5681                print $cgi->header(
5682                        -type => $content_type,
5683                        -charset => 'utf-8');
5684        }
5685
5686        # Optimization: skip generating the body if client asks only
5687        # for Last-Modified date.
5688        return if ($cgi->request_method() eq 'HEAD');
5689
5690        # header variables
5691        my $title = "$site_name - $project/$action";
5692        my $feed_type = 'log';
5693        if (defined $hash) {
5694                $title .= " - '$hash'";
5695                $feed_type = 'branch log';
5696                if (defined $file_name) {
5697                        $title .= " :: $file_name";
5698                        $feed_type = 'history';
5699                }
5700        } elsif (defined $file_name) {
5701                $title .= " - $file_name";
5702                $feed_type = 'history';
5703        }
5704        $title .= " $feed_type";
5705        my $descr = git_get_project_description($project);
5706        if (defined $descr) {
5707                $descr = esc_html($descr);
5708        } else {
5709                $descr = "$project " .
5710                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5711                         " feed";
5712        }
5713        my $owner = git_get_project_owner($project);
5714        $owner = esc_html($owner);
5715
5716        #header
5717        my $alt_url;
5718        if (defined $file_name) {
5719                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5720        } elsif (defined $hash) {
5721                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5722        } else {
5723                $alt_url = href(-full=>1, action=>"summary");
5724        }
5725        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5726        if ($format eq 'rss') {
5727                print <<XML;
5728<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5729<channel>
5730XML
5731                print "<title>$title</title>\n" .
5732                      "<link>$alt_url</link>\n" .
5733                      "<description>$descr</description>\n" .
5734                      "<language>en</language>\n";
5735        } elsif ($format eq 'atom') {
5736                print <<XML;
5737<feed xmlns="http://www.w3.org/2005/Atom">
5738XML
5739                print "<title>$title</title>\n" .
5740                      "<subtitle>$descr</subtitle>\n" .
5741                      '<link rel="alternate" type="text/html" href="' .
5742                      $alt_url . '" />' . "\n" .
5743                      '<link rel="self" type="' . $content_type . '" href="' .
5744                      $cgi->self_url() . '" />' . "\n" .
5745                      "<id>" . href(-full=>1) . "</id>\n" .
5746                      # use project owner for feed author
5747                      "<author><name>$owner</name></author>\n";
5748                if (defined $favicon) {
5749                        print "<icon>" . esc_url($favicon) . "</icon>\n";
5750                }
5751                if (defined $logo_url) {
5752                        # not twice as wide as tall: 72 x 27 pixels
5753                        print "<logo>" . esc_url($logo) . "</logo>\n";
5754                }
5755                if (! %latest_date) {
5756                        # dummy date to keep the feed valid until commits trickle in:
5757                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
5758                } else {
5759                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
5760                }
5761        }
5762
5763        # contents
5764        for (my $i = 0; $i <= $#commitlist; $i++) {
5765                my %co = %{$commitlist[$i]};
5766                my $commit = $co{'id'};
5767                # we read 150, we always show 30 and the ones more recent than 48 hours
5768                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5769                        last;
5770                }
5771                my %cd = parse_date($co{'author_epoch'});
5772
5773                # get list of changed files
5774                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5775                        $co{'parent'} || "--root",
5776                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
5777                        or next;
5778                my @difftree = map { chomp; $_ } <$fd>;
5779                close $fd
5780                        or next;
5781
5782                # print element (entry, item)
5783                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5784                if ($format eq 'rss') {
5785                        print "<item>\n" .
5786                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
5787                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
5788                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5789                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5790                              "<link>$co_url</link>\n" .
5791                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
5792                              "<content:encoded>" .
5793                              "<![CDATA[\n";
5794                } elsif ($format eq 'atom') {
5795                        print "<entry>\n" .
5796                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5797                              "<updated>$cd{'iso-8601'}</updated>\n" .
5798                              "<author>\n" .
5799                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5800                        if ($co{'author_email'}) {
5801                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5802                        }
5803                        print "</author>\n" .
5804                              # use committer for contributor
5805                              "<contributor>\n" .
5806                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5807                        if ($co{'committer_email'}) {
5808                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5809                        }
5810                        print "</contributor>\n" .
5811                              "<published>$cd{'iso-8601'}</published>\n" .
5812                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5813                              "<id>$co_url</id>\n" .
5814                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5815                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5816                }
5817                my $comment = $co{'comment'};
5818                print "<pre>\n";
5819                foreach my $line (@$comment) {
5820                        $line = esc_html($line);
5821                        print "$line\n";
5822                }
5823                print "</pre><ul>\n";
5824                foreach my $difftree_line (@difftree) {
5825                        my %difftree = parse_difftree_raw_line($difftree_line);
5826                        next if !$difftree{'from_id'};
5827
5828                        my $file = $difftree{'file'} || $difftree{'to_file'};
5829
5830                        print "<li>" .
5831                              "[" .
5832                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5833                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5834                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5835                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
5836                                      -title => "diff"}, 'D');
5837                        if ($have_blame) {
5838                                print $cgi->a({-href => href(-full=>1, action=>"blame",
5839                                                             file_name=>$file, hash_base=>$commit),
5840                                              -title => "blame"}, 'B');
5841                        }
5842                        # if this is not a feed of a file history
5843                        if (!defined $file_name || $file_name ne $file) {
5844                                print $cgi->a({-href => href(-full=>1, action=>"history",
5845                                                             file_name=>$file, hash=>$commit),
5846                                              -title => "history"}, 'H');
5847                        }
5848                        $file = esc_path($file);
5849                        print "] ".
5850                              "$file</li>\n";
5851                }
5852                if ($format eq 'rss') {
5853                        print "</ul>]]>\n" .
5854                              "</content:encoded>\n" .
5855                              "</item>\n";
5856                } elsif ($format eq 'atom') {
5857                        print "</ul>\n</div>\n" .
5858                              "</content>\n" .
5859                              "</entry>\n";
5860                }
5861        }
5862
5863        # end of feed
5864        if ($format eq 'rss') {
5865                print "</channel>\n</rss>\n";
5866        }       elsif ($format eq 'atom') {
5867                print "</feed>\n";
5868        }
5869}
5870
5871sub git_rss {
5872        git_feed('rss');
5873}
5874
5875sub git_atom {
5876        git_feed('atom');
5877}
5878
5879sub git_opml {
5880        my @list = git_get_projects_list();
5881
5882        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5883        print <<XML;
5884<?xml version="1.0" encoding="utf-8"?>
5885<opml version="1.0">
5886<head>
5887  <title>$site_name OPML Export</title>
5888</head>
5889<body>
5890<outline text="git RSS feeds">
5891XML
5892
5893        foreach my $pr (@list) {
5894                my %proj = %$pr;
5895                my $head = git_get_head_hash($proj{'path'});
5896                if (!defined $head) {
5897                        next;
5898                }
5899                $git_dir = "$projectroot/$proj{'path'}";
5900                my %co = parse_commit($head);
5901                if (!%co) {
5902                        next;
5903                }
5904
5905                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5906                my $rss  = "$my_url?p=$proj{'path'};a=rss";
5907                my $html = "$my_url?p=$proj{'path'};a=summary";
5908                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5909        }
5910        print <<XML;
5911</outline>
5912</body>
5913</opml>
5914XML
5915}