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