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