gitweb / gitweb.perlon commit gitweb: Support for simple project search form (0d1d154)
   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                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3771                        and not $pr->{'descr_long'} =~ /$searchtext/;
3772                # Weed out forks or non-matching entries of search
3773                if ($check_forks) {
3774                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3775                        $forkbase="^$forkbase" if $forkbase;
3776                        next if not $searchtext and not $tagfilter and $show_ctags
3777                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3778                }
3779
3780                if ($alternate) {
3781                        print "<tr class=\"dark\">\n";
3782                } else {
3783                        print "<tr class=\"light\">\n";
3784                }
3785                $alternate ^= 1;
3786                if ($check_forks) {
3787                        print "<td>";
3788                        if ($pr->{'forks'}) {
3789                                print "<!-- $pr->{'forks'} -->\n";
3790                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3791                        }
3792                        print "</td>\n";
3793                }
3794                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3795                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3796                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3797                                        -class => "list", -title => $pr->{'descr_long'}},
3798                                        esc_html($pr->{'descr'})) . "</td>\n" .
3799                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3800                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3801                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3802                      "<td class=\"link\">" .
3803                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3804                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3805                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3806                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3807                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3808                      "</td>\n" .
3809                      "</tr>\n";
3810        }
3811        if (defined $extra) {
3812                print "<tr>\n";
3813                if ($check_forks) {
3814                        print "<td></td>\n";
3815                }
3816                print "<td colspan=\"5\">$extra</td>\n" .
3817                      "</tr>\n";
3818        }
3819        print "</table>\n";
3820}
3821
3822sub git_shortlog_body {
3823        # uses global variable $project
3824        my ($commitlist, $from, $to, $refs, $extra) = @_;
3825
3826        $from = 0 unless defined $from;
3827        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3828
3829        print "<table class=\"shortlog\">\n";
3830        my $alternate = 1;
3831        for (my $i = $from; $i <= $to; $i++) {
3832                my %co = %{$commitlist->[$i]};
3833                my $commit = $co{'id'};
3834                my $ref = format_ref_marker($refs, $commit);
3835                if ($alternate) {
3836                        print "<tr class=\"dark\">\n";
3837                } else {
3838                        print "<tr class=\"light\">\n";
3839                }
3840                $alternate ^= 1;
3841                my $author = chop_and_escape_str($co{'author_name'}, 10);
3842                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3843                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3844                      "<td><i>" . $author . "</i></td>\n" .
3845                      "<td>";
3846                print format_subject_html($co{'title'}, $co{'title_short'},
3847                                          href(action=>"commit", hash=>$commit), $ref);
3848                print "</td>\n" .
3849                      "<td class=\"link\">" .
3850                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3851                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3852                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3853                my $snapshot_links = format_snapshot_links($commit);
3854                if (defined $snapshot_links) {
3855                        print " | " . $snapshot_links;
3856                }
3857                print "</td>\n" .
3858                      "</tr>\n";
3859        }
3860        if (defined $extra) {
3861                print "<tr>\n" .
3862                      "<td colspan=\"4\">$extra</td>\n" .
3863                      "</tr>\n";
3864        }
3865        print "</table>\n";
3866}
3867
3868sub git_history_body {
3869        # Warning: assumes constant type (blob or tree) during history
3870        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3871
3872        $from = 0 unless defined $from;
3873        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3874
3875        print "<table class=\"history\">\n";
3876        my $alternate = 1;
3877        for (my $i = $from; $i <= $to; $i++) {
3878                my %co = %{$commitlist->[$i]};
3879                if (!%co) {
3880                        next;
3881                }
3882                my $commit = $co{'id'};
3883
3884                my $ref = format_ref_marker($refs, $commit);
3885
3886                if ($alternate) {
3887                        print "<tr class=\"dark\">\n";
3888                } else {
3889                        print "<tr class=\"light\">\n";
3890                }
3891                $alternate ^= 1;
3892        # shortlog uses      chop_str($co{'author_name'}, 10)
3893                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3894                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3895                      "<td><i>" . $author . "</i></td>\n" .
3896                      "<td>";
3897                # originally git_history used chop_str($co{'title'}, 50)
3898                print format_subject_html($co{'title'}, $co{'title_short'},
3899                                          href(action=>"commit", hash=>$commit), $ref);
3900                print "</td>\n" .
3901                      "<td class=\"link\">" .
3902                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3903                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3904
3905                if ($ftype eq 'blob') {
3906                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3907                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3908                        if (defined $blob_current && defined $blob_parent &&
3909                                        $blob_current ne $blob_parent) {
3910                                print " | " .
3911                                        $cgi->a({-href => href(action=>"blobdiff",
3912                                                               hash=>$blob_current, hash_parent=>$blob_parent,
3913                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
3914                                                               file_name=>$file_name)},
3915                                                "diff to current");
3916                        }
3917                }
3918                print "</td>\n" .
3919                      "</tr>\n";
3920        }
3921        if (defined $extra) {
3922                print "<tr>\n" .
3923                      "<td colspan=\"4\">$extra</td>\n" .
3924                      "</tr>\n";
3925        }
3926        print "</table>\n";
3927}
3928
3929sub git_tags_body {
3930        # uses global variable $project
3931        my ($taglist, $from, $to, $extra) = @_;
3932        $from = 0 unless defined $from;
3933        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3934
3935        print "<table class=\"tags\">\n";
3936        my $alternate = 1;
3937        for (my $i = $from; $i <= $to; $i++) {
3938                my $entry = $taglist->[$i];
3939                my %tag = %$entry;
3940                my $comment = $tag{'subject'};
3941                my $comment_short;
3942                if (defined $comment) {
3943                        $comment_short = chop_str($comment, 30, 5);
3944                }
3945                if ($alternate) {
3946                        print "<tr class=\"dark\">\n";
3947                } else {
3948                        print "<tr class=\"light\">\n";
3949                }
3950                $alternate ^= 1;
3951                if (defined $tag{'age'}) {
3952                        print "<td><i>$tag{'age'}</i></td>\n";
3953                } else {
3954                        print "<td></td>\n";
3955                }
3956                print "<td>" .
3957                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3958                               -class => "list name"}, esc_html($tag{'name'})) .
3959                      "</td>\n" .
3960                      "<td>";
3961                if (defined $comment) {
3962                        print format_subject_html($comment, $comment_short,
3963                                                  href(action=>"tag", hash=>$tag{'id'}));
3964                }
3965                print "</td>\n" .
3966                      "<td class=\"selflink\">";
3967                if ($tag{'type'} eq "tag") {
3968                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3969                } else {
3970                        print "&nbsp;";
3971                }
3972                print "</td>\n" .
3973                      "<td class=\"link\">" . " | " .
3974                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3975                if ($tag{'reftype'} eq "commit") {
3976                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3977                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3978                } elsif ($tag{'reftype'} eq "blob") {
3979                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3980                }
3981                print "</td>\n" .
3982                      "</tr>";
3983        }
3984        if (defined $extra) {
3985                print "<tr>\n" .
3986                      "<td colspan=\"5\">$extra</td>\n" .
3987                      "</tr>\n";
3988        }
3989        print "</table>\n";
3990}
3991
3992sub git_heads_body {
3993        # uses global variable $project
3994        my ($headlist, $head, $from, $to, $extra) = @_;
3995        $from = 0 unless defined $from;
3996        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3997
3998        print "<table class=\"heads\">\n";
3999        my $alternate = 1;
4000        for (my $i = $from; $i <= $to; $i++) {
4001                my $entry = $headlist->[$i];
4002                my %ref = %$entry;
4003                my $curr = $ref{'id'} eq $head;
4004                if ($alternate) {
4005                        print "<tr class=\"dark\">\n";
4006                } else {
4007                        print "<tr class=\"light\">\n";
4008                }
4009                $alternate ^= 1;
4010                print "<td><i>$ref{'age'}</i></td>\n" .
4011                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4012                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4013                               -class => "list name"},esc_html($ref{'name'})) .
4014                      "</td>\n" .
4015                      "<td class=\"link\">" .
4016                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4017                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4018                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4019                      "</td>\n" .
4020                      "</tr>";
4021        }
4022        if (defined $extra) {
4023                print "<tr>\n" .
4024                      "<td colspan=\"3\">$extra</td>\n" .
4025                      "</tr>\n";
4026        }
4027        print "</table>\n";
4028}
4029
4030sub git_search_grep_body {
4031        my ($commitlist, $from, $to, $extra) = @_;
4032        $from = 0 unless defined $from;
4033        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4034
4035        print "<table class=\"commit_search\">\n";
4036        my $alternate = 1;
4037        for (my $i = $from; $i <= $to; $i++) {
4038                my %co = %{$commitlist->[$i]};
4039                if (!%co) {
4040                        next;
4041                }
4042                my $commit = $co{'id'};
4043                if ($alternate) {
4044                        print "<tr class=\"dark\">\n";
4045                } else {
4046                        print "<tr class=\"light\">\n";
4047                }
4048                $alternate ^= 1;
4049                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4050                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4051                      "<td><i>" . $author . "</i></td>\n" .
4052                      "<td>" .
4053                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4054                               -class => "list subject"},
4055                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4056                my $comment = $co{'comment'};
4057                foreach my $line (@$comment) {
4058                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4059                                my ($lead, $match, $trail) = ($1, $2, $3);
4060                                $match = chop_str($match, 70, 5, 'center');
4061                                my $contextlen = int((80 - length($match))/2);
4062                                $contextlen = 30 if ($contextlen > 30);
4063                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4064                                $trail = chop_str($trail, $contextlen, 10, 'right');
4065
4066                                $lead  = esc_html($lead);
4067                                $match = esc_html($match);
4068                                $trail = esc_html($trail);
4069
4070                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4071                        }
4072                }
4073                print "</td>\n" .
4074                      "<td class=\"link\">" .
4075                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4076                      " | " .
4077                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4078                      " | " .
4079                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4080                print "</td>\n" .
4081                      "</tr>\n";
4082        }
4083        if (defined $extra) {
4084                print "<tr>\n" .
4085                      "<td colspan=\"3\">$extra</td>\n" .
4086                      "</tr>\n";
4087        }
4088        print "</table>\n";
4089}
4090
4091## ======================================================================
4092## ======================================================================
4093## actions
4094
4095sub git_project_list {
4096        my $order = $cgi->param('o');
4097        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4098                die_error(400, "Unknown order parameter");
4099        }
4100
4101        my @list = git_get_projects_list();
4102        if (!@list) {
4103                die_error(404, "No projects found");
4104        }
4105
4106        git_header_html();
4107        if (-f $home_text) {
4108                print "<div class=\"index_include\">\n";
4109                open (my $fd, $home_text);
4110                print <$fd>;
4111                close $fd;
4112                print "</div>\n";
4113        }
4114        print $cgi->startform(-method => "get") .
4115              "<p class=\"projsearch\">Search:\n" .
4116              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4117              "</p>" .
4118              $cgi->end_form() . "\n";
4119        git_project_list_body(\@list, $order);
4120        git_footer_html();
4121}
4122
4123sub git_forks {
4124        my $order = $cgi->param('o');
4125        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4126                die_error(400, "Unknown order parameter");
4127        }
4128
4129        my @list = git_get_projects_list($project);
4130        if (!@list) {
4131                die_error(404, "No forks found");
4132        }
4133
4134        git_header_html();
4135        git_print_page_nav('','');
4136        git_print_header_div('summary', "$project forks");
4137        git_project_list_body(\@list, $order);
4138        git_footer_html();
4139}
4140
4141sub git_project_index {
4142        my @projects = git_get_projects_list($project);
4143
4144        print $cgi->header(
4145                -type => 'text/plain',
4146                -charset => 'utf-8',
4147                -content_disposition => 'inline; filename="index.aux"');
4148
4149        foreach my $pr (@projects) {
4150                if (!exists $pr->{'owner'}) {
4151                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4152                }
4153
4154                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4155                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4156                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4157                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4158                $path  =~ s/ /\+/g;
4159                $owner =~ s/ /\+/g;
4160
4161                print "$path $owner\n";
4162        }
4163}
4164
4165sub git_summary {
4166        my $descr = git_get_project_description($project) || "none";
4167        my %co = parse_commit("HEAD");
4168        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4169        my $head = $co{'id'};
4170
4171        my $owner = git_get_project_owner($project);
4172
4173        my $refs = git_get_references();
4174        # These get_*_list functions return one more to allow us to see if
4175        # there are more ...
4176        my @taglist  = git_get_tags_list(16);
4177        my @headlist = git_get_heads_list(16);
4178        my @forklist;
4179        my ($check_forks) = gitweb_check_feature('forks');
4180
4181        if ($check_forks) {
4182                @forklist = git_get_projects_list($project);
4183        }
4184
4185        git_header_html();
4186        git_print_page_nav('summary','', $head);
4187
4188        print "<div class=\"title\">&nbsp;</div>\n";
4189        print "<table class=\"projects_list\">\n" .
4190              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4191              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4192        if (defined $cd{'rfc2822'}) {
4193                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4194        }
4195
4196        # use per project git URL list in $projectroot/$project/cloneurl
4197        # or make project git URL from git base URL and project name
4198        my $url_tag = "URL";
4199        my @url_list = git_get_project_url_list($project);
4200        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4201        foreach my $git_url (@url_list) {
4202                next unless $git_url;
4203                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4204                $url_tag = "";
4205        }
4206
4207        # Tag cloud
4208        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4209        if ($show_ctags) {
4210                my $ctags = git_get_project_ctags($project);
4211                my $cloud = git_populate_project_tagcloud($ctags);
4212                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4213                print "</td>\n<td>" unless %$ctags;
4214                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4215                print "</td>\n<td>" if %$ctags;
4216                print git_show_project_tagcloud($cloud, 48);
4217                print "</td></tr>";
4218        }
4219
4220        print "</table>\n";
4221
4222        if (-s "$projectroot/$project/README.html") {
4223                if (open my $fd, "$projectroot/$project/README.html") {
4224                        print "<div class=\"title\">readme</div>\n" .
4225                              "<div class=\"readme\">\n";
4226                        print $_ while (<$fd>);
4227                        print "\n</div>\n"; # class="readme"
4228                        close $fd;
4229                }
4230        }
4231
4232        # we need to request one more than 16 (0..15) to check if
4233        # those 16 are all
4234        my @commitlist = $head ? parse_commits($head, 17) : ();
4235        if (@commitlist) {
4236                git_print_header_div('shortlog');
4237                git_shortlog_body(\@commitlist, 0, 15, $refs,
4238                                  $#commitlist <=  15 ? undef :
4239                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4240        }
4241
4242        if (@taglist) {
4243                git_print_header_div('tags');
4244                git_tags_body(\@taglist, 0, 15,
4245                              $#taglist <=  15 ? undef :
4246                              $cgi->a({-href => href(action=>"tags")}, "..."));
4247        }
4248
4249        if (@headlist) {
4250                git_print_header_div('heads');
4251                git_heads_body(\@headlist, $head, 0, 15,
4252                               $#headlist <= 15 ? undef :
4253                               $cgi->a({-href => href(action=>"heads")}, "..."));
4254        }
4255
4256        if (@forklist) {
4257                git_print_header_div('forks');
4258                git_project_list_body(\@forklist, undef, 0, 15,
4259                                      $#forklist <= 15 ? undef :
4260                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4261                                      'noheader');
4262        }
4263
4264        git_footer_html();
4265}
4266
4267sub git_tag {
4268        my $head = git_get_head_hash($project);
4269        git_header_html();
4270        git_print_page_nav('','', $head,undef,$head);
4271        my %tag = parse_tag($hash);
4272
4273        if (! %tag) {
4274                die_error(404, "Unknown tag object");
4275        }
4276
4277        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4278        print "<div class=\"title_text\">\n" .
4279              "<table class=\"object_header\">\n" .
4280              "<tr>\n" .
4281              "<td>object</td>\n" .
4282              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4283                               $tag{'object'}) . "</td>\n" .
4284              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4285                                              $tag{'type'}) . "</td>\n" .
4286              "</tr>\n";
4287        if (defined($tag{'author'})) {
4288                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4289                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4290                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4291                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4292                        "</td></tr>\n";
4293        }
4294        print "</table>\n\n" .
4295              "</div>\n";
4296        print "<div class=\"page_body\">";
4297        my $comment = $tag{'comment'};
4298        foreach my $line (@$comment) {
4299                chomp $line;
4300                print esc_html($line, -nbsp=>1) . "<br/>\n";
4301        }
4302        print "</div>\n";
4303        git_footer_html();
4304}
4305
4306sub git_blame {
4307        my $fd;
4308        my $ftype;
4309
4310        gitweb_check_feature('blame')
4311            or die_error(403, "Blame view not allowed");
4312
4313        die_error(400, "No file name given") unless $file_name;
4314        $hash_base ||= git_get_head_hash($project);
4315        die_error(404, "Couldn't find base commit") unless ($hash_base);
4316        my %co = parse_commit($hash_base)
4317                or die_error(404, "Commit not found");
4318        if (!defined $hash) {
4319                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4320                        or die_error(404, "Error looking up file");
4321        }
4322        $ftype = git_get_type($hash);
4323        if ($ftype !~ "blob") {
4324                die_error(400, "Object is not a blob");
4325        }
4326        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4327              $file_name, $hash_base)
4328                or die_error(500, "Open git-blame failed");
4329        git_header_html();
4330        my $formats_nav =
4331                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4332                        "blob") .
4333                " | " .
4334                $cgi->a({-href => href(action=>"history", -replay=>1)},
4335                        "history") .
4336                " | " .
4337                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4338                        "HEAD");
4339        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4340        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4341        git_print_page_path($file_name, $ftype, $hash_base);
4342        my @rev_color = (qw(light2 dark2));
4343        my $num_colors = scalar(@rev_color);
4344        my $current_color = 0;
4345        my $last_rev;
4346        print <<HTML;
4347<div class="page_body">
4348<table class="blame">
4349<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4350HTML
4351        my %metainfo = ();
4352        while (1) {
4353                $_ = <$fd>;
4354                last unless defined $_;
4355                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4356                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4357                if (!exists $metainfo{$full_rev}) {
4358                        $metainfo{$full_rev} = {};
4359                }
4360                my $meta = $metainfo{$full_rev};
4361                while (<$fd>) {
4362                        last if (s/^\t//);
4363                        if (/^(\S+) (.*)$/) {
4364                                $meta->{$1} = $2;
4365                        }
4366                }
4367                my $data = $_;
4368                chomp $data;
4369                my $rev = substr($full_rev, 0, 8);
4370                my $author = $meta->{'author'};
4371                my %date = parse_date($meta->{'author-time'},
4372                                      $meta->{'author-tz'});
4373                my $date = $date{'iso-tz'};
4374                if ($group_size) {
4375                        $current_color = ++$current_color % $num_colors;
4376                }
4377                print "<tr class=\"$rev_color[$current_color]\">\n";
4378                if ($group_size) {
4379                        print "<td class=\"sha1\"";
4380                        print " title=\"". esc_html($author) . ", $date\"";
4381                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4382                        print ">";
4383                        print $cgi->a({-href => href(action=>"commit",
4384                                                     hash=>$full_rev,
4385                                                     file_name=>$file_name)},
4386                                      esc_html($rev));
4387                        print "</td>\n";
4388                }
4389                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4390                        or die_error(500, "Open git-rev-parse failed");
4391                my $parent_commit = <$dd>;
4392                close $dd;
4393                chomp($parent_commit);
4394                my $blamed = href(action => 'blame',
4395                                  file_name => $meta->{'filename'},
4396                                  hash_base => $parent_commit);
4397                print "<td class=\"linenr\">";
4398                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4399                                -id => "l$lineno",
4400                                -class => "linenr" },
4401                              esc_html($lineno));
4402                print "</td>";
4403                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4404                print "</tr>\n";
4405        }
4406        print "</table>\n";
4407        print "</div>";
4408        close $fd
4409                or print "Reading blob failed\n";
4410        git_footer_html();
4411}
4412
4413sub git_tags {
4414        my $head = git_get_head_hash($project);
4415        git_header_html();
4416        git_print_page_nav('','', $head,undef,$head);
4417        git_print_header_div('summary', $project);
4418
4419        my @tagslist = git_get_tags_list();
4420        if (@tagslist) {
4421                git_tags_body(\@tagslist);
4422        }
4423        git_footer_html();
4424}
4425
4426sub git_heads {
4427        my $head = git_get_head_hash($project);
4428        git_header_html();
4429        git_print_page_nav('','', $head,undef,$head);
4430        git_print_header_div('summary', $project);
4431
4432        my @headslist = git_get_heads_list();
4433        if (@headslist) {
4434                git_heads_body(\@headslist, $head);
4435        }
4436        git_footer_html();
4437}
4438
4439sub git_blob_plain {
4440        my $type = shift;
4441        my $expires;
4442
4443        if (!defined $hash) {
4444                if (defined $file_name) {
4445                        my $base = $hash_base || git_get_head_hash($project);
4446                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4447                                or die_error(404, "Cannot find file");
4448                } else {
4449                        die_error(400, "No file name defined");
4450                }
4451        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4452                # blobs defined by non-textual hash id's can be cached
4453                $expires = "+1d";
4454        }
4455
4456        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4457                or die_error(500, "Open git-cat-file blob '$hash' failed");
4458
4459        # content-type (can include charset)
4460        $type = blob_contenttype($fd, $file_name, $type);
4461
4462        # "save as" filename, even when no $file_name is given
4463        my $save_as = "$hash";
4464        if (defined $file_name) {
4465                $save_as = $file_name;
4466        } elsif ($type =~ m/^text\//) {
4467                $save_as .= '.txt';
4468        }
4469
4470        print $cgi->header(
4471                -type => $type,
4472                -expires => $expires,
4473                -content_disposition => 'inline; filename="' . $save_as . '"');
4474        undef $/;
4475        binmode STDOUT, ':raw';
4476        print <$fd>;
4477        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4478        $/ = "\n";
4479        close $fd;
4480}
4481
4482sub git_blob {
4483        my $expires;
4484
4485        if (!defined $hash) {
4486                if (defined $file_name) {
4487                        my $base = $hash_base || git_get_head_hash($project);
4488                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4489                                or die_error(404, "Cannot find file");
4490                } else {
4491                        die_error(400, "No file name defined");
4492                }
4493        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4494                # blobs defined by non-textual hash id's can be cached
4495                $expires = "+1d";
4496        }
4497
4498        my ($have_blame) = gitweb_check_feature('blame');
4499        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4500                or die_error(500, "Couldn't cat $file_name, $hash");
4501        my $mimetype = blob_mimetype($fd, $file_name);
4502        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4503                close $fd;
4504                return git_blob_plain($mimetype);
4505        }
4506        # we can have blame only for text/* mimetype
4507        $have_blame &&= ($mimetype =~ m!^text/!);
4508
4509        git_header_html(undef, $expires);
4510        my $formats_nav = '';
4511        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4512                if (defined $file_name) {
4513                        if ($have_blame) {
4514                                $formats_nav .=
4515                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4516                                                "blame") .
4517                                        " | ";
4518                        }
4519                        $formats_nav .=
4520                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4521                                        "history") .
4522                                " | " .
4523                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4524                                        "raw") .
4525                                " | " .
4526                                $cgi->a({-href => href(action=>"blob",
4527                                                       hash_base=>"HEAD", file_name=>$file_name)},
4528                                        "HEAD");
4529                } else {
4530                        $formats_nav .=
4531                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4532                                        "raw");
4533                }
4534                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4535                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4536        } else {
4537                print "<div class=\"page_nav\">\n" .
4538                      "<br/><br/></div>\n" .
4539                      "<div class=\"title\">$hash</div>\n";
4540        }
4541        git_print_page_path($file_name, "blob", $hash_base);
4542        print "<div class=\"page_body\">\n";
4543        if ($mimetype =~ m!^image/!) {
4544                print qq!<img type="$mimetype"!;
4545                if ($file_name) {
4546                        print qq! alt="$file_name" title="$file_name"!;
4547                }
4548                print qq! src="! .
4549                      href(action=>"blob_plain", hash=>$hash,
4550                           hash_base=>$hash_base, file_name=>$file_name) .
4551                      qq!" />\n!;
4552        } else {
4553                my $nr;
4554                while (my $line = <$fd>) {
4555                        chomp $line;
4556                        $nr++;
4557                        $line = untabify($line);
4558                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4559                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4560                }
4561        }
4562        close $fd
4563                or print "Reading blob failed.\n";
4564        print "</div>";
4565        git_footer_html();
4566}
4567
4568sub git_tree {
4569        if (!defined $hash_base) {
4570                $hash_base = "HEAD";
4571        }
4572        if (!defined $hash) {
4573                if (defined $file_name) {
4574                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4575                } else {
4576                        $hash = $hash_base;
4577                }
4578        }
4579        die_error(404, "No such tree") unless defined($hash);
4580        $/ = "\0";
4581        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4582                or die_error(500, "Open git-ls-tree failed");
4583        my @entries = map { chomp; $_ } <$fd>;
4584        close $fd or die_error(404, "Reading tree failed");
4585        $/ = "\n";
4586
4587        my $refs = git_get_references();
4588        my $ref = format_ref_marker($refs, $hash_base);
4589        git_header_html();
4590        my $basedir = '';
4591        my ($have_blame) = gitweb_check_feature('blame');
4592        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4593                my @views_nav = ();
4594                if (defined $file_name) {
4595                        push @views_nav,
4596                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4597                                        "history"),
4598                                $cgi->a({-href => href(action=>"tree",
4599                                                       hash_base=>"HEAD", file_name=>$file_name)},
4600                                        "HEAD"),
4601                }
4602                my $snapshot_links = format_snapshot_links($hash);
4603                if (defined $snapshot_links) {
4604                        # FIXME: Should be available when we have no hash base as well.
4605                        push @views_nav, $snapshot_links;
4606                }
4607                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4608                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4609        } else {
4610                undef $hash_base;
4611                print "<div class=\"page_nav\">\n";
4612                print "<br/><br/></div>\n";
4613                print "<div class=\"title\">$hash</div>\n";
4614        }
4615        if (defined $file_name) {
4616                $basedir = $file_name;
4617                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4618                        $basedir .= '/';
4619                }
4620                git_print_page_path($file_name, 'tree', $hash_base);
4621        }
4622        print "<div class=\"page_body\">\n";
4623        print "<table class=\"tree\">\n";
4624        my $alternate = 1;
4625        # '..' (top directory) link if possible
4626        if (defined $hash_base &&
4627            defined $file_name && $file_name =~ m![^/]+$!) {
4628                if ($alternate) {
4629                        print "<tr class=\"dark\">\n";
4630                } else {
4631                        print "<tr class=\"light\">\n";
4632                }
4633                $alternate ^= 1;
4634
4635                my $up = $file_name;
4636                $up =~ s!/?[^/]+$!!;
4637                undef $up unless $up;
4638                # based on git_print_tree_entry
4639                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4640                print '<td class="list">';
4641                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4642                                             file_name=>$up)},
4643                              "..");
4644                print "</td>\n";
4645                print "<td class=\"link\"></td>\n";
4646
4647                print "</tr>\n";
4648        }
4649        foreach my $line (@entries) {
4650                my %t = parse_ls_tree_line($line, -z => 1);
4651
4652                if ($alternate) {
4653                        print "<tr class=\"dark\">\n";
4654                } else {
4655                        print "<tr class=\"light\">\n";
4656                }
4657                $alternate ^= 1;
4658
4659                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4660
4661                print "</tr>\n";
4662        }
4663        print "</table>\n" .
4664              "</div>";
4665        git_footer_html();
4666}
4667
4668sub git_snapshot {
4669        my @supported_fmts = gitweb_check_feature('snapshot');
4670        @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4671
4672        my $format = $cgi->param('sf');
4673        if (!@supported_fmts) {
4674                die_error(403, "Snapshots not allowed");
4675        }
4676        # default to first supported snapshot format
4677        $format ||= $supported_fmts[0];
4678        if ($format !~ m/^[a-z0-9]+$/) {
4679                die_error(400, "Invalid snapshot format parameter");
4680        } elsif (!exists($known_snapshot_formats{$format})) {
4681                die_error(400, "Unknown snapshot format");
4682        } elsif (!grep($_ eq $format, @supported_fmts)) {
4683                die_error(403, "Unsupported snapshot format");
4684        }
4685
4686        if (!defined $hash) {
4687                $hash = git_get_head_hash($project);
4688        }
4689
4690        my $name = $project;
4691        $name =~ s,([^/])/*\.git$,$1,;
4692        $name = basename($name);
4693        my $filename = to_utf8($name);
4694        $name =~ s/\047/\047\\\047\047/g;
4695        my $cmd;
4696        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4697        $cmd = quote_command(
4698                git_cmd(), 'archive',
4699                "--format=$known_snapshot_formats{$format}{'format'}",
4700                "--prefix=$name/", $hash);
4701        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4702                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4703        }
4704
4705        print $cgi->header(
4706                -type => $known_snapshot_formats{$format}{'type'},
4707                -content_disposition => 'inline; filename="' . "$filename" . '"',
4708                -status => '200 OK');
4709
4710        open my $fd, "-|", $cmd
4711                or die_error(500, "Execute git-archive failed");
4712        binmode STDOUT, ':raw';
4713        print <$fd>;
4714        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4715        close $fd;
4716}
4717
4718sub git_log {
4719        my $head = git_get_head_hash($project);
4720        if (!defined $hash) {
4721                $hash = $head;
4722        }
4723        if (!defined $page) {
4724                $page = 0;
4725        }
4726        my $refs = git_get_references();
4727
4728        my @commitlist = parse_commits($hash, 101, (100 * $page));
4729
4730        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4731
4732        git_header_html();
4733        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4734
4735        if (!@commitlist) {
4736                my %co = parse_commit($hash);
4737
4738                git_print_header_div('summary', $project);
4739                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4740        }
4741        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4742        for (my $i = 0; $i <= $to; $i++) {
4743                my %co = %{$commitlist[$i]};
4744                next if !%co;
4745                my $commit = $co{'id'};
4746                my $ref = format_ref_marker($refs, $commit);
4747                my %ad = parse_date($co{'author_epoch'});
4748                git_print_header_div('commit',
4749                               "<span class=\"age\">$co{'age_string'}</span>" .
4750                               esc_html($co{'title'}) . $ref,
4751                               $commit);
4752                print "<div class=\"title_text\">\n" .
4753                      "<div class=\"log_link\">\n" .
4754                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4755                      " | " .
4756                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4757                      " | " .
4758                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4759                      "<br/>\n" .
4760                      "</div>\n" .
4761                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4762                      "</div>\n";
4763
4764                print "<div class=\"log_body\">\n";
4765                git_print_log($co{'comment'}, -final_empty_line=> 1);
4766                print "</div>\n";
4767        }
4768        if ($#commitlist >= 100) {
4769                print "<div class=\"page_nav\">\n";
4770                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4771                               -accesskey => "n", -title => "Alt-n"}, "next");
4772                print "</div>\n";
4773        }
4774        git_footer_html();
4775}
4776
4777sub git_commit {
4778        $hash ||= $hash_base || "HEAD";
4779        my %co = parse_commit($hash)
4780            or die_error(404, "Unknown commit object");
4781        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4782        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4783
4784        my $parent  = $co{'parent'};
4785        my $parents = $co{'parents'}; # listref
4786
4787        # we need to prepare $formats_nav before any parameter munging
4788        my $formats_nav;
4789        if (!defined $parent) {
4790                # --root commitdiff
4791                $formats_nav .= '(initial)';
4792        } elsif (@$parents == 1) {
4793                # single parent commit
4794                $formats_nav .=
4795                        '(parent: ' .
4796                        $cgi->a({-href => href(action=>"commit",
4797                                               hash=>$parent)},
4798                                esc_html(substr($parent, 0, 7))) .
4799                        ')';
4800        } else {
4801                # merge commit
4802                $formats_nav .=
4803                        '(merge: ' .
4804                        join(' ', map {
4805                                $cgi->a({-href => href(action=>"commit",
4806                                                       hash=>$_)},
4807                                        esc_html(substr($_, 0, 7)));
4808                        } @$parents ) .
4809                        ')';
4810        }
4811
4812        if (!defined $parent) {
4813                $parent = "--root";
4814        }
4815        my @difftree;
4816        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4817                @diff_opts,
4818                (@$parents <= 1 ? $parent : '-c'),
4819                $hash, "--"
4820                or die_error(500, "Open git-diff-tree failed");
4821        @difftree = map { chomp; $_ } <$fd>;
4822        close $fd or die_error(404, "Reading git-diff-tree failed");
4823
4824        # non-textual hash id's can be cached
4825        my $expires;
4826        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4827                $expires = "+1d";
4828        }
4829        my $refs = git_get_references();
4830        my $ref = format_ref_marker($refs, $co{'id'});
4831
4832        git_header_html(undef, $expires);
4833        git_print_page_nav('commit', '',
4834                           $hash, $co{'tree'}, $hash,
4835                           $formats_nav);
4836
4837        if (defined $co{'parent'}) {
4838                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4839        } else {
4840                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4841        }
4842        print "<div class=\"title_text\">\n" .
4843              "<table class=\"object_header\">\n";
4844        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4845              "<tr>" .
4846              "<td></td><td> $ad{'rfc2822'}";
4847        if ($ad{'hour_local'} < 6) {
4848                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4849                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4850        } else {
4851                printf(" (%02d:%02d %s)",
4852                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4853        }
4854        print "</td>" .
4855              "</tr>\n";
4856        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4857        print "<tr><td></td><td> $cd{'rfc2822'}" .
4858              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4859              "</td></tr>\n";
4860        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4861        print "<tr>" .
4862              "<td>tree</td>" .
4863              "<td class=\"sha1\">" .
4864              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4865                       class => "list"}, $co{'tree'}) .
4866              "</td>" .
4867              "<td class=\"link\">" .
4868              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4869                      "tree");
4870        my $snapshot_links = format_snapshot_links($hash);
4871        if (defined $snapshot_links) {
4872                print " | " . $snapshot_links;
4873        }
4874        print "</td>" .
4875              "</tr>\n";
4876
4877        foreach my $par (@$parents) {
4878                print "<tr>" .
4879                      "<td>parent</td>" .
4880                      "<td class=\"sha1\">" .
4881                      $cgi->a({-href => href(action=>"commit", hash=>$par),
4882                               class => "list"}, $par) .
4883                      "</td>" .
4884                      "<td class=\"link\">" .
4885                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4886                      " | " .
4887                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4888                      "</td>" .
4889                      "</tr>\n";
4890        }
4891        print "</table>".
4892              "</div>\n";
4893
4894        print "<div class=\"page_body\">\n";
4895        git_print_log($co{'comment'});
4896        print "</div>\n";
4897
4898        git_difftree_body(\@difftree, $hash, @$parents);
4899
4900        git_footer_html();
4901}
4902
4903sub git_object {
4904        # object is defined by:
4905        # - hash or hash_base alone
4906        # - hash_base and file_name
4907        my $type;
4908
4909        # - hash or hash_base alone
4910        if ($hash || ($hash_base && !defined $file_name)) {
4911                my $object_id = $hash || $hash_base;
4912
4913                open my $fd, "-|", quote_command(
4914                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4915                        or die_error(404, "Object does not exist");
4916                $type = <$fd>;
4917                chomp $type;
4918                close $fd
4919                        or die_error(404, "Object does not exist");
4920
4921        # - hash_base and file_name
4922        } elsif ($hash_base && defined $file_name) {
4923                $file_name =~ s,/+$,,;
4924
4925                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4926                        or die_error(404, "Base object does not exist");
4927
4928                # here errors should not hapen
4929                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4930                        or die_error(500, "Open git-ls-tree failed");
4931                my $line = <$fd>;
4932                close $fd;
4933
4934                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4935                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4936                        die_error(404, "File or directory for given base does not exist");
4937                }
4938                $type = $2;
4939                $hash = $3;
4940        } else {
4941                die_error(400, "Not enough information to find object");
4942        }
4943
4944        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4945                                          hash=>$hash, hash_base=>$hash_base,
4946                                          file_name=>$file_name),
4947                             -status => '302 Found');
4948}
4949
4950sub git_blobdiff {
4951        my $format = shift || 'html';
4952
4953        my $fd;
4954        my @difftree;
4955        my %diffinfo;
4956        my $expires;
4957
4958        # preparing $fd and %diffinfo for git_patchset_body
4959        # new style URI
4960        if (defined $hash_base && defined $hash_parent_base) {
4961                if (defined $file_name) {
4962                        # read raw output
4963                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4964                                $hash_parent_base, $hash_base,
4965                                "--", (defined $file_parent ? $file_parent : ()), $file_name
4966                                or die_error(500, "Open git-diff-tree failed");
4967                        @difftree = map { chomp; $_ } <$fd>;
4968                        close $fd
4969                                or die_error(404, "Reading git-diff-tree failed");
4970                        @difftree
4971                                or die_error(404, "Blob diff not found");
4972
4973                } elsif (defined $hash &&
4974                         $hash =~ /[0-9a-fA-F]{40}/) {
4975                        # try to find filename from $hash
4976
4977                        # read filtered raw output
4978                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4979                                $hash_parent_base, $hash_base, "--"
4980                                or die_error(500, "Open git-diff-tree failed");
4981                        @difftree =
4982                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4983                                # $hash == to_id
4984                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4985                                map { chomp; $_ } <$fd>;
4986                        close $fd
4987                                or die_error(404, "Reading git-diff-tree failed");
4988                        @difftree
4989                                or die_error(404, "Blob diff not found");
4990
4991                } else {
4992                        die_error(400, "Missing one of the blob diff parameters");
4993                }
4994
4995                if (@difftree > 1) {
4996                        die_error(400, "Ambiguous blob diff specification");
4997                }
4998
4999                %diffinfo = parse_difftree_raw_line($difftree[0]);
5000                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5001                $file_name   ||= $diffinfo{'to_file'};
5002
5003                $hash_parent ||= $diffinfo{'from_id'};
5004                $hash        ||= $diffinfo{'to_id'};
5005
5006                # non-textual hash id's can be cached
5007                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5008                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5009                        $expires = '+1d';
5010                }
5011
5012                # open patch output
5013                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5014                        '-p', ($format eq 'html' ? "--full-index" : ()),
5015                        $hash_parent_base, $hash_base,
5016                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5017                        or die_error(500, "Open git-diff-tree failed");
5018        }
5019
5020        # old/legacy style URI
5021        if (!%diffinfo && # if new style URI failed
5022            defined $hash && defined $hash_parent) {
5023                # fake git-diff-tree raw output
5024                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5025                $diffinfo{'from_id'} = $hash_parent;
5026                $diffinfo{'to_id'}   = $hash;
5027                if (defined $file_name) {
5028                        if (defined $file_parent) {
5029                                $diffinfo{'status'} = '2';
5030                                $diffinfo{'from_file'} = $file_parent;
5031                                $diffinfo{'to_file'}   = $file_name;
5032                        } else { # assume not renamed
5033                                $diffinfo{'status'} = '1';
5034                                $diffinfo{'from_file'} = $file_name;
5035                                $diffinfo{'to_file'}   = $file_name;
5036                        }
5037                } else { # no filename given
5038                        $diffinfo{'status'} = '2';
5039                        $diffinfo{'from_file'} = $hash_parent;
5040                        $diffinfo{'to_file'}   = $hash;
5041                }
5042
5043                # non-textual hash id's can be cached
5044                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5045                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5046                        $expires = '+1d';
5047                }
5048
5049                # open patch output
5050                open $fd, "-|", git_cmd(), "diff", @diff_opts,
5051                        '-p', ($format eq 'html' ? "--full-index" : ()),
5052                        $hash_parent, $hash, "--"
5053                        or die_error(500, "Open git-diff failed");
5054        } else  {
5055                die_error(400, "Missing one of the blob diff parameters")
5056                        unless %diffinfo;
5057        }
5058
5059        # header
5060        if ($format eq 'html') {
5061                my $formats_nav =
5062                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5063                                "raw");
5064                git_header_html(undef, $expires);
5065                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5066                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5067                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5068                } else {
5069                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5070                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5071                }
5072                if (defined $file_name) {
5073                        git_print_page_path($file_name, "blob", $hash_base);
5074                } else {
5075                        print "<div class=\"page_path\"></div>\n";
5076                }
5077
5078        } elsif ($format eq 'plain') {
5079                print $cgi->header(
5080                        -type => 'text/plain',
5081                        -charset => 'utf-8',
5082                        -expires => $expires,
5083                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5084
5085                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5086
5087        } else {
5088                die_error(400, "Unknown blobdiff format");
5089        }
5090
5091        # patch
5092        if ($format eq 'html') {
5093                print "<div class=\"page_body\">\n";
5094
5095                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5096                close $fd;
5097
5098                print "</div>\n"; # class="page_body"
5099                git_footer_html();
5100
5101        } else {
5102                while (my $line = <$fd>) {
5103                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5104                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5105
5106                        print $line;
5107
5108                        last if $line =~ m!^\+\+\+!;
5109                }
5110                local $/ = undef;
5111                print <$fd>;
5112                close $fd;
5113        }
5114}
5115
5116sub git_blobdiff_plain {
5117        git_blobdiff('plain');
5118}
5119
5120sub git_commitdiff {
5121        my $format = shift || 'html';
5122        $hash ||= $hash_base || "HEAD";
5123        my %co = parse_commit($hash)
5124            or die_error(404, "Unknown commit object");
5125
5126        # choose format for commitdiff for merge
5127        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5128                $hash_parent = '--cc';
5129        }
5130        # we need to prepare $formats_nav before almost any parameter munging
5131        my $formats_nav;
5132        if ($format eq 'html') {
5133                $formats_nav =
5134                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5135                                "raw");
5136
5137                if (defined $hash_parent &&
5138                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5139                        # commitdiff with two commits given
5140                        my $hash_parent_short = $hash_parent;
5141                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5142                                $hash_parent_short = substr($hash_parent, 0, 7);
5143                        }
5144                        $formats_nav .=
5145                                ' (from';
5146                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5147                                if ($co{'parents'}[$i] eq $hash_parent) {
5148                                        $formats_nav .= ' parent ' . ($i+1);
5149                                        last;
5150                                }
5151                        }
5152                        $formats_nav .= ': ' .
5153                                $cgi->a({-href => href(action=>"commitdiff",
5154                                                       hash=>$hash_parent)},
5155                                        esc_html($hash_parent_short)) .
5156                                ')';
5157                } elsif (!$co{'parent'}) {
5158                        # --root commitdiff
5159                        $formats_nav .= ' (initial)';
5160                } elsif (scalar @{$co{'parents'}} == 1) {
5161                        # single parent commit
5162                        $formats_nav .=
5163                                ' (parent: ' .
5164                                $cgi->a({-href => href(action=>"commitdiff",
5165                                                       hash=>$co{'parent'})},
5166                                        esc_html(substr($co{'parent'}, 0, 7))) .
5167                                ')';
5168                } else {
5169                        # merge commit
5170                        if ($hash_parent eq '--cc') {
5171                                $formats_nav .= ' | ' .
5172                                        $cgi->a({-href => href(action=>"commitdiff",
5173                                                               hash=>$hash, hash_parent=>'-c')},
5174                                                'combined');
5175                        } else { # $hash_parent eq '-c'
5176                                $formats_nav .= ' | ' .
5177                                        $cgi->a({-href => href(action=>"commitdiff",
5178                                                               hash=>$hash, hash_parent=>'--cc')},
5179                                                'compact');
5180                        }
5181                        $formats_nav .=
5182                                ' (merge: ' .
5183                                join(' ', map {
5184                                        $cgi->a({-href => href(action=>"commitdiff",
5185                                                               hash=>$_)},
5186                                                esc_html(substr($_, 0, 7)));
5187                                } @{$co{'parents'}} ) .
5188                                ')';
5189                }
5190        }
5191
5192        my $hash_parent_param = $hash_parent;
5193        if (!defined $hash_parent_param) {
5194                # --cc for multiple parents, --root for parentless
5195                $hash_parent_param =
5196                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5197        }
5198
5199        # read commitdiff
5200        my $fd;
5201        my @difftree;
5202        if ($format eq 'html') {
5203                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5204                        "--no-commit-id", "--patch-with-raw", "--full-index",
5205                        $hash_parent_param, $hash, "--"
5206                        or die_error(500, "Open git-diff-tree failed");
5207
5208                while (my $line = <$fd>) {
5209                        chomp $line;
5210                        # empty line ends raw part of diff-tree output
5211                        last unless $line;
5212                        push @difftree, scalar parse_difftree_raw_line($line);
5213                }
5214
5215        } elsif ($format eq 'plain') {
5216                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5217                        '-p', $hash_parent_param, $hash, "--"
5218                        or die_error(500, "Open git-diff-tree failed");
5219
5220        } else {
5221                die_error(400, "Unknown commitdiff format");
5222        }
5223
5224        # non-textual hash id's can be cached
5225        my $expires;
5226        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5227                $expires = "+1d";
5228        }
5229
5230        # write commit message
5231        if ($format eq 'html') {
5232                my $refs = git_get_references();
5233                my $ref = format_ref_marker($refs, $co{'id'});
5234
5235                git_header_html(undef, $expires);
5236                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5237                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5238                git_print_authorship(\%co);
5239                print "<div class=\"page_body\">\n";
5240                if (@{$co{'comment'}} > 1) {
5241                        print "<div class=\"log\">\n";
5242                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5243                        print "</div>\n"; # class="log"
5244                }
5245
5246        } elsif ($format eq 'plain') {
5247                my $refs = git_get_references("tags");
5248                my $tagname = git_get_rev_name_tags($hash);
5249                my $filename = basename($project) . "-$hash.patch";
5250
5251                print $cgi->header(
5252                        -type => 'text/plain',
5253                        -charset => 'utf-8',
5254                        -expires => $expires,
5255                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5256                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5257                print "From: " . to_utf8($co{'author'}) . "\n";
5258                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5259                print "Subject: " . to_utf8($co{'title'}) . "\n";
5260
5261                print "X-Git-Tag: $tagname\n" if $tagname;
5262                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5263
5264                foreach my $line (@{$co{'comment'}}) {
5265                        print to_utf8($line) . "\n";
5266                }
5267                print "---\n\n";
5268        }
5269
5270        # write patch
5271        if ($format eq 'html') {
5272                my $use_parents = !defined $hash_parent ||
5273                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5274                git_difftree_body(\@difftree, $hash,
5275                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5276                print "<br/>\n";
5277
5278                git_patchset_body($fd, \@difftree, $hash,
5279                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5280                close $fd;
5281                print "</div>\n"; # class="page_body"
5282                git_footer_html();
5283
5284        } elsif ($format eq 'plain') {
5285                local $/ = undef;
5286                print <$fd>;
5287                close $fd
5288                        or print "Reading git-diff-tree failed\n";
5289        }
5290}
5291
5292sub git_commitdiff_plain {
5293        git_commitdiff('plain');
5294}
5295
5296sub git_history {
5297        if (!defined $hash_base) {
5298                $hash_base = git_get_head_hash($project);
5299        }
5300        if (!defined $page) {
5301                $page = 0;
5302        }
5303        my $ftype;
5304        my %co = parse_commit($hash_base)
5305            or die_error(404, "Unknown commit object");
5306
5307        my $refs = git_get_references();
5308        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5309
5310        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5311                                       $file_name, "--full-history")
5312            or die_error(404, "No such file or directory on given branch");
5313
5314        if (!defined $hash && defined $file_name) {
5315                # some commits could have deleted file in question,
5316                # and not have it in tree, but one of them has to have it
5317                for (my $i = 0; $i <= @commitlist; $i++) {
5318                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5319                        last if defined $hash;
5320                }
5321        }
5322        if (defined $hash) {
5323                $ftype = git_get_type($hash);
5324        }
5325        if (!defined $ftype) {
5326                die_error(500, "Unknown type of object");
5327        }
5328
5329        my $paging_nav = '';
5330        if ($page > 0) {
5331                $paging_nav .=
5332                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5333                                               file_name=>$file_name)},
5334                                "first");
5335                $paging_nav .= " &sdot; " .
5336                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5337                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5338        } else {
5339                $paging_nav .= "first";
5340                $paging_nav .= " &sdot; prev";
5341        }
5342        my $next_link = '';
5343        if ($#commitlist >= 100) {
5344                $next_link =
5345                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5346                                 -accesskey => "n", -title => "Alt-n"}, "next");
5347                $paging_nav .= " &sdot; $next_link";
5348        } else {
5349                $paging_nav .= " &sdot; next";
5350        }
5351
5352        git_header_html();
5353        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5354        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5355        git_print_page_path($file_name, $ftype, $hash_base);
5356
5357        git_history_body(\@commitlist, 0, 99,
5358                         $refs, $hash_base, $ftype, $next_link);
5359
5360        git_footer_html();
5361}
5362
5363sub git_search {
5364        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5365        if (!defined $searchtext) {
5366                die_error(400, "Text field is empty");
5367        }
5368        if (!defined $hash) {
5369                $hash = git_get_head_hash($project);
5370        }
5371        my %co = parse_commit($hash);
5372        if (!%co) {
5373                die_error(404, "Unknown commit object");
5374        }
5375        if (!defined $page) {
5376                $page = 0;
5377        }
5378
5379        $searchtype ||= 'commit';
5380        if ($searchtype eq 'pickaxe') {
5381                # pickaxe may take all resources of your box and run for several minutes
5382                # with every query - so decide by yourself how public you make this feature
5383                gitweb_check_feature('pickaxe')
5384                    or die_error(403, "Pickaxe is disabled");
5385        }
5386        if ($searchtype eq 'grep') {
5387                gitweb_check_feature('grep')
5388                    or die_error(403, "Grep is disabled");
5389        }
5390
5391        git_header_html();
5392
5393        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5394                my $greptype;
5395                if ($searchtype eq 'commit') {
5396                        $greptype = "--grep=";
5397                } elsif ($searchtype eq 'author') {
5398                        $greptype = "--author=";
5399                } elsif ($searchtype eq 'committer') {
5400                        $greptype = "--committer=";
5401                }
5402                $greptype .= $searchtext;
5403                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5404                                               $greptype, '--regexp-ignore-case',
5405                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5406
5407                my $paging_nav = '';
5408                if ($page > 0) {
5409                        $paging_nav .=
5410                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5411                                                       searchtext=>$searchtext,
5412                                                       searchtype=>$searchtype)},
5413                                        "first");
5414                        $paging_nav .= " &sdot; " .
5415                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5416                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5417                } else {
5418                        $paging_nav .= "first";
5419                        $paging_nav .= " &sdot; prev";
5420                }
5421                my $next_link = '';
5422                if ($#commitlist >= 100) {
5423                        $next_link =
5424                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5425                                         -accesskey => "n", -title => "Alt-n"}, "next");
5426                        $paging_nav .= " &sdot; $next_link";
5427                } else {
5428                        $paging_nav .= " &sdot; next";
5429                }
5430
5431                if ($#commitlist >= 100) {
5432                }
5433
5434                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5435                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5436                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5437        }
5438
5439        if ($searchtype eq 'pickaxe') {
5440                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5441                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5442
5443                print "<table class=\"pickaxe search\">\n";
5444                my $alternate = 1;
5445                $/ = "\n";
5446                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5447                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5448                        ($search_use_regexp ? '--pickaxe-regex' : ());
5449                undef %co;
5450                my @files;
5451                while (my $line = <$fd>) {
5452                        chomp $line;
5453                        next unless $line;
5454
5455                        my %set = parse_difftree_raw_line($line);
5456                        if (defined $set{'commit'}) {
5457                                # finish previous commit
5458                                if (%co) {
5459                                        print "</td>\n" .
5460                                              "<td class=\"link\">" .
5461                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5462                                              " | " .
5463                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5464                                        print "</td>\n" .
5465                                              "</tr>\n";
5466                                }
5467
5468                                if ($alternate) {
5469                                        print "<tr class=\"dark\">\n";
5470                                } else {
5471                                        print "<tr class=\"light\">\n";
5472                                }
5473                                $alternate ^= 1;
5474                                %co = parse_commit($set{'commit'});
5475                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5476                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5477                                      "<td><i>$author</i></td>\n" .
5478                                      "<td>" .
5479                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5480                                              -class => "list subject"},
5481                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5482                        } elsif (defined $set{'to_id'}) {
5483                                next if ($set{'to_id'} =~ m/^0{40}$/);
5484
5485                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5486                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5487                                              -class => "list"},
5488                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5489                                      "<br/>\n";
5490                        }
5491                }
5492                close $fd;
5493
5494                # finish last commit (warning: repetition!)
5495                if (%co) {
5496                        print "</td>\n" .
5497                              "<td class=\"link\">" .
5498                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5499                              " | " .
5500                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5501                        print "</td>\n" .
5502                              "</tr>\n";
5503                }
5504
5505                print "</table>\n";
5506        }
5507
5508        if ($searchtype eq 'grep') {
5509                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5510                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5511
5512                print "<table class=\"grep_search\">\n";
5513                my $alternate = 1;
5514                my $matches = 0;
5515                $/ = "\n";
5516                open my $fd, "-|", git_cmd(), 'grep', '-n',
5517                        $search_use_regexp ? ('-E', '-i') : '-F',
5518                        $searchtext, $co{'tree'};
5519                my $lastfile = '';
5520                while (my $line = <$fd>) {
5521                        chomp $line;
5522                        my ($file, $lno, $ltext, $binary);
5523                        last if ($matches++ > 1000);
5524                        if ($line =~ /^Binary file (.+) matches$/) {
5525                                $file = $1;
5526                                $binary = 1;
5527                        } else {
5528                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5529                        }
5530                        if ($file ne $lastfile) {
5531                                $lastfile and print "</td></tr>\n";
5532                                if ($alternate++) {
5533                                        print "<tr class=\"dark\">\n";
5534                                } else {
5535                                        print "<tr class=\"light\">\n";
5536                                }
5537                                print "<td class=\"list\">".
5538                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5539                                                               file_name=>"$file"),
5540                                                -class => "list"}, esc_path($file));
5541                                print "</td><td>\n";
5542                                $lastfile = $file;
5543                        }
5544                        if ($binary) {
5545                                print "<div class=\"binary\">Binary file</div>\n";
5546                        } else {
5547                                $ltext = untabify($ltext);
5548                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5549                                        $ltext = esc_html($1, -nbsp=>1);
5550                                        $ltext .= '<span class="match">';
5551                                        $ltext .= esc_html($2, -nbsp=>1);
5552                                        $ltext .= '</span>';
5553                                        $ltext .= esc_html($3, -nbsp=>1);
5554                                } else {
5555                                        $ltext = esc_html($ltext, -nbsp=>1);
5556                                }
5557                                print "<div class=\"pre\">" .
5558                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5559                                                               file_name=>"$file").'#l'.$lno,
5560                                                -class => "linenr"}, sprintf('%4i', $lno))
5561                                        . ' ' .  $ltext . "</div>\n";
5562                        }
5563                }
5564                if ($lastfile) {
5565                        print "</td></tr>\n";
5566                        if ($matches > 1000) {
5567                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5568                        }
5569                } else {
5570                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5571                }
5572                close $fd;
5573
5574                print "</table>\n";
5575        }
5576        git_footer_html();
5577}
5578
5579sub git_search_help {
5580        git_header_html();
5581        git_print_page_nav('','', $hash,$hash,$hash);
5582        print <<EOT;
5583<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5584regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5585the pattern entered is recognized as the POSIX extended
5586<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5587insensitive).</p>
5588<dl>
5589<dt><b>commit</b></dt>
5590<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5591EOT
5592        my ($have_grep) = gitweb_check_feature('grep');
5593        if ($have_grep) {
5594                print <<EOT;
5595<dt><b>grep</b></dt>
5596<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5597    a different one) are searched for the given pattern. On large trees, this search can take
5598a while and put some strain on the server, so please use it with some consideration. Note that
5599due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5600case-sensitive.</dd>
5601EOT
5602        }
5603        print <<EOT;
5604<dt><b>author</b></dt>
5605<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5606<dt><b>committer</b></dt>
5607<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5608EOT
5609        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5610        if ($have_pickaxe) {
5611                print <<EOT;
5612<dt><b>pickaxe</b></dt>
5613<dd>All commits that caused the string to appear or disappear from any file (changes that
5614added, removed or "modified" the string) will be listed. This search can take a while and
5615takes a lot of strain on the server, so please use it wisely. Note that since you may be
5616interested even in changes just changing the case as well, this search is case sensitive.</dd>
5617EOT
5618        }
5619        print "</dl>\n";
5620        git_footer_html();
5621}
5622
5623sub git_shortlog {
5624        my $head = git_get_head_hash($project);
5625        if (!defined $hash) {
5626                $hash = $head;
5627        }
5628        if (!defined $page) {
5629                $page = 0;
5630        }
5631        my $refs = git_get_references();
5632
5633        my $commit_hash = $hash;
5634        if (defined $hash_parent) {
5635                $commit_hash = "$hash_parent..$hash";
5636        }
5637        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5638
5639        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5640        my $next_link = '';
5641        if ($#commitlist >= 100) {
5642                $next_link =
5643                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5644                                 -accesskey => "n", -title => "Alt-n"}, "next");
5645        }
5646
5647        git_header_html();
5648        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5649        git_print_header_div('summary', $project);
5650
5651        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5652
5653        git_footer_html();
5654}
5655
5656## ......................................................................
5657## feeds (RSS, Atom; OPML)
5658
5659sub git_feed {
5660        my $format = shift || 'atom';
5661        my ($have_blame) = gitweb_check_feature('blame');
5662
5663        # Atom: http://www.atomenabled.org/developers/syndication/
5664        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5665        if ($format ne 'rss' && $format ne 'atom') {
5666                die_error(400, "Unknown web feed format");
5667        }
5668
5669        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5670        my $head = $hash || 'HEAD';
5671        my @commitlist = parse_commits($head, 150, 0, $file_name);
5672
5673        my %latest_commit;
5674        my %latest_date;
5675        my $content_type = "application/$format+xml";
5676        if (defined $cgi->http('HTTP_ACCEPT') &&
5677                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5678                # browser (feed reader) prefers text/xml
5679                $content_type = 'text/xml';
5680        }
5681        if (defined($commitlist[0])) {
5682                %latest_commit = %{$commitlist[0]};
5683                %latest_date   = parse_date($latest_commit{'author_epoch'});
5684                print $cgi->header(
5685                        -type => $content_type,
5686                        -charset => 'utf-8',
5687                        -last_modified => $latest_date{'rfc2822'});
5688        } else {
5689                print $cgi->header(
5690                        -type => $content_type,
5691                        -charset => 'utf-8');
5692        }
5693
5694        # Optimization: skip generating the body if client asks only
5695        # for Last-Modified date.
5696        return if ($cgi->request_method() eq 'HEAD');
5697
5698        # header variables
5699        my $title = "$site_name - $project/$action";
5700        my $feed_type = 'log';
5701        if (defined $hash) {
5702                $title .= " - '$hash'";
5703                $feed_type = 'branch log';
5704                if (defined $file_name) {
5705                        $title .= " :: $file_name";
5706                        $feed_type = 'history';
5707                }
5708        } elsif (defined $file_name) {
5709                $title .= " - $file_name";
5710                $feed_type = 'history';
5711        }
5712        $title .= " $feed_type";
5713        my $descr = git_get_project_description($project);
5714        if (defined $descr) {
5715                $descr = esc_html($descr);
5716        } else {
5717                $descr = "$project " .
5718                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5719                         " feed";
5720        }
5721        my $owner = git_get_project_owner($project);
5722        $owner = esc_html($owner);
5723
5724        #header
5725        my $alt_url;
5726        if (defined $file_name) {
5727                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5728        } elsif (defined $hash) {
5729                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5730        } else {
5731                $alt_url = href(-full=>1, action=>"summary");
5732        }
5733        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5734        if ($format eq 'rss') {
5735                print <<XML;
5736<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5737<channel>
5738XML
5739                print "<title>$title</title>\n" .
5740                      "<link>$alt_url</link>\n" .
5741                      "<description>$descr</description>\n" .
5742                      "<language>en</language>\n";
5743        } elsif ($format eq 'atom') {
5744                print <<XML;
5745<feed xmlns="http://www.w3.org/2005/Atom">
5746XML
5747                print "<title>$title</title>\n" .
5748                      "<subtitle>$descr</subtitle>\n" .
5749                      '<link rel="alternate" type="text/html" href="' .
5750                      $alt_url . '" />' . "\n" .
5751                      '<link rel="self" type="' . $content_type . '" href="' .
5752                      $cgi->self_url() . '" />' . "\n" .
5753                      "<id>" . href(-full=>1) . "</id>\n" .
5754                      # use project owner for feed author
5755                      "<author><name>$owner</name></author>\n";
5756                if (defined $favicon) {
5757                        print "<icon>" . esc_url($favicon) . "</icon>\n";
5758                }
5759                if (defined $logo_url) {
5760                        # not twice as wide as tall: 72 x 27 pixels
5761                        print "<logo>" . esc_url($logo) . "</logo>\n";
5762                }
5763                if (! %latest_date) {
5764                        # dummy date to keep the feed valid until commits trickle in:
5765                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
5766                } else {
5767                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
5768                }
5769        }
5770
5771        # contents
5772        for (my $i = 0; $i <= $#commitlist; $i++) {
5773                my %co = %{$commitlist[$i]};
5774                my $commit = $co{'id'};
5775                # we read 150, we always show 30 and the ones more recent than 48 hours
5776                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5777                        last;
5778                }
5779                my %cd = parse_date($co{'author_epoch'});
5780
5781                # get list of changed files
5782                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5783                        $co{'parent'} || "--root",
5784                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
5785                        or next;
5786                my @difftree = map { chomp; $_ } <$fd>;
5787                close $fd
5788                        or next;
5789
5790                # print element (entry, item)
5791                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5792                if ($format eq 'rss') {
5793                        print "<item>\n" .
5794                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
5795                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
5796                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5797                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5798                              "<link>$co_url</link>\n" .
5799                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
5800                              "<content:encoded>" .
5801                              "<![CDATA[\n";
5802                } elsif ($format eq 'atom') {
5803                        print "<entry>\n" .
5804                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5805                              "<updated>$cd{'iso-8601'}</updated>\n" .
5806                              "<author>\n" .
5807                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5808                        if ($co{'author_email'}) {
5809                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5810                        }
5811                        print "</author>\n" .
5812                              # use committer for contributor
5813                              "<contributor>\n" .
5814                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5815                        if ($co{'committer_email'}) {
5816                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5817                        }
5818                        print "</contributor>\n" .
5819                              "<published>$cd{'iso-8601'}</published>\n" .
5820                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5821                              "<id>$co_url</id>\n" .
5822                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5823                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5824                }
5825                my $comment = $co{'comment'};
5826                print "<pre>\n";
5827                foreach my $line (@$comment) {
5828                        $line = esc_html($line);
5829                        print "$line\n";
5830                }
5831                print "</pre><ul>\n";
5832                foreach my $difftree_line (@difftree) {
5833                        my %difftree = parse_difftree_raw_line($difftree_line);
5834                        next if !$difftree{'from_id'};
5835
5836                        my $file = $difftree{'file'} || $difftree{'to_file'};
5837
5838                        print "<li>" .
5839                              "[" .
5840                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5841                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5842                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5843                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
5844                                      -title => "diff"}, 'D');
5845                        if ($have_blame) {
5846                                print $cgi->a({-href => href(-full=>1, action=>"blame",
5847                                                             file_name=>$file, hash_base=>$commit),
5848                                              -title => "blame"}, 'B');
5849                        }
5850                        # if this is not a feed of a file history
5851                        if (!defined $file_name || $file_name ne $file) {
5852                                print $cgi->a({-href => href(-full=>1, action=>"history",
5853                                                             file_name=>$file, hash=>$commit),
5854                                              -title => "history"}, 'H');
5855                        }
5856                        $file = esc_path($file);
5857                        print "] ".
5858                              "$file</li>\n";
5859                }
5860                if ($format eq 'rss') {
5861                        print "</ul>]]>\n" .
5862                              "</content:encoded>\n" .
5863                              "</item>\n";
5864                } elsif ($format eq 'atom') {
5865                        print "</ul>\n</div>\n" .
5866                              "</content>\n" .
5867                              "</entry>\n";
5868                }
5869        }
5870
5871        # end of feed
5872        if ($format eq 'rss') {
5873                print "</channel>\n</rss>\n";
5874        }       elsif ($format eq 'atom') {
5875                print "</feed>\n";
5876        }
5877}
5878
5879sub git_rss {
5880        git_feed('rss');
5881}
5882
5883sub git_atom {
5884        git_feed('atom');
5885}
5886
5887sub git_opml {
5888        my @list = git_get_projects_list();
5889
5890        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5891        print <<XML;
5892<?xml version="1.0" encoding="utf-8"?>
5893<opml version="1.0">
5894<head>
5895  <title>$site_name OPML Export</title>
5896</head>
5897<body>
5898<outline text="git RSS feeds">
5899XML
5900
5901        foreach my $pr (@list) {
5902                my %proj = %$pr;
5903                my $head = git_get_head_hash($proj{'path'});
5904                if (!defined $head) {
5905                        next;
5906                }
5907                $git_dir = "$projectroot/$proj{'path'}";
5908                my %co = parse_commit($head);
5909                if (!%co) {
5910                        next;
5911                }
5912
5913                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5914                my $rss  = "$my_url?p=$proj{'path'};a=rss";
5915                my $html = "$my_url?p=$proj{'path'};a=summary";
5916                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5917        }
5918        print <<XML;
5919</outline>
5920</body>
5921</opml>
5922XML
5923}