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