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