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