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