gitweb / gitweb.perlon commit diff --no-index -q: fix endless loop (a324fc4)
   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 inster 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).
 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        while (@actions) {
2870                my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
2871                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2872                # munch munch
2873                $link =~ s#%n#$project#g;
2874                $link =~ s#%f#$git_dir#g;
2875                $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
2876                $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
2877                $arg{$label}{'_href'} = $link;
2878        }
2879
2880        print "<div class=\"page_nav\">\n" .
2881                (join " | ",
2882                 map { $_ eq $current ?
2883                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
2884                 } @navs);
2885        print "<br/>\n$extra<br/>\n" .
2886              "</div>\n";
2887}
2888
2889sub format_paging_nav {
2890        my ($action, $hash, $head, $page, $has_next_link) = @_;
2891        my $paging_nav;
2892
2893
2894        if ($hash ne $head || $page) {
2895                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2896        } else {
2897                $paging_nav .= "HEAD";
2898        }
2899
2900        if ($page > 0) {
2901                $paging_nav .= " &sdot; " .
2902                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
2903                                 -accesskey => "p", -title => "Alt-p"}, "prev");
2904        } else {
2905                $paging_nav .= " &sdot; prev";
2906        }
2907
2908        if ($has_next_link) {
2909                $paging_nav .= " &sdot; " .
2910                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
2911                                 -accesskey => "n", -title => "Alt-n"}, "next");
2912        } else {
2913                $paging_nav .= " &sdot; next";
2914        }
2915
2916        return $paging_nav;
2917}
2918
2919## ......................................................................
2920## functions printing or outputting HTML: div
2921
2922sub git_print_header_div {
2923        my ($action, $title, $hash, $hash_base) = @_;
2924        my %args = ();
2925
2926        $args{'action'} = $action;
2927        $args{'hash'} = $hash if $hash;
2928        $args{'hash_base'} = $hash_base if $hash_base;
2929
2930        print "<div class=\"header\">\n" .
2931              $cgi->a({-href => href(%args), -class => "title"},
2932              $title ? $title : $action) .
2933              "\n</div>\n";
2934}
2935
2936#sub git_print_authorship (\%) {
2937sub git_print_authorship {
2938        my $co = shift;
2939
2940        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2941        print "<div class=\"author_date\">" .
2942              esc_html($co->{'author_name'}) .
2943              " [$ad{'rfc2822'}";
2944        if ($ad{'hour_local'} < 6) {
2945                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2946                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2947        } else {
2948                printf(" (%02d:%02d %s)",
2949                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2950        }
2951        print "]</div>\n";
2952}
2953
2954sub git_print_page_path {
2955        my $name = shift;
2956        my $type = shift;
2957        my $hb = shift;
2958
2959
2960        print "<div class=\"page_path\">";
2961        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2962                      -title => 'tree root'}, to_utf8("[$project]"));
2963        print " / ";
2964        if (defined $name) {
2965                my @dirname = split '/', $name;
2966                my $basename = pop @dirname;
2967                my $fullname = '';
2968
2969                foreach my $dir (@dirname) {
2970                        $fullname .= ($fullname ? '/' : '') . $dir;
2971                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2972                                                     hash_base=>$hb),
2973                                      -title => $fullname}, esc_path($dir));
2974                        print " / ";
2975                }
2976                if (defined $type && $type eq 'blob') {
2977                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2978                                                     hash_base=>$hb),
2979                                      -title => $name}, esc_path($basename));
2980                } elsif (defined $type && $type eq 'tree') {
2981                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2982                                                     hash_base=>$hb),
2983                                      -title => $name}, esc_path($basename));
2984                        print " / ";
2985                } else {
2986                        print esc_path($basename);
2987                }
2988        }
2989        print "<br/></div>\n";
2990}
2991
2992# sub git_print_log (\@;%) {
2993sub git_print_log ($;%) {
2994        my $log = shift;
2995        my %opts = @_;
2996
2997        if ($opts{'-remove_title'}) {
2998                # remove title, i.e. first line of log
2999                shift @$log;
3000        }
3001        # remove leading empty lines
3002        while (defined $log->[0] && $log->[0] eq "") {
3003                shift @$log;
3004        }
3005
3006        # print log
3007        my $signoff = 0;
3008        my $empty = 0;
3009        foreach my $line (@$log) {
3010                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3011                        $signoff = 1;
3012                        $empty = 0;
3013                        if (! $opts{'-remove_signoff'}) {
3014                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3015                                next;
3016                        } else {
3017                                # remove signoff lines
3018                                next;
3019                        }
3020                } else {
3021                        $signoff = 0;
3022                }
3023
3024                # print only one empty line
3025                # do not print empty line after signoff
3026                if ($line eq "") {
3027                        next if ($empty || $signoff);
3028                        $empty = 1;
3029                } else {
3030                        $empty = 0;
3031                }
3032
3033                print format_log_line_html($line) . "<br/>\n";
3034        }
3035
3036        if ($opts{'-final_empty_line'}) {
3037                # end with single empty line
3038                print "<br/>\n" unless $empty;
3039        }
3040}
3041
3042# return link target (what link points to)
3043sub git_get_link_target {
3044        my $hash = shift;
3045        my $link_target;
3046
3047        # read link
3048        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3049                or return;
3050        {
3051                local $/;
3052                $link_target = <$fd>;
3053        }
3054        close $fd
3055                or return;
3056
3057        return $link_target;
3058}
3059
3060# given link target, and the directory (basedir) the link is in,
3061# return target of link relative to top directory (top tree);
3062# return undef if it is not possible (including absolute links).
3063sub normalize_link_target {
3064        my ($link_target, $basedir, $hash_base) = @_;
3065
3066        # we can normalize symlink target only if $hash_base is provided
3067        return unless $hash_base;
3068
3069        # absolute symlinks (beginning with '/') cannot be normalized
3070        return if (substr($link_target, 0, 1) eq '/');
3071
3072        # normalize link target to path from top (root) tree (dir)
3073        my $path;
3074        if ($basedir) {
3075                $path = $basedir . '/' . $link_target;
3076        } else {
3077                # we are in top (root) tree (dir)
3078                $path = $link_target;
3079        }
3080
3081        # remove //, /./, and /../
3082        my @path_parts;
3083        foreach my $part (split('/', $path)) {
3084                # discard '.' and ''
3085                next if (!$part || $part eq '.');
3086                # handle '..'
3087                if ($part eq '..') {
3088                        if (@path_parts) {
3089                                pop @path_parts;
3090                        } else {
3091                                # link leads outside repository (outside top dir)
3092                                return;
3093                        }
3094                } else {
3095                        push @path_parts, $part;
3096                }
3097        }
3098        $path = join('/', @path_parts);
3099
3100        return $path;
3101}
3102
3103# print tree entry (row of git_tree), but without encompassing <tr> element
3104sub git_print_tree_entry {
3105        my ($t, $basedir, $hash_base, $have_blame) = @_;
3106
3107        my %base_key = ();
3108        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3109
3110        # The format of a table row is: mode list link.  Where mode is
3111        # the mode of the entry, list is the name of the entry, an href,
3112        # and link is the action links of the entry.
3113
3114        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3115        if ($t->{'type'} eq "blob") {
3116                print "<td class=\"list\">" .
3117                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3118                                               file_name=>"$basedir$t->{'name'}", %base_key),
3119                                -class => "list"}, esc_path($t->{'name'}));
3120                if (S_ISLNK(oct $t->{'mode'})) {
3121                        my $link_target = git_get_link_target($t->{'hash'});
3122                        if ($link_target) {
3123                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3124                                if (defined $norm_target) {
3125                                        print " -> " .
3126                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3127                                                                     file_name=>$norm_target),
3128                                                       -title => $norm_target}, esc_path($link_target));
3129                                } else {
3130                                        print " -> " . esc_path($link_target);
3131                                }
3132                        }
3133                }
3134                print "</td>\n";
3135                print "<td class=\"link\">";
3136                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3137                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3138                              "blob");
3139                if ($have_blame) {
3140                        print " | " .
3141                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3142                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3143                                      "blame");
3144                }
3145                if (defined $hash_base) {
3146                        print " | " .
3147                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3148                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3149                                      "history");
3150                }
3151                print " | " .
3152                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3153                                               file_name=>"$basedir$t->{'name'}")},
3154                                "raw");
3155                print "</td>\n";
3156
3157        } elsif ($t->{'type'} eq "tree") {
3158                print "<td class=\"list\">";
3159                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3160                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3161                              esc_path($t->{'name'}));
3162                print "</td>\n";
3163                print "<td class=\"link\">";
3164                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3165                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3166                              "tree");
3167                if (defined $hash_base) {
3168                        print " | " .
3169                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3170                                                     file_name=>"$basedir$t->{'name'}")},
3171                                      "history");
3172                }
3173                print "</td>\n";
3174        } else {
3175                # unknown object: we can only present history for it
3176                # (this includes 'commit' object, i.e. submodule support)
3177                print "<td class=\"list\">" .
3178                      esc_path($t->{'name'}) .
3179                      "</td>\n";
3180                print "<td class=\"link\">";
3181                if (defined $hash_base) {
3182                        print $cgi->a({-href => href(action=>"history",
3183                                                     hash_base=>$hash_base,
3184                                                     file_name=>"$basedir$t->{'name'}")},
3185                                      "history");
3186                }
3187                print "</td>\n";
3188        }
3189}
3190
3191## ......................................................................
3192## functions printing large fragments of HTML
3193
3194# get pre-image filenames for merge (combined) diff
3195sub fill_from_file_info {
3196        my ($diff, @parents) = @_;
3197
3198        $diff->{'from_file'} = [ ];
3199        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3200        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3201                if ($diff->{'status'}[$i] eq 'R' ||
3202                    $diff->{'status'}[$i] eq 'C') {
3203                        $diff->{'from_file'}[$i] =
3204                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3205                }
3206        }
3207
3208        return $diff;
3209}
3210
3211# is current raw difftree line of file deletion
3212sub is_deleted {
3213        my $diffinfo = shift;
3214
3215        return $diffinfo->{'to_id'} eq ('0' x 40);
3216}
3217
3218# does patch correspond to [previous] difftree raw line
3219# $diffinfo  - hashref of parsed raw diff format
3220# $patchinfo - hashref of parsed patch diff format
3221#              (the same keys as in $diffinfo)
3222sub is_patch_split {
3223        my ($diffinfo, $patchinfo) = @_;
3224
3225        return defined $diffinfo && defined $patchinfo
3226                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3227}
3228
3229
3230sub git_difftree_body {
3231        my ($difftree, $hash, @parents) = @_;
3232        my ($parent) = $parents[0];
3233        my ($have_blame) = gitweb_check_feature('blame');
3234        print "<div class=\"list_head\">\n";
3235        if ($#{$difftree} > 10) {
3236                print(($#{$difftree} + 1) . " files changed:\n");
3237        }
3238        print "</div>\n";
3239
3240        print "<table class=\"" .
3241              (@parents > 1 ? "combined " : "") .
3242              "diff_tree\">\n";
3243
3244        # header only for combined diff in 'commitdiff' view
3245        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3246        if ($has_header) {
3247                # table header
3248                print "<thead><tr>\n" .
3249                       "<th></th><th></th>\n"; # filename, patchN link
3250                for (my $i = 0; $i < @parents; $i++) {
3251                        my $par = $parents[$i];
3252                        print "<th>" .
3253                              $cgi->a({-href => href(action=>"commitdiff",
3254                                                     hash=>$hash, hash_parent=>$par),
3255                                       -title => 'commitdiff to parent number ' .
3256                                                  ($i+1) . ': ' . substr($par,0,7)},
3257                                      $i+1) .
3258                              "&nbsp;</th>\n";
3259                }
3260                print "</tr></thead>\n<tbody>\n";
3261        }
3262
3263        my $alternate = 1;
3264        my $patchno = 0;
3265        foreach my $line (@{$difftree}) {
3266                my $diff = parsed_difftree_line($line);
3267
3268                if ($alternate) {
3269                        print "<tr class=\"dark\">\n";
3270                } else {
3271                        print "<tr class=\"light\">\n";
3272                }
3273                $alternate ^= 1;
3274
3275                if (exists $diff->{'nparents'}) { # combined diff
3276
3277                        fill_from_file_info($diff, @parents)
3278                                unless exists $diff->{'from_file'};
3279
3280                        if (!is_deleted($diff)) {
3281                                # file exists in the result (child) commit
3282                                print "<td>" .
3283                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3284                                                             file_name=>$diff->{'to_file'},
3285                                                             hash_base=>$hash),
3286                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3287                                      "</td>\n";
3288                        } else {
3289                                print "<td>" .
3290                                      esc_path($diff->{'to_file'}) .
3291                                      "</td>\n";
3292                        }
3293
3294                        if ($action eq 'commitdiff') {
3295                                # link to patch
3296                                $patchno++;
3297                                print "<td class=\"link\">" .
3298                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3299                                      " | " .
3300                                      "</td>\n";
3301                        }
3302
3303                        my $has_history = 0;
3304                        my $not_deleted = 0;
3305                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3306                                my $hash_parent = $parents[$i];
3307                                my $from_hash = $diff->{'from_id'}[$i];
3308                                my $from_path = $diff->{'from_file'}[$i];
3309                                my $status = $diff->{'status'}[$i];
3310
3311                                $has_history ||= ($status ne 'A');
3312                                $not_deleted ||= ($status ne 'D');
3313
3314                                if ($status eq 'A') {
3315                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3316                                } elsif ($status eq 'D') {
3317                                        print "<td class=\"link\">" .
3318                                              $cgi->a({-href => href(action=>"blob",
3319                                                                     hash_base=>$hash,
3320                                                                     hash=>$from_hash,
3321                                                                     file_name=>$from_path)},
3322                                                      "blob" . ($i+1)) .
3323                                              " | </td>\n";
3324                                } else {
3325                                        if ($diff->{'to_id'} eq $from_hash) {
3326                                                print "<td class=\"link nochange\">";
3327                                        } else {
3328                                                print "<td class=\"link\">";
3329                                        }
3330                                        print $cgi->a({-href => href(action=>"blobdiff",
3331                                                                     hash=>$diff->{'to_id'},
3332                                                                     hash_parent=>$from_hash,
3333                                                                     hash_base=>$hash,
3334                                                                     hash_parent_base=>$hash_parent,
3335                                                                     file_name=>$diff->{'to_file'},
3336                                                                     file_parent=>$from_path)},
3337                                                      "diff" . ($i+1)) .
3338                                              " | </td>\n";
3339                                }
3340                        }
3341
3342                        print "<td class=\"link\">";
3343                        if ($not_deleted) {
3344                                print $cgi->a({-href => href(action=>"blob",
3345                                                             hash=>$diff->{'to_id'},
3346                                                             file_name=>$diff->{'to_file'},
3347                                                             hash_base=>$hash)},
3348                                              "blob");
3349                                print " | " if ($has_history);
3350                        }
3351                        if ($has_history) {
3352                                print $cgi->a({-href => href(action=>"history",
3353                                                             file_name=>$diff->{'to_file'},
3354                                                             hash_base=>$hash)},
3355                                              "history");
3356                        }
3357                        print "</td>\n";
3358
3359                        print "</tr>\n";
3360                        next; # instead of 'else' clause, to avoid extra indent
3361                }
3362                # else ordinary diff
3363
3364                my ($to_mode_oct, $to_mode_str, $to_file_type);
3365                my ($from_mode_oct, $from_mode_str, $from_file_type);
3366                if ($diff->{'to_mode'} ne ('0' x 6)) {
3367                        $to_mode_oct = oct $diff->{'to_mode'};
3368                        if (S_ISREG($to_mode_oct)) { # only for regular file
3369                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3370                        }
3371                        $to_file_type = file_type($diff->{'to_mode'});
3372                }
3373                if ($diff->{'from_mode'} ne ('0' x 6)) {
3374                        $from_mode_oct = oct $diff->{'from_mode'};
3375                        if (S_ISREG($to_mode_oct)) { # only for regular file
3376                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3377                        }
3378                        $from_file_type = file_type($diff->{'from_mode'});
3379                }
3380
3381                if ($diff->{'status'} eq "A") { # created
3382                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3383                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3384                        $mode_chng   .= "]</span>";
3385                        print "<td>";
3386                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3387                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3388                                      -class => "list"}, esc_path($diff->{'file'}));
3389                        print "</td>\n";
3390                        print "<td>$mode_chng</td>\n";
3391                        print "<td class=\"link\">";
3392                        if ($action eq 'commitdiff') {
3393                                # link to patch
3394                                $patchno++;
3395                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3396                                print " | ";
3397                        }
3398                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3399                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3400                                      "blob");
3401                        print "</td>\n";
3402
3403                } elsif ($diff->{'status'} eq "D") { # deleted
3404                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3405                        print "<td>";
3406                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3407                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3408                                       -class => "list"}, esc_path($diff->{'file'}));
3409                        print "</td>\n";
3410                        print "<td>$mode_chng</td>\n";
3411                        print "<td class=\"link\">";
3412                        if ($action eq 'commitdiff') {
3413                                # link to patch
3414                                $patchno++;
3415                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3416                                print " | ";
3417                        }
3418                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3419                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3420                                      "blob") . " | ";
3421                        if ($have_blame) {
3422                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3423                                                             file_name=>$diff->{'file'})},
3424                                              "blame") . " | ";
3425                        }
3426                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3427                                                     file_name=>$diff->{'file'})},
3428                                      "history");
3429                        print "</td>\n";
3430
3431                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3432                        my $mode_chnge = "";
3433                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3434                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3435                                if ($from_file_type ne $to_file_type) {
3436                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3437                                }
3438                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3439                                        if ($from_mode_str && $to_mode_str) {
3440                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3441                                        } elsif ($to_mode_str) {
3442                                                $mode_chnge .= " mode: $to_mode_str";
3443                                        }
3444                                }
3445                                $mode_chnge .= "]</span>\n";
3446                        }
3447                        print "<td>";
3448                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3449                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3450                                      -class => "list"}, esc_path($diff->{'file'}));
3451                        print "</td>\n";
3452                        print "<td>$mode_chnge</td>\n";
3453                        print "<td class=\"link\">";
3454                        if ($action eq 'commitdiff') {
3455                                # link to patch
3456                                $patchno++;
3457                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3458                                      " | ";
3459                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3460                                # "commit" view and modified file (not onlu mode changed)
3461                                print $cgi->a({-href => href(action=>"blobdiff",
3462                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3463                                                             hash_base=>$hash, hash_parent_base=>$parent,
3464                                                             file_name=>$diff->{'file'})},
3465                                              "diff") .
3466                                      " | ";
3467                        }
3468                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3469                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3470                                       "blob") . " | ";
3471                        if ($have_blame) {
3472                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3473                                                             file_name=>$diff->{'file'})},
3474                                              "blame") . " | ";
3475                        }
3476                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3477                                                     file_name=>$diff->{'file'})},
3478                                      "history");
3479                        print "</td>\n";
3480
3481                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3482                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3483                        my $nstatus = $status_name{$diff->{'status'}};
3484                        my $mode_chng = "";
3485                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3486                                # mode also for directories, so we cannot use $to_mode_str
3487                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3488                        }
3489                        print "<td>" .
3490                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3491                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3492                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3493                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3494                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3495                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3496                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3497                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3498                              "<td class=\"link\">";
3499                        if ($action eq 'commitdiff') {
3500                                # link to patch
3501                                $patchno++;
3502                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3503                                      " | ";
3504                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3505                                # "commit" view and modified file (not only pure rename or copy)
3506                                print $cgi->a({-href => href(action=>"blobdiff",
3507                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3508                                                             hash_base=>$hash, hash_parent_base=>$parent,
3509                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3510                                              "diff") .
3511                                      " | ";
3512                        }
3513                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3514                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3515                                      "blob") . " | ";
3516                        if ($have_blame) {
3517                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3518                                                             file_name=>$diff->{'to_file'})},
3519                                              "blame") . " | ";
3520                        }
3521                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3522                                                    file_name=>$diff->{'to_file'})},
3523                                      "history");
3524                        print "</td>\n";
3525
3526                } # we should not encounter Unmerged (U) or Unknown (X) status
3527                print "</tr>\n";
3528        }
3529        print "</tbody>" if $has_header;
3530        print "</table>\n";
3531}
3532
3533sub git_patchset_body {
3534        my ($fd, $difftree, $hash, @hash_parents) = @_;
3535        my ($hash_parent) = $hash_parents[0];
3536
3537        my $is_combined = (@hash_parents > 1);
3538        my $patch_idx = 0;
3539        my $patch_number = 0;
3540        my $patch_line;
3541        my $diffinfo;
3542        my $to_name;
3543        my (%from, %to);
3544
3545        print "<div class=\"patchset\">\n";
3546
3547        # skip to first patch
3548        while ($patch_line = <$fd>) {
3549                chomp $patch_line;
3550
3551                last if ($patch_line =~ m/^diff /);
3552        }
3553
3554 PATCH:
3555        while ($patch_line) {
3556
3557                # parse "git diff" header line
3558                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3559                        # $1 is from_name, which we do not use
3560                        $to_name = unquote($2);
3561                        $to_name =~ s!^b/!!;
3562                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3563                        # $1 is 'cc' or 'combined', which we do not use
3564                        $to_name = unquote($2);
3565                } else {
3566                        $to_name = undef;
3567                }
3568
3569                # check if current patch belong to current raw line
3570                # and parse raw git-diff line if needed
3571                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3572                        # this is continuation of a split patch
3573                        print "<div class=\"patch cont\">\n";
3574                } else {
3575                        # advance raw git-diff output if needed
3576                        $patch_idx++ if defined $diffinfo;
3577
3578                        # read and prepare patch information
3579                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3580
3581                        # compact combined diff output can have some patches skipped
3582                        # find which patch (using pathname of result) we are at now;
3583                        if ($is_combined) {
3584                                while ($to_name ne $diffinfo->{'to_file'}) {
3585                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3586                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3587                                              "</div>\n";  # class="patch"
3588
3589                                        $patch_idx++;
3590                                        $patch_number++;
3591
3592                                        last if $patch_idx > $#$difftree;
3593                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3594                                }
3595                        }
3596
3597                        # modifies %from, %to hashes
3598                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3599
3600                        # this is first patch for raw difftree line with $patch_idx index
3601                        # we index @$difftree array from 0, but number patches from 1
3602                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3603                }
3604
3605                # git diff header
3606                #assert($patch_line =~ m/^diff /) if DEBUG;
3607                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3608                $patch_number++;
3609                # print "git diff" header
3610                print format_git_diff_header_line($patch_line, $diffinfo,
3611                                                  \%from, \%to);
3612
3613                # print extended diff header
3614                print "<div class=\"diff extended_header\">\n";
3615        EXTENDED_HEADER:
3616                while ($patch_line = <$fd>) {
3617                        chomp $patch_line;
3618
3619                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3620
3621                        print format_extended_diff_header_line($patch_line, $diffinfo,
3622                                                               \%from, \%to);
3623                }
3624                print "</div>\n"; # class="diff extended_header"
3625
3626                # from-file/to-file diff header
3627                if (! $patch_line) {
3628                        print "</div>\n"; # class="patch"
3629                        last PATCH;
3630                }
3631                next PATCH if ($patch_line =~ m/^diff /);
3632                #assert($patch_line =~ m/^---/) if DEBUG;
3633
3634                my $last_patch_line = $patch_line;
3635                $patch_line = <$fd>;
3636                chomp $patch_line;
3637                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3638
3639                print format_diff_from_to_header($last_patch_line, $patch_line,
3640                                                 $diffinfo, \%from, \%to,
3641                                                 @hash_parents);
3642
3643                # the patch itself
3644        LINE:
3645                while ($patch_line = <$fd>) {
3646                        chomp $patch_line;
3647
3648                        next PATCH if ($patch_line =~ m/^diff /);
3649
3650                        print format_diff_line($patch_line, \%from, \%to);
3651                }
3652
3653        } continue {
3654                print "</div>\n"; # class="patch"
3655        }
3656
3657        # for compact combined (--cc) format, with chunk and patch simpliciaction
3658        # patchset might be empty, but there might be unprocessed raw lines
3659        for (++$patch_idx if $patch_number > 0;
3660             $patch_idx < @$difftree;
3661             ++$patch_idx) {
3662                # read and prepare patch information
3663                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3664
3665                # generate anchor for "patch" links in difftree / whatchanged part
3666                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3667                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3668                      "</div>\n";  # class="patch"
3669
3670                $patch_number++;
3671        }
3672
3673        if ($patch_number == 0) {
3674                if (@hash_parents > 1) {
3675                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3676                } else {
3677                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3678                }
3679        }
3680
3681        print "</div>\n"; # class="patchset"
3682}
3683
3684# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3685
3686# fills project list info (age, description, owner, forks) for each
3687# project in the list, removing invalid projects from returned list
3688# NOTE: modifies $projlist, but does not remove entries from it
3689sub fill_project_list_info {
3690        my ($projlist, $check_forks) = @_;
3691        my @projects;
3692
3693        my $show_ctags = gitweb_check_feature('ctags');
3694 PROJECT:
3695        foreach my $pr (@$projlist) {
3696                my (@activity) = git_get_last_activity($pr->{'path'});
3697                unless (@activity) {
3698                        next PROJECT;
3699                }
3700                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3701                if (!defined $pr->{'descr'}) {
3702                        my $descr = git_get_project_description($pr->{'path'}) || "";
3703                        $descr = to_utf8($descr);
3704                        $pr->{'descr_long'} = $descr;
3705                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3706                }
3707                if (!defined $pr->{'owner'}) {
3708                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3709                }
3710                if ($check_forks) {
3711                        my $pname = $pr->{'path'};
3712                        if (($pname =~ s/\.git$//) &&
3713                            ($pname !~ /\/$/) &&
3714                            (-d "$projectroot/$pname")) {
3715                                $pr->{'forks'} = "-d $projectroot/$pname";
3716                        }       else {
3717                                $pr->{'forks'} = 0;
3718                        }
3719                }
3720                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3721                push @projects, $pr;
3722        }
3723
3724        return @projects;
3725}
3726
3727# print 'sort by' <th> element, generating 'sort by $name' replay link
3728# if that order is not selected
3729sub print_sort_th {
3730        my ($name, $order, $header) = @_;
3731        $header ||= ucfirst($name);
3732
3733        if ($order eq $name) {
3734                print "<th>$header</th>\n";
3735        } else {
3736                print "<th>" .
3737                      $cgi->a({-href => href(-replay=>1, order=>$name),
3738                               -class => "header"}, $header) .
3739                      "</th>\n";
3740        }
3741}
3742
3743sub git_project_list_body {
3744        # actually uses global variable $project
3745        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3746
3747        my ($check_forks) = gitweb_check_feature('forks');
3748        my @projects = fill_project_list_info($projlist, $check_forks);
3749
3750        $order ||= $default_projects_order;
3751        $from = 0 unless defined $from;
3752        $to = $#projects if (!defined $to || $#projects < $to);
3753
3754        my %order_info = (
3755                project => { key => 'path', type => 'str' },
3756                descr => { key => 'descr_long', type => 'str' },
3757                owner => { key => 'owner', type => 'str' },
3758                age => { key => 'age', type => 'num' }
3759        );
3760        my $oi = $order_info{$order};
3761        if ($oi->{'type'} eq 'str') {
3762                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3763        } else {
3764                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3765        }
3766
3767        my $show_ctags = gitweb_check_feature('ctags');
3768        if ($show_ctags) {
3769                my %ctags;
3770                foreach my $p (@projects) {
3771                        foreach my $ct (keys %{$p->{'ctags'}}) {
3772                                $ctags{$ct} += $p->{'ctags'}->{$ct};
3773                        }
3774                }
3775                my $cloud = git_populate_project_tagcloud(\%ctags);
3776                print git_show_project_tagcloud($cloud, 64);
3777        }
3778
3779        print "<table class=\"project_list\">\n";
3780        unless ($no_header) {
3781                print "<tr>\n";
3782                if ($check_forks) {
3783                        print "<th></th>\n";
3784                }
3785                print_sort_th('project', $order, 'Project');
3786                print_sort_th('descr', $order, 'Description');
3787                print_sort_th('owner', $order, 'Owner');
3788                print_sort_th('age', $order, 'Last Change');
3789                print "<th></th>\n" . # for links
3790                      "</tr>\n";
3791        }
3792        my $alternate = 1;
3793        my $tagfilter = $cgi->param('by_tag');
3794        for (my $i = $from; $i <= $to; $i++) {
3795                my $pr = $projects[$i];
3796
3797                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3798                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3799                        and not $pr->{'descr_long'} =~ /$searchtext/;
3800                # Weed out forks or non-matching entries of search
3801                if ($check_forks) {
3802                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3803                        $forkbase="^$forkbase" if $forkbase;
3804                        next if not $searchtext and not $tagfilter and $show_ctags
3805                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3806                }
3807
3808                if ($alternate) {
3809                        print "<tr class=\"dark\">\n";
3810                } else {
3811                        print "<tr class=\"light\">\n";
3812                }
3813                $alternate ^= 1;
3814                if ($check_forks) {
3815                        print "<td>";
3816                        if ($pr->{'forks'}) {
3817                                print "<!-- $pr->{'forks'} -->\n";
3818                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3819                        }
3820                        print "</td>\n";
3821                }
3822                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3823                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3824                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3825                                        -class => "list", -title => $pr->{'descr_long'}},
3826                                        esc_html($pr->{'descr'})) . "</td>\n" .
3827                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3828                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3829                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3830                      "<td class=\"link\">" .
3831                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3832                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3833                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3834                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3835                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3836                      "</td>\n" .
3837                      "</tr>\n";
3838        }
3839        if (defined $extra) {
3840                print "<tr>\n";
3841                if ($check_forks) {
3842                        print "<td></td>\n";
3843                }
3844                print "<td colspan=\"5\">$extra</td>\n" .
3845                      "</tr>\n";
3846        }
3847        print "</table>\n";
3848}
3849
3850sub git_shortlog_body {
3851        # uses global variable $project
3852        my ($commitlist, $from, $to, $refs, $extra) = @_;
3853
3854        $from = 0 unless defined $from;
3855        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3856
3857        print "<table class=\"shortlog\">\n";
3858        my $alternate = 1;
3859        for (my $i = $from; $i <= $to; $i++) {
3860                my %co = %{$commitlist->[$i]};
3861                my $commit = $co{'id'};
3862                my $ref = format_ref_marker($refs, $commit);
3863                if ($alternate) {
3864                        print "<tr class=\"dark\">\n";
3865                } else {
3866                        print "<tr class=\"light\">\n";
3867                }
3868                $alternate ^= 1;
3869                my $author = chop_and_escape_str($co{'author_name'}, 10);
3870                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3871                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3872                      "<td><i>" . $author . "</i></td>\n" .
3873                      "<td>";
3874                print format_subject_html($co{'title'}, $co{'title_short'},
3875                                          href(action=>"commit", hash=>$commit), $ref);
3876                print "</td>\n" .
3877                      "<td class=\"link\">" .
3878                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3879                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3880                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3881                my $snapshot_links = format_snapshot_links($commit);
3882                if (defined $snapshot_links) {
3883                        print " | " . $snapshot_links;
3884                }
3885                print "</td>\n" .
3886                      "</tr>\n";
3887        }
3888        if (defined $extra) {
3889                print "<tr>\n" .
3890                      "<td colspan=\"4\">$extra</td>\n" .
3891                      "</tr>\n";
3892        }
3893        print "</table>\n";
3894}
3895
3896sub git_history_body {
3897        # Warning: assumes constant type (blob or tree) during history
3898        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3899
3900        $from = 0 unless defined $from;
3901        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3902
3903        print "<table class=\"history\">\n";
3904        my $alternate = 1;
3905        for (my $i = $from; $i <= $to; $i++) {
3906                my %co = %{$commitlist->[$i]};
3907                if (!%co) {
3908                        next;
3909                }
3910                my $commit = $co{'id'};
3911
3912                my $ref = format_ref_marker($refs, $commit);
3913
3914                if ($alternate) {
3915                        print "<tr class=\"dark\">\n";
3916                } else {
3917                        print "<tr class=\"light\">\n";
3918                }
3919                $alternate ^= 1;
3920        # shortlog uses      chop_str($co{'author_name'}, 10)
3921                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3922                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3923                      "<td><i>" . $author . "</i></td>\n" .
3924                      "<td>";
3925                # originally git_history used chop_str($co{'title'}, 50)
3926                print format_subject_html($co{'title'}, $co{'title_short'},
3927                                          href(action=>"commit", hash=>$commit), $ref);
3928                print "</td>\n" .
3929                      "<td class=\"link\">" .
3930                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3931                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3932
3933                if ($ftype eq 'blob') {
3934                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3935                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3936                        if (defined $blob_current && defined $blob_parent &&
3937                                        $blob_current ne $blob_parent) {
3938                                print " | " .
3939                                        $cgi->a({-href => href(action=>"blobdiff",
3940                                                               hash=>$blob_current, hash_parent=>$blob_parent,
3941                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
3942                                                               file_name=>$file_name)},
3943                                                "diff to current");
3944                        }
3945                }
3946                print "</td>\n" .
3947                      "</tr>\n";
3948        }
3949        if (defined $extra) {
3950                print "<tr>\n" .
3951                      "<td colspan=\"4\">$extra</td>\n" .
3952                      "</tr>\n";
3953        }
3954        print "</table>\n";
3955}
3956
3957sub git_tags_body {
3958        # uses global variable $project
3959        my ($taglist, $from, $to, $extra) = @_;
3960        $from = 0 unless defined $from;
3961        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3962
3963        print "<table class=\"tags\">\n";
3964        my $alternate = 1;
3965        for (my $i = $from; $i <= $to; $i++) {
3966                my $entry = $taglist->[$i];
3967                my %tag = %$entry;
3968                my $comment = $tag{'subject'};
3969                my $comment_short;
3970                if (defined $comment) {
3971                        $comment_short = chop_str($comment, 30, 5);
3972                }
3973                if ($alternate) {
3974                        print "<tr class=\"dark\">\n";
3975                } else {
3976                        print "<tr class=\"light\">\n";
3977                }
3978                $alternate ^= 1;
3979                if (defined $tag{'age'}) {
3980                        print "<td><i>$tag{'age'}</i></td>\n";
3981                } else {
3982                        print "<td></td>\n";
3983                }
3984                print "<td>" .
3985                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3986                               -class => "list name"}, esc_html($tag{'name'})) .
3987                      "</td>\n" .
3988                      "<td>";
3989                if (defined $comment) {
3990                        print format_subject_html($comment, $comment_short,
3991                                                  href(action=>"tag", hash=>$tag{'id'}));
3992                }
3993                print "</td>\n" .
3994                      "<td class=\"selflink\">";
3995                if ($tag{'type'} eq "tag") {
3996                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3997                } else {
3998                        print "&nbsp;";
3999                }
4000                print "</td>\n" .
4001                      "<td class=\"link\">" . " | " .
4002                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4003                if ($tag{'reftype'} eq "commit") {
4004                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4005                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4006                } elsif ($tag{'reftype'} eq "blob") {
4007                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4008                }
4009                print "</td>\n" .
4010                      "</tr>";
4011        }
4012        if (defined $extra) {
4013                print "<tr>\n" .
4014                      "<td colspan=\"5\">$extra</td>\n" .
4015                      "</tr>\n";
4016        }
4017        print "</table>\n";
4018}
4019
4020sub git_heads_body {
4021        # uses global variable $project
4022        my ($headlist, $head, $from, $to, $extra) = @_;
4023        $from = 0 unless defined $from;
4024        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4025
4026        print "<table class=\"heads\">\n";
4027        my $alternate = 1;
4028        for (my $i = $from; $i <= $to; $i++) {
4029                my $entry = $headlist->[$i];
4030                my %ref = %$entry;
4031                my $curr = $ref{'id'} eq $head;
4032                if ($alternate) {
4033                        print "<tr class=\"dark\">\n";
4034                } else {
4035                        print "<tr class=\"light\">\n";
4036                }
4037                $alternate ^= 1;
4038                print "<td><i>$ref{'age'}</i></td>\n" .
4039                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4040                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4041                               -class => "list name"},esc_html($ref{'name'})) .
4042                      "</td>\n" .
4043                      "<td class=\"link\">" .
4044                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4045                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4046                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4047                      "</td>\n" .
4048                      "</tr>";
4049        }
4050        if (defined $extra) {
4051                print "<tr>\n" .
4052                      "<td colspan=\"3\">$extra</td>\n" .
4053                      "</tr>\n";
4054        }
4055        print "</table>\n";
4056}
4057
4058sub git_search_grep_body {
4059        my ($commitlist, $from, $to, $extra) = @_;
4060        $from = 0 unless defined $from;
4061        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4062
4063        print "<table class=\"commit_search\">\n";
4064        my $alternate = 1;
4065        for (my $i = $from; $i <= $to; $i++) {
4066                my %co = %{$commitlist->[$i]};
4067                if (!%co) {
4068                        next;
4069                }
4070                my $commit = $co{'id'};
4071                if ($alternate) {
4072                        print "<tr class=\"dark\">\n";
4073                } else {
4074                        print "<tr class=\"light\">\n";
4075                }
4076                $alternate ^= 1;
4077                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4078                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4079                      "<td><i>" . $author . "</i></td>\n" .
4080                      "<td>" .
4081                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4082                               -class => "list subject"},
4083                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4084                my $comment = $co{'comment'};
4085                foreach my $line (@$comment) {
4086                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4087                                my ($lead, $match, $trail) = ($1, $2, $3);
4088                                $match = chop_str($match, 70, 5, 'center');
4089                                my $contextlen = int((80 - length($match))/2);
4090                                $contextlen = 30 if ($contextlen > 30);
4091                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4092                                $trail = chop_str($trail, $contextlen, 10, 'right');
4093
4094                                $lead  = esc_html($lead);
4095                                $match = esc_html($match);
4096                                $trail = esc_html($trail);
4097
4098                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4099                        }
4100                }
4101                print "</td>\n" .
4102                      "<td class=\"link\">" .
4103                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4104                      " | " .
4105                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4106                      " | " .
4107                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4108                print "</td>\n" .
4109                      "</tr>\n";
4110        }
4111        if (defined $extra) {
4112                print "<tr>\n" .
4113                      "<td colspan=\"3\">$extra</td>\n" .
4114                      "</tr>\n";
4115        }
4116        print "</table>\n";
4117}
4118
4119## ======================================================================
4120## ======================================================================
4121## actions
4122
4123sub git_project_list {
4124        my $order = $cgi->param('o');
4125        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4126                die_error(400, "Unknown order parameter");
4127        }
4128
4129        my @list = git_get_projects_list();
4130        if (!@list) {
4131                die_error(404, "No projects found");
4132        }
4133
4134        git_header_html();
4135        if (-f $home_text) {
4136                print "<div class=\"index_include\">\n";
4137                open (my $fd, $home_text);
4138                print <$fd>;
4139                close $fd;
4140                print "</div>\n";
4141        }
4142        print $cgi->startform(-method => "get") .
4143              "<p class=\"projsearch\">Search:\n" .
4144              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4145              "</p>" .
4146              $cgi->end_form() . "\n";
4147        git_project_list_body(\@list, $order);
4148        git_footer_html();
4149}
4150
4151sub git_forks {
4152        my $order = $cgi->param('o');
4153        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4154                die_error(400, "Unknown order parameter");
4155        }
4156
4157        my @list = git_get_projects_list($project);
4158        if (!@list) {
4159                die_error(404, "No forks found");
4160        }
4161
4162        git_header_html();
4163        git_print_page_nav('','');
4164        git_print_header_div('summary', "$project forks");
4165        git_project_list_body(\@list, $order);
4166        git_footer_html();
4167}
4168
4169sub git_project_index {
4170        my @projects = git_get_projects_list($project);
4171
4172        print $cgi->header(
4173                -type => 'text/plain',
4174                -charset => 'utf-8',
4175                -content_disposition => 'inline; filename="index.aux"');
4176
4177        foreach my $pr (@projects) {
4178                if (!exists $pr->{'owner'}) {
4179                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4180                }
4181
4182                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4183                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4184                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4185                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4186                $path  =~ s/ /\+/g;
4187                $owner =~ s/ /\+/g;
4188
4189                print "$path $owner\n";
4190        }
4191}
4192
4193sub git_summary {
4194        my $descr = git_get_project_description($project) || "none";
4195        my %co = parse_commit("HEAD");
4196        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4197        my $head = $co{'id'};
4198
4199        my $owner = git_get_project_owner($project);
4200
4201        my $refs = git_get_references();
4202        # These get_*_list functions return one more to allow us to see if
4203        # there are more ...
4204        my @taglist  = git_get_tags_list(16);
4205        my @headlist = git_get_heads_list(16);
4206        my @forklist;
4207        my ($check_forks) = gitweb_check_feature('forks');
4208
4209        if ($check_forks) {
4210                @forklist = git_get_projects_list($project);
4211        }
4212
4213        git_header_html();
4214        git_print_page_nav('summary','', $head);
4215
4216        print "<div class=\"title\">&nbsp;</div>\n";
4217        print "<table class=\"projects_list\">\n" .
4218              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4219              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4220        if (defined $cd{'rfc2822'}) {
4221                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4222        }
4223
4224        # use per project git URL list in $projectroot/$project/cloneurl
4225        # or make project git URL from git base URL and project name
4226        my $url_tag = "URL";
4227        my @url_list = git_get_project_url_list($project);
4228        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4229        foreach my $git_url (@url_list) {
4230                next unless $git_url;
4231                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4232                $url_tag = "";
4233        }
4234
4235        # Tag cloud
4236        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4237        if ($show_ctags) {
4238                my $ctags = git_get_project_ctags($project);
4239                my $cloud = git_populate_project_tagcloud($ctags);
4240                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4241                print "</td>\n<td>" unless %$ctags;
4242                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4243                print "</td>\n<td>" if %$ctags;
4244                print git_show_project_tagcloud($cloud, 48);
4245                print "</td></tr>";
4246        }
4247
4248        print "</table>\n";
4249
4250        if (-s "$projectroot/$project/README.html") {
4251                if (open my $fd, "$projectroot/$project/README.html") {
4252                        print "<div class=\"title\">readme</div>\n" .
4253                              "<div class=\"readme\">\n";
4254                        print $_ while (<$fd>);
4255                        print "\n</div>\n"; # class="readme"
4256                        close $fd;
4257                }
4258        }
4259
4260        # we need to request one more than 16 (0..15) to check if
4261        # those 16 are all
4262        my @commitlist = $head ? parse_commits($head, 17) : ();
4263        if (@commitlist) {
4264                git_print_header_div('shortlog');
4265                git_shortlog_body(\@commitlist, 0, 15, $refs,
4266                                  $#commitlist <=  15 ? undef :
4267                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4268        }
4269
4270        if (@taglist) {
4271                git_print_header_div('tags');
4272                git_tags_body(\@taglist, 0, 15,
4273                              $#taglist <=  15 ? undef :
4274                              $cgi->a({-href => href(action=>"tags")}, "..."));
4275        }
4276
4277        if (@headlist) {
4278                git_print_header_div('heads');
4279                git_heads_body(\@headlist, $head, 0, 15,
4280                               $#headlist <= 15 ? undef :
4281                               $cgi->a({-href => href(action=>"heads")}, "..."));
4282        }
4283
4284        if (@forklist) {
4285                git_print_header_div('forks');
4286                git_project_list_body(\@forklist, 'age', 0, 15,
4287                                      $#forklist <= 15 ? undef :
4288                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4289                                      'no_header');
4290        }
4291
4292        git_footer_html();
4293}
4294
4295sub git_tag {
4296        my $head = git_get_head_hash($project);
4297        git_header_html();
4298        git_print_page_nav('','', $head,undef,$head);
4299        my %tag = parse_tag($hash);
4300
4301        if (! %tag) {
4302                die_error(404, "Unknown tag object");
4303        }
4304
4305        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4306        print "<div class=\"title_text\">\n" .
4307              "<table class=\"object_header\">\n" .
4308              "<tr>\n" .
4309              "<td>object</td>\n" .
4310              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4311                               $tag{'object'}) . "</td>\n" .
4312              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4313                                              $tag{'type'}) . "</td>\n" .
4314              "</tr>\n";
4315        if (defined($tag{'author'})) {
4316                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4317                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4318                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4319                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4320                        "</td></tr>\n";
4321        }
4322        print "</table>\n\n" .
4323              "</div>\n";
4324        print "<div class=\"page_body\">";
4325        my $comment = $tag{'comment'};
4326        foreach my $line (@$comment) {
4327                chomp $line;
4328                print esc_html($line, -nbsp=>1) . "<br/>\n";
4329        }
4330        print "</div>\n";
4331        git_footer_html();
4332}
4333
4334sub git_blame {
4335        my $fd;
4336        my $ftype;
4337
4338        gitweb_check_feature('blame')
4339            or die_error(403, "Blame view not allowed");
4340
4341        die_error(400, "No file name given") unless $file_name;
4342        $hash_base ||= git_get_head_hash($project);
4343        die_error(404, "Couldn't find base commit") unless ($hash_base);
4344        my %co = parse_commit($hash_base)
4345                or die_error(404, "Commit not found");
4346        if (!defined $hash) {
4347                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4348                        or die_error(404, "Error looking up file");
4349        }
4350        $ftype = git_get_type($hash);
4351        if ($ftype !~ "blob") {
4352                die_error(400, "Object is not a blob");
4353        }
4354        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4355              $file_name, $hash_base)
4356                or die_error(500, "Open git-blame failed");
4357        git_header_html();
4358        my $formats_nav =
4359                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4360                        "blob") .
4361                " | " .
4362                $cgi->a({-href => href(action=>"history", -replay=>1)},
4363                        "history") .
4364                " | " .
4365                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4366                        "HEAD");
4367        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4368        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4369        git_print_page_path($file_name, $ftype, $hash_base);
4370        my @rev_color = (qw(light2 dark2));
4371        my $num_colors = scalar(@rev_color);
4372        my $current_color = 0;
4373        my $last_rev;
4374        print <<HTML;
4375<div class="page_body">
4376<table class="blame">
4377<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4378HTML
4379        my %metainfo = ();
4380        while (1) {
4381                $_ = <$fd>;
4382                last unless defined $_;
4383                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4384                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4385                if (!exists $metainfo{$full_rev}) {
4386                        $metainfo{$full_rev} = {};
4387                }
4388                my $meta = $metainfo{$full_rev};
4389                while (<$fd>) {
4390                        last if (s/^\t//);
4391                        if (/^(\S+) (.*)$/) {
4392                                $meta->{$1} = $2;
4393                        }
4394                }
4395                my $data = $_;
4396                chomp $data;
4397                my $rev = substr($full_rev, 0, 8);
4398                my $author = $meta->{'author'};
4399                my %date = parse_date($meta->{'author-time'},
4400                                      $meta->{'author-tz'});
4401                my $date = $date{'iso-tz'};
4402                if ($group_size) {
4403                        $current_color = ++$current_color % $num_colors;
4404                }
4405                print "<tr class=\"$rev_color[$current_color]\">\n";
4406                if ($group_size) {
4407                        print "<td class=\"sha1\"";
4408                        print " title=\"". esc_html($author) . ", $date\"";
4409                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4410                        print ">";
4411                        print $cgi->a({-href => href(action=>"commit",
4412                                                     hash=>$full_rev,
4413                                                     file_name=>$file_name)},
4414                                      esc_html($rev));
4415                        print "</td>\n";
4416                }
4417                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4418                        or die_error(500, "Open git-rev-parse failed");
4419                my $parent_commit = <$dd>;
4420                close $dd;
4421                chomp($parent_commit);
4422                my $blamed = href(action => 'blame',
4423                                  file_name => $meta->{'filename'},
4424                                  hash_base => $parent_commit);
4425                print "<td class=\"linenr\">";
4426                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4427                                -id => "l$lineno",
4428                                -class => "linenr" },
4429                              esc_html($lineno));
4430                print "</td>";
4431                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4432                print "</tr>\n";
4433        }
4434        print "</table>\n";
4435        print "</div>";
4436        close $fd
4437                or print "Reading blob failed\n";
4438        git_footer_html();
4439}
4440
4441sub git_tags {
4442        my $head = git_get_head_hash($project);
4443        git_header_html();
4444        git_print_page_nav('','', $head,undef,$head);
4445        git_print_header_div('summary', $project);
4446
4447        my @tagslist = git_get_tags_list();
4448        if (@tagslist) {
4449                git_tags_body(\@tagslist);
4450        }
4451        git_footer_html();
4452}
4453
4454sub git_heads {
4455        my $head = git_get_head_hash($project);
4456        git_header_html();
4457        git_print_page_nav('','', $head,undef,$head);
4458        git_print_header_div('summary', $project);
4459
4460        my @headslist = git_get_heads_list();
4461        if (@headslist) {
4462                git_heads_body(\@headslist, $head);
4463        }
4464        git_footer_html();
4465}
4466
4467sub git_blob_plain {
4468        my $type = shift;
4469        my $expires;
4470
4471        if (!defined $hash) {
4472                if (defined $file_name) {
4473                        my $base = $hash_base || git_get_head_hash($project);
4474                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4475                                or die_error(404, "Cannot find file");
4476                } else {
4477                        die_error(400, "No file name defined");
4478                }
4479        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4480                # blobs defined by non-textual hash id's can be cached
4481                $expires = "+1d";
4482        }
4483
4484        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4485                or die_error(500, "Open git-cat-file blob '$hash' failed");
4486
4487        # content-type (can include charset)
4488        $type = blob_contenttype($fd, $file_name, $type);
4489
4490        # "save as" filename, even when no $file_name is given
4491        my $save_as = "$hash";
4492        if (defined $file_name) {
4493                $save_as = $file_name;
4494        } elsif ($type =~ m/^text\//) {
4495                $save_as .= '.txt';
4496        }
4497
4498        print $cgi->header(
4499                -type => $type,
4500                -expires => $expires,
4501                -content_disposition => 'inline; filename="' . $save_as . '"');
4502        undef $/;
4503        binmode STDOUT, ':raw';
4504        print <$fd>;
4505        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4506        $/ = "\n";
4507        close $fd;
4508}
4509
4510sub git_blob {
4511        my $expires;
4512
4513        if (!defined $hash) {
4514                if (defined $file_name) {
4515                        my $base = $hash_base || git_get_head_hash($project);
4516                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4517                                or die_error(404, "Cannot find file");
4518                } else {
4519                        die_error(400, "No file name defined");
4520                }
4521        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4522                # blobs defined by non-textual hash id's can be cached
4523                $expires = "+1d";
4524        }
4525
4526        my ($have_blame) = gitweb_check_feature('blame');
4527        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4528                or die_error(500, "Couldn't cat $file_name, $hash");
4529        my $mimetype = blob_mimetype($fd, $file_name);
4530        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4531                close $fd;
4532                return git_blob_plain($mimetype);
4533        }
4534        # we can have blame only for text/* mimetype
4535        $have_blame &&= ($mimetype =~ m!^text/!);
4536
4537        git_header_html(undef, $expires);
4538        my $formats_nav = '';
4539        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4540                if (defined $file_name) {
4541                        if ($have_blame) {
4542                                $formats_nav .=
4543                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4544                                                "blame") .
4545                                        " | ";
4546                        }
4547                        $formats_nav .=
4548                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4549                                        "history") .
4550                                " | " .
4551                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4552                                        "raw") .
4553                                " | " .
4554                                $cgi->a({-href => href(action=>"blob",
4555                                                       hash_base=>"HEAD", file_name=>$file_name)},
4556                                        "HEAD");
4557                } else {
4558                        $formats_nav .=
4559                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4560                                        "raw");
4561                }
4562                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4563                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4564        } else {
4565                print "<div class=\"page_nav\">\n" .
4566                      "<br/><br/></div>\n" .
4567                      "<div class=\"title\">$hash</div>\n";
4568        }
4569        git_print_page_path($file_name, "blob", $hash_base);
4570        print "<div class=\"page_body\">\n";
4571        if ($mimetype =~ m!^image/!) {
4572                print qq!<img type="$mimetype"!;
4573                if ($file_name) {
4574                        print qq! alt="$file_name" title="$file_name"!;
4575                }
4576                print qq! src="! .
4577                      href(action=>"blob_plain", hash=>$hash,
4578                           hash_base=>$hash_base, file_name=>$file_name) .
4579                      qq!" />\n!;
4580        } else {
4581                my $nr;
4582                while (my $line = <$fd>) {
4583                        chomp $line;
4584                        $nr++;
4585                        $line = untabify($line);
4586                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4587                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4588                }
4589        }
4590        close $fd
4591                or print "Reading blob failed.\n";
4592        print "</div>";
4593        git_footer_html();
4594}
4595
4596sub git_tree {
4597        if (!defined $hash_base) {
4598                $hash_base = "HEAD";
4599        }
4600        if (!defined $hash) {
4601                if (defined $file_name) {
4602                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4603                } else {
4604                        $hash = $hash_base;
4605                }
4606        }
4607        die_error(404, "No such tree") unless defined($hash);
4608        $/ = "\0";
4609        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4610                or die_error(500, "Open git-ls-tree failed");
4611        my @entries = map { chomp; $_ } <$fd>;
4612        close $fd or die_error(404, "Reading tree failed");
4613        $/ = "\n";
4614
4615        my $refs = git_get_references();
4616        my $ref = format_ref_marker($refs, $hash_base);
4617        git_header_html();
4618        my $basedir = '';
4619        my ($have_blame) = gitweb_check_feature('blame');
4620        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4621                my @views_nav = ();
4622                if (defined $file_name) {
4623                        push @views_nav,
4624                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4625                                        "history"),
4626                                $cgi->a({-href => href(action=>"tree",
4627                                                       hash_base=>"HEAD", file_name=>$file_name)},
4628                                        "HEAD"),
4629                }
4630                my $snapshot_links = format_snapshot_links($hash);
4631                if (defined $snapshot_links) {
4632                        # FIXME: Should be available when we have no hash base as well.
4633                        push @views_nav, $snapshot_links;
4634                }
4635                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4636                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4637        } else {
4638                undef $hash_base;
4639                print "<div class=\"page_nav\">\n";
4640                print "<br/><br/></div>\n";
4641                print "<div class=\"title\">$hash</div>\n";
4642        }
4643        if (defined $file_name) {
4644                $basedir = $file_name;
4645                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4646                        $basedir .= '/';
4647                }
4648                git_print_page_path($file_name, 'tree', $hash_base);
4649        }
4650        print "<div class=\"page_body\">\n";
4651        print "<table class=\"tree\">\n";
4652        my $alternate = 1;
4653        # '..' (top directory) link if possible
4654        if (defined $hash_base &&
4655            defined $file_name && $file_name =~ m![^/]+$!) {
4656                if ($alternate) {
4657                        print "<tr class=\"dark\">\n";
4658                } else {
4659                        print "<tr class=\"light\">\n";
4660                }
4661                $alternate ^= 1;
4662
4663                my $up = $file_name;
4664                $up =~ s!/?[^/]+$!!;
4665                undef $up unless $up;
4666                # based on git_print_tree_entry
4667                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4668                print '<td class="list">';
4669                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4670                                             file_name=>$up)},
4671                              "..");
4672                print "</td>\n";
4673                print "<td class=\"link\"></td>\n";
4674
4675                print "</tr>\n";
4676        }
4677        foreach my $line (@entries) {
4678                my %t = parse_ls_tree_line($line, -z => 1);
4679
4680                if ($alternate) {
4681                        print "<tr class=\"dark\">\n";
4682                } else {
4683                        print "<tr class=\"light\">\n";
4684                }
4685                $alternate ^= 1;
4686
4687                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4688
4689                print "</tr>\n";
4690        }
4691        print "</table>\n" .
4692              "</div>";
4693        git_footer_html();
4694}
4695
4696sub git_snapshot {
4697        my @supported_fmts = gitweb_check_feature('snapshot');
4698        @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4699
4700        my $format = $cgi->param('sf');
4701        if (!@supported_fmts) {
4702                die_error(403, "Snapshots not allowed");
4703        }
4704        # default to first supported snapshot format
4705        $format ||= $supported_fmts[0];
4706        if ($format !~ m/^[a-z0-9]+$/) {
4707                die_error(400, "Invalid snapshot format parameter");
4708        } elsif (!exists($known_snapshot_formats{$format})) {
4709                die_error(400, "Unknown snapshot format");
4710        } elsif (!grep($_ eq $format, @supported_fmts)) {
4711                die_error(403, "Unsupported snapshot format");
4712        }
4713
4714        if (!defined $hash) {
4715                $hash = git_get_head_hash($project);
4716        }
4717
4718        my $name = $project;
4719        $name =~ s,([^/])/*\.git$,$1,;
4720        $name = basename($name);
4721        my $filename = to_utf8($name);
4722        $name =~ s/\047/\047\\\047\047/g;
4723        my $cmd;
4724        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4725        $cmd = quote_command(
4726                git_cmd(), 'archive',
4727                "--format=$known_snapshot_formats{$format}{'format'}",
4728                "--prefix=$name/", $hash);
4729        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4730                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4731        }
4732
4733        print $cgi->header(
4734                -type => $known_snapshot_formats{$format}{'type'},
4735                -content_disposition => 'inline; filename="' . "$filename" . '"',
4736                -status => '200 OK');
4737
4738        open my $fd, "-|", $cmd
4739                or die_error(500, "Execute git-archive failed");
4740        binmode STDOUT, ':raw';
4741        print <$fd>;
4742        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4743        close $fd;
4744}
4745
4746sub git_log {
4747        my $head = git_get_head_hash($project);
4748        if (!defined $hash) {
4749                $hash = $head;
4750        }
4751        if (!defined $page) {
4752                $page = 0;
4753        }
4754        my $refs = git_get_references();
4755
4756        my @commitlist = parse_commits($hash, 101, (100 * $page));
4757
4758        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4759
4760        git_header_html();
4761        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4762
4763        if (!@commitlist) {
4764                my %co = parse_commit($hash);
4765
4766                git_print_header_div('summary', $project);
4767                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4768        }
4769        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4770        for (my $i = 0; $i <= $to; $i++) {
4771                my %co = %{$commitlist[$i]};
4772                next if !%co;
4773                my $commit = $co{'id'};
4774                my $ref = format_ref_marker($refs, $commit);
4775                my %ad = parse_date($co{'author_epoch'});
4776                git_print_header_div('commit',
4777                               "<span class=\"age\">$co{'age_string'}</span>" .
4778                               esc_html($co{'title'}) . $ref,
4779                               $commit);
4780                print "<div class=\"title_text\">\n" .
4781                      "<div class=\"log_link\">\n" .
4782                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4783                      " | " .
4784                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4785                      " | " .
4786                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4787                      "<br/>\n" .
4788                      "</div>\n" .
4789                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4790                      "</div>\n";
4791
4792                print "<div class=\"log_body\">\n";
4793                git_print_log($co{'comment'}, -final_empty_line=> 1);
4794                print "</div>\n";
4795        }
4796        if ($#commitlist >= 100) {
4797                print "<div class=\"page_nav\">\n";
4798                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4799                               -accesskey => "n", -title => "Alt-n"}, "next");
4800                print "</div>\n";
4801        }
4802        git_footer_html();
4803}
4804
4805sub git_commit {
4806        $hash ||= $hash_base || "HEAD";
4807        my %co = parse_commit($hash)
4808            or die_error(404, "Unknown commit object");
4809        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4810        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4811
4812        my $parent  = $co{'parent'};
4813        my $parents = $co{'parents'}; # listref
4814
4815        # we need to prepare $formats_nav before any parameter munging
4816        my $formats_nav;
4817        if (!defined $parent) {
4818                # --root commitdiff
4819                $formats_nav .= '(initial)';
4820        } elsif (@$parents == 1) {
4821                # single parent commit
4822                $formats_nav .=
4823                        '(parent: ' .
4824                        $cgi->a({-href => href(action=>"commit",
4825                                               hash=>$parent)},
4826                                esc_html(substr($parent, 0, 7))) .
4827                        ')';
4828        } else {
4829                # merge commit
4830                $formats_nav .=
4831                        '(merge: ' .
4832                        join(' ', map {
4833                                $cgi->a({-href => href(action=>"commit",
4834                                                       hash=>$_)},
4835                                        esc_html(substr($_, 0, 7)));
4836                        } @$parents ) .
4837                        ')';
4838        }
4839
4840        if (!defined $parent) {
4841                $parent = "--root";
4842        }
4843        my @difftree;
4844        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4845                @diff_opts,
4846                (@$parents <= 1 ? $parent : '-c'),
4847                $hash, "--"
4848                or die_error(500, "Open git-diff-tree failed");
4849        @difftree = map { chomp; $_ } <$fd>;
4850        close $fd or die_error(404, "Reading git-diff-tree failed");
4851
4852        # non-textual hash id's can be cached
4853        my $expires;
4854        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4855                $expires = "+1d";
4856        }
4857        my $refs = git_get_references();
4858        my $ref = format_ref_marker($refs, $co{'id'});
4859
4860        git_header_html(undef, $expires);
4861        git_print_page_nav('commit', '',
4862                           $hash, $co{'tree'}, $hash,
4863                           $formats_nav);
4864
4865        if (defined $co{'parent'}) {
4866                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4867        } else {
4868                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4869        }
4870        print "<div class=\"title_text\">\n" .
4871              "<table class=\"object_header\">\n";
4872        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4873              "<tr>" .
4874              "<td></td><td> $ad{'rfc2822'}";
4875        if ($ad{'hour_local'} < 6) {
4876                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4877                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4878        } else {
4879                printf(" (%02d:%02d %s)",
4880                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4881        }
4882        print "</td>" .
4883              "</tr>\n";
4884        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4885        print "<tr><td></td><td> $cd{'rfc2822'}" .
4886              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4887              "</td></tr>\n";
4888        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4889        print "<tr>" .
4890              "<td>tree</td>" .
4891              "<td class=\"sha1\">" .
4892              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4893                       class => "list"}, $co{'tree'}) .
4894              "</td>" .
4895              "<td class=\"link\">" .
4896              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4897                      "tree");
4898        my $snapshot_links = format_snapshot_links($hash);
4899        if (defined $snapshot_links) {
4900                print " | " . $snapshot_links;
4901        }
4902        print "</td>" .
4903              "</tr>\n";
4904
4905        foreach my $par (@$parents) {
4906                print "<tr>" .
4907                      "<td>parent</td>" .
4908                      "<td class=\"sha1\">" .
4909                      $cgi->a({-href => href(action=>"commit", hash=>$par),
4910                               class => "list"}, $par) .
4911                      "</td>" .
4912                      "<td class=\"link\">" .
4913                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4914                      " | " .
4915                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4916                      "</td>" .
4917                      "</tr>\n";
4918        }
4919        print "</table>".
4920              "</div>\n";
4921
4922        print "<div class=\"page_body\">\n";
4923        git_print_log($co{'comment'});
4924        print "</div>\n";
4925
4926        git_difftree_body(\@difftree, $hash, @$parents);
4927
4928        git_footer_html();
4929}
4930
4931sub git_object {
4932        # object is defined by:
4933        # - hash or hash_base alone
4934        # - hash_base and file_name
4935        my $type;
4936
4937        # - hash or hash_base alone
4938        if ($hash || ($hash_base && !defined $file_name)) {
4939                my $object_id = $hash || $hash_base;
4940
4941                open my $fd, "-|", quote_command(
4942                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4943                        or die_error(404, "Object does not exist");
4944                $type = <$fd>;
4945                chomp $type;
4946                close $fd
4947                        or die_error(404, "Object does not exist");
4948
4949        # - hash_base and file_name
4950        } elsif ($hash_base && defined $file_name) {
4951                $file_name =~ s,/+$,,;
4952
4953                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4954                        or die_error(404, "Base object does not exist");
4955
4956                # here errors should not hapen
4957                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4958                        or die_error(500, "Open git-ls-tree failed");
4959                my $line = <$fd>;
4960                close $fd;
4961
4962                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4963                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4964                        die_error(404, "File or directory for given base does not exist");
4965                }
4966                $type = $2;
4967                $hash = $3;
4968        } else {
4969                die_error(400, "Not enough information to find object");
4970        }
4971
4972        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4973                                          hash=>$hash, hash_base=>$hash_base,
4974                                          file_name=>$file_name),
4975                             -status => '302 Found');
4976}
4977
4978sub git_blobdiff {
4979        my $format = shift || 'html';
4980
4981        my $fd;
4982        my @difftree;
4983        my %diffinfo;
4984        my $expires;
4985
4986        # preparing $fd and %diffinfo for git_patchset_body
4987        # new style URI
4988        if (defined $hash_base && defined $hash_parent_base) {
4989                if (defined $file_name) {
4990                        # read raw output
4991                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4992                                $hash_parent_base, $hash_base,
4993                                "--", (defined $file_parent ? $file_parent : ()), $file_name
4994                                or die_error(500, "Open git-diff-tree failed");
4995                        @difftree = map { chomp; $_ } <$fd>;
4996                        close $fd
4997                                or die_error(404, "Reading git-diff-tree failed");
4998                        @difftree
4999                                or die_error(404, "Blob diff not found");
5000
5001                } elsif (defined $hash &&
5002                         $hash =~ /[0-9a-fA-F]{40}/) {
5003                        # try to find filename from $hash
5004
5005                        # read filtered raw output
5006                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5007                                $hash_parent_base, $hash_base, "--"
5008                                or die_error(500, "Open git-diff-tree failed");
5009                        @difftree =
5010                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5011                                # $hash == to_id
5012                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5013                                map { chomp; $_ } <$fd>;
5014                        close $fd
5015                                or die_error(404, "Reading git-diff-tree failed");
5016                        @difftree
5017                                or die_error(404, "Blob diff not found");
5018
5019                } else {
5020                        die_error(400, "Missing one of the blob diff parameters");
5021                }
5022
5023                if (@difftree > 1) {
5024                        die_error(400, "Ambiguous blob diff specification");
5025                }
5026
5027                %diffinfo = parse_difftree_raw_line($difftree[0]);
5028                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5029                $file_name   ||= $diffinfo{'to_file'};
5030
5031                $hash_parent ||= $diffinfo{'from_id'};
5032                $hash        ||= $diffinfo{'to_id'};
5033
5034                # non-textual hash id's can be cached
5035                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5036                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5037                        $expires = '+1d';
5038                }
5039
5040                # open patch output
5041                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5042                        '-p', ($format eq 'html' ? "--full-index" : ()),
5043                        $hash_parent_base, $hash_base,
5044                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5045                        or die_error(500, "Open git-diff-tree failed");
5046        }
5047
5048        # old/legacy style URI
5049        if (!%diffinfo && # if new style URI failed
5050            defined $hash && defined $hash_parent) {
5051                # fake git-diff-tree raw output
5052                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5053                $diffinfo{'from_id'} = $hash_parent;
5054                $diffinfo{'to_id'}   = $hash;
5055                if (defined $file_name) {
5056                        if (defined $file_parent) {
5057                                $diffinfo{'status'} = '2';
5058                                $diffinfo{'from_file'} = $file_parent;
5059                                $diffinfo{'to_file'}   = $file_name;
5060                        } else { # assume not renamed
5061                                $diffinfo{'status'} = '1';
5062                                $diffinfo{'from_file'} = $file_name;
5063                                $diffinfo{'to_file'}   = $file_name;
5064                        }
5065                } else { # no filename given
5066                        $diffinfo{'status'} = '2';
5067                        $diffinfo{'from_file'} = $hash_parent;
5068                        $diffinfo{'to_file'}   = $hash;
5069                }
5070
5071                # non-textual hash id's can be cached
5072                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5073                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5074                        $expires = '+1d';
5075                }
5076
5077                # open patch output
5078                open $fd, "-|", git_cmd(), "diff", @diff_opts,
5079                        '-p', ($format eq 'html' ? "--full-index" : ()),
5080                        $hash_parent, $hash, "--"
5081                        or die_error(500, "Open git-diff failed");
5082        } else  {
5083                die_error(400, "Missing one of the blob diff parameters")
5084                        unless %diffinfo;
5085        }
5086
5087        # header
5088        if ($format eq 'html') {
5089                my $formats_nav =
5090                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5091                                "raw");
5092                git_header_html(undef, $expires);
5093                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5094                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5095                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5096                } else {
5097                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5098                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5099                }
5100                if (defined $file_name) {
5101                        git_print_page_path($file_name, "blob", $hash_base);
5102                } else {
5103                        print "<div class=\"page_path\"></div>\n";
5104                }
5105
5106        } elsif ($format eq 'plain') {
5107                print $cgi->header(
5108                        -type => 'text/plain',
5109                        -charset => 'utf-8',
5110                        -expires => $expires,
5111                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5112
5113                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5114
5115        } else {
5116                die_error(400, "Unknown blobdiff format");
5117        }
5118
5119        # patch
5120        if ($format eq 'html') {
5121                print "<div class=\"page_body\">\n";
5122
5123                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5124                close $fd;
5125
5126                print "</div>\n"; # class="page_body"
5127                git_footer_html();
5128
5129        } else {
5130                while (my $line = <$fd>) {
5131                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5132                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5133
5134                        print $line;
5135
5136                        last if $line =~ m!^\+\+\+!;
5137                }
5138                local $/ = undef;
5139                print <$fd>;
5140                close $fd;
5141        }
5142}
5143
5144sub git_blobdiff_plain {
5145        git_blobdiff('plain');
5146}
5147
5148sub git_commitdiff {
5149        my $format = shift || 'html';
5150        $hash ||= $hash_base || "HEAD";
5151        my %co = parse_commit($hash)
5152            or die_error(404, "Unknown commit object");
5153
5154        # choose format for commitdiff for merge
5155        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5156                $hash_parent = '--cc';
5157        }
5158        # we need to prepare $formats_nav before almost any parameter munging
5159        my $formats_nav;
5160        if ($format eq 'html') {
5161                $formats_nav =
5162                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5163                                "raw");
5164
5165                if (defined $hash_parent &&
5166                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5167                        # commitdiff with two commits given
5168                        my $hash_parent_short = $hash_parent;
5169                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5170                                $hash_parent_short = substr($hash_parent, 0, 7);
5171                        }
5172                        $formats_nav .=
5173                                ' (from';
5174                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5175                                if ($co{'parents'}[$i] eq $hash_parent) {
5176                                        $formats_nav .= ' parent ' . ($i+1);
5177                                        last;
5178                                }
5179                        }
5180                        $formats_nav .= ': ' .
5181                                $cgi->a({-href => href(action=>"commitdiff",
5182                                                       hash=>$hash_parent)},
5183                                        esc_html($hash_parent_short)) .
5184                                ')';
5185                } elsif (!$co{'parent'}) {
5186                        # --root commitdiff
5187                        $formats_nav .= ' (initial)';
5188                } elsif (scalar @{$co{'parents'}} == 1) {
5189                        # single parent commit
5190                        $formats_nav .=
5191                                ' (parent: ' .
5192                                $cgi->a({-href => href(action=>"commitdiff",
5193                                                       hash=>$co{'parent'})},
5194                                        esc_html(substr($co{'parent'}, 0, 7))) .
5195                                ')';
5196                } else {
5197                        # merge commit
5198                        if ($hash_parent eq '--cc') {
5199                                $formats_nav .= ' | ' .
5200                                        $cgi->a({-href => href(action=>"commitdiff",
5201                                                               hash=>$hash, hash_parent=>'-c')},
5202                                                'combined');
5203                        } else { # $hash_parent eq '-c'
5204                                $formats_nav .= ' | ' .
5205                                        $cgi->a({-href => href(action=>"commitdiff",
5206                                                               hash=>$hash, hash_parent=>'--cc')},
5207                                                'compact');
5208                        }
5209                        $formats_nav .=
5210                                ' (merge: ' .
5211                                join(' ', map {
5212                                        $cgi->a({-href => href(action=>"commitdiff",
5213                                                               hash=>$_)},
5214                                                esc_html(substr($_, 0, 7)));
5215                                } @{$co{'parents'}} ) .
5216                                ')';
5217                }
5218        }
5219
5220        my $hash_parent_param = $hash_parent;
5221        if (!defined $hash_parent_param) {
5222                # --cc for multiple parents, --root for parentless
5223                $hash_parent_param =
5224                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5225        }
5226
5227        # read commitdiff
5228        my $fd;
5229        my @difftree;
5230        if ($format eq 'html') {
5231                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5232                        "--no-commit-id", "--patch-with-raw", "--full-index",
5233                        $hash_parent_param, $hash, "--"
5234                        or die_error(500, "Open git-diff-tree failed");
5235
5236                while (my $line = <$fd>) {
5237                        chomp $line;
5238                        # empty line ends raw part of diff-tree output
5239                        last unless $line;
5240                        push @difftree, scalar parse_difftree_raw_line($line);
5241                }
5242
5243        } elsif ($format eq 'plain') {
5244                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5245                        '-p', $hash_parent_param, $hash, "--"
5246                        or die_error(500, "Open git-diff-tree failed");
5247
5248        } else {
5249                die_error(400, "Unknown commitdiff format");
5250        }
5251
5252        # non-textual hash id's can be cached
5253        my $expires;
5254        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5255                $expires = "+1d";
5256        }
5257
5258        # write commit message
5259        if ($format eq 'html') {
5260                my $refs = git_get_references();
5261                my $ref = format_ref_marker($refs, $co{'id'});
5262
5263                git_header_html(undef, $expires);
5264                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5265                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5266                git_print_authorship(\%co);
5267                print "<div class=\"page_body\">\n";
5268                if (@{$co{'comment'}} > 1) {
5269                        print "<div class=\"log\">\n";
5270                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5271                        print "</div>\n"; # class="log"
5272                }
5273
5274        } elsif ($format eq 'plain') {
5275                my $refs = git_get_references("tags");
5276                my $tagname = git_get_rev_name_tags($hash);
5277                my $filename = basename($project) . "-$hash.patch";
5278
5279                print $cgi->header(
5280                        -type => 'text/plain',
5281                        -charset => 'utf-8',
5282                        -expires => $expires,
5283                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5284                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5285                print "From: " . to_utf8($co{'author'}) . "\n";
5286                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5287                print "Subject: " . to_utf8($co{'title'}) . "\n";
5288
5289                print "X-Git-Tag: $tagname\n" if $tagname;
5290                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5291
5292                foreach my $line (@{$co{'comment'}}) {
5293                        print to_utf8($line) . "\n";
5294                }
5295                print "---\n\n";
5296        }
5297
5298        # write patch
5299        if ($format eq 'html') {
5300                my $use_parents = !defined $hash_parent ||
5301                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5302                git_difftree_body(\@difftree, $hash,
5303                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5304                print "<br/>\n";
5305
5306                git_patchset_body($fd, \@difftree, $hash,
5307                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5308                close $fd;
5309                print "</div>\n"; # class="page_body"
5310                git_footer_html();
5311
5312        } elsif ($format eq 'plain') {
5313                local $/ = undef;
5314                print <$fd>;
5315                close $fd
5316                        or print "Reading git-diff-tree failed\n";
5317        }
5318}
5319
5320sub git_commitdiff_plain {
5321        git_commitdiff('plain');
5322}
5323
5324sub git_history {
5325        if (!defined $hash_base) {
5326                $hash_base = git_get_head_hash($project);
5327        }
5328        if (!defined $page) {
5329                $page = 0;
5330        }
5331        my $ftype;
5332        my %co = parse_commit($hash_base)
5333            or die_error(404, "Unknown commit object");
5334
5335        my $refs = git_get_references();
5336        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5337
5338        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5339                                       $file_name, "--full-history")
5340            or die_error(404, "No such file or directory on given branch");
5341
5342        if (!defined $hash && defined $file_name) {
5343                # some commits could have deleted file in question,
5344                # and not have it in tree, but one of them has to have it
5345                for (my $i = 0; $i <= @commitlist; $i++) {
5346                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5347                        last if defined $hash;
5348                }
5349        }
5350        if (defined $hash) {
5351                $ftype = git_get_type($hash);
5352        }
5353        if (!defined $ftype) {
5354                die_error(500, "Unknown type of object");
5355        }
5356
5357        my $paging_nav = '';
5358        if ($page > 0) {
5359                $paging_nav .=
5360                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5361                                               file_name=>$file_name)},
5362                                "first");
5363                $paging_nav .= " &sdot; " .
5364                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5365                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5366        } else {
5367                $paging_nav .= "first";
5368                $paging_nav .= " &sdot; prev";
5369        }
5370        my $next_link = '';
5371        if ($#commitlist >= 100) {
5372                $next_link =
5373                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5374                                 -accesskey => "n", -title => "Alt-n"}, "next");
5375                $paging_nav .= " &sdot; $next_link";
5376        } else {
5377                $paging_nav .= " &sdot; next";
5378        }
5379
5380        git_header_html();
5381        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5382        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5383        git_print_page_path($file_name, $ftype, $hash_base);
5384
5385        git_history_body(\@commitlist, 0, 99,
5386                         $refs, $hash_base, $ftype, $next_link);
5387
5388        git_footer_html();
5389}
5390
5391sub git_search {
5392        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5393        if (!defined $searchtext) {
5394                die_error(400, "Text field is empty");
5395        }
5396        if (!defined $hash) {
5397                $hash = git_get_head_hash($project);
5398        }
5399        my %co = parse_commit($hash);
5400        if (!%co) {
5401                die_error(404, "Unknown commit object");
5402        }
5403        if (!defined $page) {
5404                $page = 0;
5405        }
5406
5407        $searchtype ||= 'commit';
5408        if ($searchtype eq 'pickaxe') {
5409                # pickaxe may take all resources of your box and run for several minutes
5410                # with every query - so decide by yourself how public you make this feature
5411                gitweb_check_feature('pickaxe')
5412                    or die_error(403, "Pickaxe is disabled");
5413        }
5414        if ($searchtype eq 'grep') {
5415                gitweb_check_feature('grep')
5416                    or die_error(403, "Grep is disabled");
5417        }
5418
5419        git_header_html();
5420
5421        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5422                my $greptype;
5423                if ($searchtype eq 'commit') {
5424                        $greptype = "--grep=";
5425                } elsif ($searchtype eq 'author') {
5426                        $greptype = "--author=";
5427                } elsif ($searchtype eq 'committer') {
5428                        $greptype = "--committer=";
5429                }
5430                $greptype .= $searchtext;
5431                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5432                                               $greptype, '--regexp-ignore-case',
5433                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5434
5435                my $paging_nav = '';
5436                if ($page > 0) {
5437                        $paging_nav .=
5438                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5439                                                       searchtext=>$searchtext,
5440                                                       searchtype=>$searchtype)},
5441                                        "first");
5442                        $paging_nav .= " &sdot; " .
5443                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5444                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5445                } else {
5446                        $paging_nav .= "first";
5447                        $paging_nav .= " &sdot; prev";
5448                }
5449                my $next_link = '';
5450                if ($#commitlist >= 100) {
5451                        $next_link =
5452                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5453                                         -accesskey => "n", -title => "Alt-n"}, "next");
5454                        $paging_nav .= " &sdot; $next_link";
5455                } else {
5456                        $paging_nav .= " &sdot; next";
5457                }
5458
5459                if ($#commitlist >= 100) {
5460                }
5461
5462                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5463                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5464                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5465        }
5466
5467        if ($searchtype eq 'pickaxe') {
5468                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5469                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5470
5471                print "<table class=\"pickaxe search\">\n";
5472                my $alternate = 1;
5473                $/ = "\n";
5474                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5475                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5476                        ($search_use_regexp ? '--pickaxe-regex' : ());
5477                undef %co;
5478                my @files;
5479                while (my $line = <$fd>) {
5480                        chomp $line;
5481                        next unless $line;
5482
5483                        my %set = parse_difftree_raw_line($line);
5484                        if (defined $set{'commit'}) {
5485                                # finish previous commit
5486                                if (%co) {
5487                                        print "</td>\n" .
5488                                              "<td class=\"link\">" .
5489                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5490                                              " | " .
5491                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5492                                        print "</td>\n" .
5493                                              "</tr>\n";
5494                                }
5495
5496                                if ($alternate) {
5497                                        print "<tr class=\"dark\">\n";
5498                                } else {
5499                                        print "<tr class=\"light\">\n";
5500                                }
5501                                $alternate ^= 1;
5502                                %co = parse_commit($set{'commit'});
5503                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5504                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5505                                      "<td><i>$author</i></td>\n" .
5506                                      "<td>" .
5507                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5508                                              -class => "list subject"},
5509                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5510                        } elsif (defined $set{'to_id'}) {
5511                                next if ($set{'to_id'} =~ m/^0{40}$/);
5512
5513                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5514                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5515                                              -class => "list"},
5516                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5517                                      "<br/>\n";
5518                        }
5519                }
5520                close $fd;
5521
5522                # finish last commit (warning: repetition!)
5523                if (%co) {
5524                        print "</td>\n" .
5525                              "<td class=\"link\">" .
5526                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5527                              " | " .
5528                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5529                        print "</td>\n" .
5530                              "</tr>\n";
5531                }
5532
5533                print "</table>\n";
5534        }
5535
5536        if ($searchtype eq 'grep') {
5537                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5538                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5539
5540                print "<table class=\"grep_search\">\n";
5541                my $alternate = 1;
5542                my $matches = 0;
5543                $/ = "\n";
5544                open my $fd, "-|", git_cmd(), 'grep', '-n',
5545                        $search_use_regexp ? ('-E', '-i') : '-F',
5546                        $searchtext, $co{'tree'};
5547                my $lastfile = '';
5548                while (my $line = <$fd>) {
5549                        chomp $line;
5550                        my ($file, $lno, $ltext, $binary);
5551                        last if ($matches++ > 1000);
5552                        if ($line =~ /^Binary file (.+) matches$/) {
5553                                $file = $1;
5554                                $binary = 1;
5555                        } else {
5556                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5557                        }
5558                        if ($file ne $lastfile) {
5559                                $lastfile and print "</td></tr>\n";
5560                                if ($alternate++) {
5561                                        print "<tr class=\"dark\">\n";
5562                                } else {
5563                                        print "<tr class=\"light\">\n";
5564                                }
5565                                print "<td class=\"list\">".
5566                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5567                                                               file_name=>"$file"),
5568                                                -class => "list"}, esc_path($file));
5569                                print "</td><td>\n";
5570                                $lastfile = $file;
5571                        }
5572                        if ($binary) {
5573                                print "<div class=\"binary\">Binary file</div>\n";
5574                        } else {
5575                                $ltext = untabify($ltext);
5576                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5577                                        $ltext = esc_html($1, -nbsp=>1);
5578                                        $ltext .= '<span class="match">';
5579                                        $ltext .= esc_html($2, -nbsp=>1);
5580                                        $ltext .= '</span>';
5581                                        $ltext .= esc_html($3, -nbsp=>1);
5582                                } else {
5583                                        $ltext = esc_html($ltext, -nbsp=>1);
5584                                }
5585                                print "<div class=\"pre\">" .
5586                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5587                                                               file_name=>"$file").'#l'.$lno,
5588                                                -class => "linenr"}, sprintf('%4i', $lno))
5589                                        . ' ' .  $ltext . "</div>\n";
5590                        }
5591                }
5592                if ($lastfile) {
5593                        print "</td></tr>\n";
5594                        if ($matches > 1000) {
5595                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5596                        }
5597                } else {
5598                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5599                }
5600                close $fd;
5601
5602                print "</table>\n";
5603        }
5604        git_footer_html();
5605}
5606
5607sub git_search_help {
5608        git_header_html();
5609        git_print_page_nav('','', $hash,$hash,$hash);
5610        print <<EOT;
5611<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5612regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5613the pattern entered is recognized as the POSIX extended
5614<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5615insensitive).</p>
5616<dl>
5617<dt><b>commit</b></dt>
5618<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5619EOT
5620        my ($have_grep) = gitweb_check_feature('grep');
5621        if ($have_grep) {
5622                print <<EOT;
5623<dt><b>grep</b></dt>
5624<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5625    a different one) are searched for the given pattern. On large trees, this search can take
5626a while and put some strain on the server, so please use it with some consideration. Note that
5627due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5628case-sensitive.</dd>
5629EOT
5630        }
5631        print <<EOT;
5632<dt><b>author</b></dt>
5633<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5634<dt><b>committer</b></dt>
5635<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5636EOT
5637        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5638        if ($have_pickaxe) {
5639                print <<EOT;
5640<dt><b>pickaxe</b></dt>
5641<dd>All commits that caused the string to appear or disappear from any file (changes that
5642added, removed or "modified" the string) will be listed. This search can take a while and
5643takes a lot of strain on the server, so please use it wisely. Note that since you may be
5644interested even in changes just changing the case as well, this search is case sensitive.</dd>
5645EOT
5646        }
5647        print "</dl>\n";
5648        git_footer_html();
5649}
5650
5651sub git_shortlog {
5652        my $head = git_get_head_hash($project);
5653        if (!defined $hash) {
5654                $hash = $head;
5655        }
5656        if (!defined $page) {
5657                $page = 0;
5658        }
5659        my $refs = git_get_references();
5660
5661        my $commit_hash = $hash;
5662        if (defined $hash_parent) {
5663                $commit_hash = "$hash_parent..$hash";
5664        }
5665        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5666
5667        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5668        my $next_link = '';
5669        if ($#commitlist >= 100) {
5670                $next_link =
5671                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5672                                 -accesskey => "n", -title => "Alt-n"}, "next");
5673        }
5674
5675        git_header_html();
5676        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5677        git_print_header_div('summary', $project);
5678
5679        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5680
5681        git_footer_html();
5682}
5683
5684## ......................................................................
5685## feeds (RSS, Atom; OPML)
5686
5687sub git_feed {
5688        my $format = shift || 'atom';
5689        my ($have_blame) = gitweb_check_feature('blame');
5690
5691        # Atom: http://www.atomenabled.org/developers/syndication/
5692        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5693        if ($format ne 'rss' && $format ne 'atom') {
5694                die_error(400, "Unknown web feed format");
5695        }
5696
5697        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5698        my $head = $hash || 'HEAD';
5699        my @commitlist = parse_commits($head, 150, 0, $file_name);
5700
5701        my %latest_commit;
5702        my %latest_date;
5703        my $content_type = "application/$format+xml";
5704        if (defined $cgi->http('HTTP_ACCEPT') &&
5705                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5706                # browser (feed reader) prefers text/xml
5707                $content_type = 'text/xml';
5708        }
5709        if (defined($commitlist[0])) {
5710                %latest_commit = %{$commitlist[0]};
5711                %latest_date   = parse_date($latest_commit{'author_epoch'});
5712                print $cgi->header(
5713                        -type => $content_type,
5714                        -charset => 'utf-8',
5715                        -last_modified => $latest_date{'rfc2822'});
5716        } else {
5717                print $cgi->header(
5718                        -type => $content_type,
5719                        -charset => 'utf-8');
5720        }
5721
5722        # Optimization: skip generating the body if client asks only
5723        # for Last-Modified date.
5724        return if ($cgi->request_method() eq 'HEAD');
5725
5726        # header variables
5727        my $title = "$site_name - $project/$action";
5728        my $feed_type = 'log';
5729        if (defined $hash) {
5730                $title .= " - '$hash'";
5731                $feed_type = 'branch log';
5732                if (defined $file_name) {
5733                        $title .= " :: $file_name";
5734                        $feed_type = 'history';
5735                }
5736        } elsif (defined $file_name) {
5737                $title .= " - $file_name";
5738                $feed_type = 'history';
5739        }
5740        $title .= " $feed_type";
5741        my $descr = git_get_project_description($project);
5742        if (defined $descr) {
5743                $descr = esc_html($descr);
5744        } else {
5745                $descr = "$project " .
5746                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5747                         " feed";
5748        }
5749        my $owner = git_get_project_owner($project);
5750        $owner = esc_html($owner);
5751
5752        #header
5753        my $alt_url;
5754        if (defined $file_name) {
5755                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5756        } elsif (defined $hash) {
5757                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5758        } else {
5759                $alt_url = href(-full=>1, action=>"summary");
5760        }
5761        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5762        if ($format eq 'rss') {
5763                print <<XML;
5764<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5765<channel>
5766XML
5767                print "<title>$title</title>\n" .
5768                      "<link>$alt_url</link>\n" .
5769                      "<description>$descr</description>\n" .
5770                      "<language>en</language>\n";
5771        } elsif ($format eq 'atom') {
5772                print <<XML;
5773<feed xmlns="http://www.w3.org/2005/Atom">
5774XML
5775                print "<title>$title</title>\n" .
5776                      "<subtitle>$descr</subtitle>\n" .
5777                      '<link rel="alternate" type="text/html" href="' .
5778                      $alt_url . '" />' . "\n" .
5779                      '<link rel="self" type="' . $content_type . '" href="' .
5780                      $cgi->self_url() . '" />' . "\n" .
5781                      "<id>" . href(-full=>1) . "</id>\n" .
5782                      # use project owner for feed author
5783                      "<author><name>$owner</name></author>\n";
5784                if (defined $favicon) {
5785                        print "<icon>" . esc_url($favicon) . "</icon>\n";
5786                }
5787                if (defined $logo_url) {
5788                        # not twice as wide as tall: 72 x 27 pixels
5789                        print "<logo>" . esc_url($logo) . "</logo>\n";
5790                }
5791                if (! %latest_date) {
5792                        # dummy date to keep the feed valid until commits trickle in:
5793                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
5794                } else {
5795                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
5796                }
5797        }
5798
5799        # contents
5800        for (my $i = 0; $i <= $#commitlist; $i++) {
5801                my %co = %{$commitlist[$i]};
5802                my $commit = $co{'id'};
5803                # we read 150, we always show 30 and the ones more recent than 48 hours
5804                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5805                        last;
5806                }
5807                my %cd = parse_date($co{'author_epoch'});
5808
5809                # get list of changed files
5810                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5811                        $co{'parent'} || "--root",
5812                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
5813                        or next;
5814                my @difftree = map { chomp; $_ } <$fd>;
5815                close $fd
5816                        or next;
5817
5818                # print element (entry, item)
5819                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5820                if ($format eq 'rss') {
5821                        print "<item>\n" .
5822                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
5823                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
5824                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5825                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5826                              "<link>$co_url</link>\n" .
5827                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
5828                              "<content:encoded>" .
5829                              "<![CDATA[\n";
5830                } elsif ($format eq 'atom') {
5831                        print "<entry>\n" .
5832                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5833                              "<updated>$cd{'iso-8601'}</updated>\n" .
5834                              "<author>\n" .
5835                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5836                        if ($co{'author_email'}) {
5837                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5838                        }
5839                        print "</author>\n" .
5840                              # use committer for contributor
5841                              "<contributor>\n" .
5842                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5843                        if ($co{'committer_email'}) {
5844                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5845                        }
5846                        print "</contributor>\n" .
5847                              "<published>$cd{'iso-8601'}</published>\n" .
5848                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5849                              "<id>$co_url</id>\n" .
5850                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5851                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5852                }
5853                my $comment = $co{'comment'};
5854                print "<pre>\n";
5855                foreach my $line (@$comment) {
5856                        $line = esc_html($line);
5857                        print "$line\n";
5858                }
5859                print "</pre><ul>\n";
5860                foreach my $difftree_line (@difftree) {
5861                        my %difftree = parse_difftree_raw_line($difftree_line);
5862                        next if !$difftree{'from_id'};
5863
5864                        my $file = $difftree{'file'} || $difftree{'to_file'};
5865
5866                        print "<li>" .
5867                              "[" .
5868                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5869                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5870                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5871                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
5872                                      -title => "diff"}, 'D');
5873                        if ($have_blame) {
5874                                print $cgi->a({-href => href(-full=>1, action=>"blame",
5875                                                             file_name=>$file, hash_base=>$commit),
5876                                              -title => "blame"}, 'B');
5877                        }
5878                        # if this is not a feed of a file history
5879                        if (!defined $file_name || $file_name ne $file) {
5880                                print $cgi->a({-href => href(-full=>1, action=>"history",
5881                                                             file_name=>$file, hash=>$commit),
5882                                              -title => "history"}, 'H');
5883                        }
5884                        $file = esc_path($file);
5885                        print "] ".
5886                              "$file</li>\n";
5887                }
5888                if ($format eq 'rss') {
5889                        print "</ul>]]>\n" .
5890                              "</content:encoded>\n" .
5891                              "</item>\n";
5892                } elsif ($format eq 'atom') {
5893                        print "</ul>\n</div>\n" .
5894                              "</content>\n" .
5895                              "</entry>\n";
5896                }
5897        }
5898
5899        # end of feed
5900        if ($format eq 'rss') {
5901                print "</channel>\n</rss>\n";
5902        }       elsif ($format eq 'atom') {
5903                print "</feed>\n";
5904        }
5905}
5906
5907sub git_rss {
5908        git_feed('rss');
5909}
5910
5911sub git_atom {
5912        git_feed('atom');
5913}
5914
5915sub git_opml {
5916        my @list = git_get_projects_list();
5917
5918        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5919        print <<XML;
5920<?xml version="1.0" encoding="utf-8"?>
5921<opml version="1.0">
5922<head>
5923  <title>$site_name OPML Export</title>
5924</head>
5925<body>
5926<outline text="git RSS feeds">
5927XML
5928
5929        foreach my $pr (@list) {
5930                my %proj = %$pr;
5931                my $head = git_get_head_hash($proj{'path'});
5932                if (!defined $head) {
5933                        next;
5934                }
5935                $git_dir = "$projectroot/$proj{'path'}";
5936                my %co = parse_commit($head);
5937                if (!%co) {
5938                        next;
5939                }
5940
5941                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5942                my $rss  = "$my_url?p=$proj{'path'};a=rss";
5943                my $html = "$my_url?p=$proj{'path'};a=summary";
5944                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5945        }
5946        print <<XML;
5947</outline>
5948</body>
5949</opml>
5950XML
5951}