gitweb / gitweb.perlon commit Merge branch 'nd/narrow' (early part) into jc/add-i-t-a (fe60dff)
   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## ......................................................................
2744## mimetype related functions
2745
2746sub mimetype_guess_file {
2747        my $filename = shift;
2748        my $mimemap = shift;
2749        -r $mimemap or return undef;
2750
2751        my %mimemap;
2752        open(MIME, $mimemap) or return undef;
2753        while (<MIME>) {
2754                next if m/^#/; # skip comments
2755                my ($mime, $exts) = split(/\t+/);
2756                if (defined $exts) {
2757                        my @exts = split(/\s+/, $exts);
2758                        foreach my $ext (@exts) {
2759                                $mimemap{$ext} = $mime;
2760                        }
2761                }
2762        }
2763        close(MIME);
2764
2765        $filename =~ /\.([^.]*)$/;
2766        return $mimemap{$1};
2767}
2768
2769sub mimetype_guess {
2770        my $filename = shift;
2771        my $mime;
2772        $filename =~ /\./ or return undef;
2773
2774        if ($mimetypes_file) {
2775                my $file = $mimetypes_file;
2776                if ($file !~ m!^/!) { # if it is relative path
2777                        # it is relative to project
2778                        $file = "$projectroot/$project/$file";
2779                }
2780                $mime = mimetype_guess_file($filename, $file);
2781        }
2782        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2783        return $mime;
2784}
2785
2786sub blob_mimetype {
2787        my $fd = shift;
2788        my $filename = shift;
2789
2790        if ($filename) {
2791                my $mime = mimetype_guess($filename);
2792                $mime and return $mime;
2793        }
2794
2795        # just in case
2796        return $default_blob_plain_mimetype unless $fd;
2797
2798        if (-T $fd) {
2799                return 'text/plain';
2800        } elsif (! $filename) {
2801                return 'application/octet-stream';
2802        } elsif ($filename =~ m/\.png$/i) {
2803                return 'image/png';
2804        } elsif ($filename =~ m/\.gif$/i) {
2805                return 'image/gif';
2806        } elsif ($filename =~ m/\.jpe?g$/i) {
2807                return 'image/jpeg';
2808        } else {
2809                return 'application/octet-stream';
2810        }
2811}
2812
2813sub blob_contenttype {
2814        my ($fd, $file_name, $type) = @_;
2815
2816        $type ||= blob_mimetype($fd, $file_name);
2817        if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2818                $type .= "; charset=$default_text_plain_charset";
2819        }
2820
2821        return $type;
2822}
2823
2824## ======================================================================
2825## functions printing HTML: header, footer, error page
2826
2827sub git_header_html {
2828        my $status = shift || "200 OK";
2829        my $expires = shift;
2830
2831        my $title = "$site_name";
2832        if (defined $project) {
2833                $title .= " - " . to_utf8($project);
2834                if (defined $action) {
2835                        $title .= "/$action";
2836                        if (defined $file_name) {
2837                                $title .= " - " . esc_path($file_name);
2838                                if ($action eq "tree" && $file_name !~ m|/$|) {
2839                                        $title .= "/";
2840                                }
2841                        }
2842                }
2843        }
2844        my $content_type;
2845        # require explicit support from the UA if we are to send the page as
2846        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2847        # we have to do this because MSIE sometimes globs '*/*', pretending to
2848        # support xhtml+xml but choking when it gets what it asked for.
2849        if (defined $cgi->http('HTTP_ACCEPT') &&
2850            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2851            $cgi->Accept('application/xhtml+xml') != 0) {
2852                $content_type = 'application/xhtml+xml';
2853        } else {
2854                $content_type = 'text/html';
2855        }
2856        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2857                           -status=> $status, -expires => $expires);
2858        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2859        print <<EOF;
2860<?xml version="1.0" encoding="utf-8"?>
2861<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2862<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2863<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2864<!-- git core binaries version $git_version -->
2865<head>
2866<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2867<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2868<meta name="robots" content="index, nofollow"/>
2869<title>$title</title>
2870EOF
2871# print out each stylesheet that exist
2872        if (defined $stylesheet) {
2873#provides backwards capability for those people who define style sheet in a config file
2874                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2875        } else {
2876                foreach my $stylesheet (@stylesheets) {
2877                        next unless $stylesheet;
2878                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2879                }
2880        }
2881        if (defined $project) {
2882                my %href_params = get_feed_info();
2883                if (!exists $href_params{'-title'}) {
2884                        $href_params{'-title'} = 'log';
2885                }
2886
2887                foreach my $format qw(RSS Atom) {
2888                        my $type = lc($format);
2889                        my %link_attr = (
2890                                '-rel' => 'alternate',
2891                                '-title' => "$project - $href_params{'-title'} - $format feed",
2892                                '-type' => "application/$type+xml"
2893                        );
2894
2895                        $href_params{'action'} = $type;
2896                        $link_attr{'-href'} = href(%href_params);
2897                        print "<link ".
2898                              "rel=\"$link_attr{'-rel'}\" ".
2899                              "title=\"$link_attr{'-title'}\" ".
2900                              "href=\"$link_attr{'-href'}\" ".
2901                              "type=\"$link_attr{'-type'}\" ".
2902                              "/>\n";
2903
2904                        $href_params{'extra_options'} = '--no-merges';
2905                        $link_attr{'-href'} = href(%href_params);
2906                        $link_attr{'-title'} .= ' (no merges)';
2907                        print "<link ".
2908                              "rel=\"$link_attr{'-rel'}\" ".
2909                              "title=\"$link_attr{'-title'}\" ".
2910                              "href=\"$link_attr{'-href'}\" ".
2911                              "type=\"$link_attr{'-type'}\" ".
2912                              "/>\n";
2913                }
2914
2915        } else {
2916                printf('<link rel="alternate" title="%s projects list" '.
2917                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
2918                       $site_name, href(project=>undef, action=>"project_index"));
2919                printf('<link rel="alternate" title="%s projects feeds" '.
2920                       'href="%s" type="text/x-opml" />'."\n",
2921                       $site_name, href(project=>undef, action=>"opml"));
2922        }
2923        if (defined $favicon) {
2924                print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2925        }
2926
2927        print "</head>\n" .
2928              "<body>\n";
2929
2930        if (-f $site_header) {
2931                open (my $fd, $site_header);
2932                print <$fd>;
2933                close $fd;
2934        }
2935
2936        print "<div class=\"page_header\">\n" .
2937              $cgi->a({-href => esc_url($logo_url),
2938                       -title => $logo_label},
2939                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2940        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2941        if (defined $project) {
2942                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2943                if (defined $action) {
2944                        print " / $action";
2945                }
2946                print "\n";
2947        }
2948        print "</div>\n";
2949
2950        my ($have_search) = gitweb_check_feature('search');
2951        if (defined $project && $have_search) {
2952                if (!defined $searchtext) {
2953                        $searchtext = "";
2954                }
2955                my $search_hash;
2956                if (defined $hash_base) {
2957                        $search_hash = $hash_base;
2958                } elsif (defined $hash) {
2959                        $search_hash = $hash;
2960                } else {
2961                        $search_hash = "HEAD";
2962                }
2963                my $action = $my_uri;
2964                my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2965                if ($use_pathinfo) {
2966                        $action .= "/".esc_url($project);
2967                }
2968                print $cgi->startform(-method => "get", -action => $action) .
2969                      "<div class=\"search\">\n" .
2970                      (!$use_pathinfo &&
2971                      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2972                      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2973                      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2974                      $cgi->popup_menu(-name => 'st', -default => 'commit',
2975                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2976                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2977                      " search:\n",
2978                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2979                      "<span title=\"Extended regular expression\">" .
2980                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2981                                     -checked => $search_use_regexp) .
2982                      "</span>" .
2983                      "</div>" .
2984                      $cgi->end_form() . "\n";
2985        }
2986}
2987
2988sub git_footer_html {
2989        my $feed_class = 'rss_logo';
2990
2991        print "<div class=\"page_footer\">\n";
2992        if (defined $project) {
2993                my $descr = git_get_project_description($project);
2994                if (defined $descr) {
2995                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2996                }
2997
2998                my %href_params = get_feed_info();
2999                if (!%href_params) {
3000                        $feed_class .= ' generic';
3001                }
3002                $href_params{'-title'} ||= 'log';
3003
3004                foreach my $format qw(RSS Atom) {
3005                        $href_params{'action'} = lc($format);
3006                        print $cgi->a({-href => href(%href_params),
3007                                      -title => "$href_params{'-title'} $format feed",
3008                                      -class => $feed_class}, $format)."\n";
3009                }
3010
3011        } else {
3012                print $cgi->a({-href => href(project=>undef, action=>"opml"),
3013                              -class => $feed_class}, "OPML") . " ";
3014                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3015                              -class => $feed_class}, "TXT") . "\n";
3016        }
3017        print "</div>\n"; # class="page_footer"
3018
3019        if (-f $site_footer) {
3020                open (my $fd, $site_footer);
3021                print <$fd>;
3022                close $fd;
3023        }
3024
3025        print "</body>\n" .
3026              "</html>";
3027}
3028
3029# die_error(<http_status_code>, <error_message>)
3030# Example: die_error(404, 'Hash not found')
3031# By convention, use the following status codes (as defined in RFC 2616):
3032# 400: Invalid or missing CGI parameters, or
3033#      requested object exists but has wrong type.
3034# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3035#      this server or project.
3036# 404: Requested object/revision/project doesn't exist.
3037# 500: The server isn't configured properly, or
3038#      an internal error occurred (e.g. failed assertions caused by bugs), or
3039#      an unknown error occurred (e.g. the git binary died unexpectedly).
3040sub die_error {
3041        my $status = shift || 500;
3042        my $error = shift || "Internal server error";
3043
3044        my %http_responses = (400 => '400 Bad Request',
3045                              403 => '403 Forbidden',
3046                              404 => '404 Not Found',
3047                              500 => '500 Internal Server Error');
3048        git_header_html($http_responses{$status});
3049        print <<EOF;
3050<div class="page_body">
3051<br /><br />
3052$status - $error
3053<br />
3054</div>
3055EOF
3056        git_footer_html();
3057        exit;
3058}
3059
3060## ----------------------------------------------------------------------
3061## functions printing or outputting HTML: navigation
3062
3063sub git_print_page_nav {
3064        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3065        $extra = '' if !defined $extra; # pager or formats
3066
3067        my @navs = qw(summary shortlog log commit commitdiff tree);
3068        if ($suppress) {
3069                @navs = grep { $_ ne $suppress } @navs;
3070        }
3071
3072        my %arg = map { $_ => {action=>$_} } @navs;
3073        if (defined $head) {
3074                for (qw(commit commitdiff)) {
3075                        $arg{$_}{'hash'} = $head;
3076                }
3077                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3078                        for (qw(shortlog log)) {
3079                                $arg{$_}{'hash'} = $head;
3080                        }
3081                }
3082        }
3083
3084        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3085        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3086
3087        my @actions = gitweb_check_feature('actions');
3088        my %repl = (
3089                '%' => '%',
3090                'n' => $project,         # project name
3091                'f' => $git_dir,         # project path within filesystem
3092                'h' => $treehead || '',  # current hash ('h' parameter)
3093                'b' => $treebase || '',  # hash base ('hb' parameter)
3094        );
3095        while (@actions) {
3096                my ($label, $link, $pos) = splice(@actions,0,3);
3097                # insert
3098                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3099                # munch munch
3100                $link =~ s/%([%nfhb])/$repl{$1}/g;
3101                $arg{$label}{'_href'} = $link;
3102        }
3103
3104        print "<div class=\"page_nav\">\n" .
3105                (join " | ",
3106                 map { $_ eq $current ?
3107                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3108                 } @navs);
3109        print "<br/>\n$extra<br/>\n" .
3110              "</div>\n";
3111}
3112
3113sub format_paging_nav {
3114        my ($action, $hash, $head, $page, $has_next_link) = @_;
3115        my $paging_nav;
3116
3117
3118        if ($hash ne $head || $page) {
3119                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3120        } else {
3121                $paging_nav .= "HEAD";
3122        }
3123
3124        if ($page > 0) {
3125                $paging_nav .= " &sdot; " .
3126                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
3127                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3128        } else {
3129                $paging_nav .= " &sdot; prev";
3130        }
3131
3132        if ($has_next_link) {
3133                $paging_nav .= " &sdot; " .
3134                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
3135                                 -accesskey => "n", -title => "Alt-n"}, "next");
3136        } else {
3137                $paging_nav .= " &sdot; next";
3138        }
3139
3140        return $paging_nav;
3141}
3142
3143## ......................................................................
3144## functions printing or outputting HTML: div
3145
3146sub git_print_header_div {
3147        my ($action, $title, $hash, $hash_base) = @_;
3148        my %args = ();
3149
3150        $args{'action'} = $action;
3151        $args{'hash'} = $hash if $hash;
3152        $args{'hash_base'} = $hash_base if $hash_base;
3153
3154        print "<div class=\"header\">\n" .
3155              $cgi->a({-href => href(%args), -class => "title"},
3156              $title ? $title : $action) .
3157              "\n</div>\n";
3158}
3159
3160#sub git_print_authorship (\%) {
3161sub git_print_authorship {
3162        my $co = shift;
3163
3164        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3165        print "<div class=\"author_date\">" .
3166              esc_html($co->{'author_name'}) .
3167              " [$ad{'rfc2822'}";
3168        if ($ad{'hour_local'} < 6) {
3169                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3170                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3171        } else {
3172                printf(" (%02d:%02d %s)",
3173                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3174        }
3175        print "]</div>\n";
3176}
3177
3178sub git_print_page_path {
3179        my $name = shift;
3180        my $type = shift;
3181        my $hb = shift;
3182
3183
3184        print "<div class=\"page_path\">";
3185        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3186                      -title => 'tree root'}, to_utf8("[$project]"));
3187        print " / ";
3188        if (defined $name) {
3189                my @dirname = split '/', $name;
3190                my $basename = pop @dirname;
3191                my $fullname = '';
3192
3193                foreach my $dir (@dirname) {
3194                        $fullname .= ($fullname ? '/' : '') . $dir;
3195                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3196                                                     hash_base=>$hb),
3197                                      -title => $fullname}, esc_path($dir));
3198                        print " / ";
3199                }
3200                if (defined $type && $type eq 'blob') {
3201                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3202                                                     hash_base=>$hb),
3203                                      -title => $name}, esc_path($basename));
3204                } elsif (defined $type && $type eq 'tree') {
3205                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3206                                                     hash_base=>$hb),
3207                                      -title => $name}, esc_path($basename));
3208                        print " / ";
3209                } else {
3210                        print esc_path($basename);
3211                }
3212        }
3213        print "<br/></div>\n";
3214}
3215
3216# sub git_print_log (\@;%) {
3217sub git_print_log ($;%) {
3218        my $log = shift;
3219        my %opts = @_;
3220
3221        if ($opts{'-remove_title'}) {
3222                # remove title, i.e. first line of log
3223                shift @$log;
3224        }
3225        # remove leading empty lines
3226        while (defined $log->[0] && $log->[0] eq "") {
3227                shift @$log;
3228        }
3229
3230        # print log
3231        my $signoff = 0;
3232        my $empty = 0;
3233        foreach my $line (@$log) {
3234                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3235                        $signoff = 1;
3236                        $empty = 0;
3237                        if (! $opts{'-remove_signoff'}) {
3238                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3239                                next;
3240                        } else {
3241                                # remove signoff lines
3242                                next;
3243                        }
3244                } else {
3245                        $signoff = 0;
3246                }
3247
3248                # print only one empty line
3249                # do not print empty line after signoff
3250                if ($line eq "") {
3251                        next if ($empty || $signoff);
3252                        $empty = 1;
3253                } else {
3254                        $empty = 0;
3255                }
3256
3257                print format_log_line_html($line) . "<br/>\n";
3258        }
3259
3260        if ($opts{'-final_empty_line'}) {
3261                # end with single empty line
3262                print "<br/>\n" unless $empty;
3263        }
3264}
3265
3266# return link target (what link points to)
3267sub git_get_link_target {
3268        my $hash = shift;
3269        my $link_target;
3270
3271        # read link
3272        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3273                or return;
3274        {
3275                local $/;
3276                $link_target = <$fd>;
3277        }
3278        close $fd
3279                or return;
3280
3281        return $link_target;
3282}
3283
3284# given link target, and the directory (basedir) the link is in,
3285# return target of link relative to top directory (top tree);
3286# return undef if it is not possible (including absolute links).
3287sub normalize_link_target {
3288        my ($link_target, $basedir, $hash_base) = @_;
3289
3290        # we can normalize symlink target only if $hash_base is provided
3291        return unless $hash_base;
3292
3293        # absolute symlinks (beginning with '/') cannot be normalized
3294        return if (substr($link_target, 0, 1) eq '/');
3295
3296        # normalize link target to path from top (root) tree (dir)
3297        my $path;
3298        if ($basedir) {
3299                $path = $basedir . '/' . $link_target;
3300        } else {
3301                # we are in top (root) tree (dir)
3302                $path = $link_target;
3303        }
3304
3305        # remove //, /./, and /../
3306        my @path_parts;
3307        foreach my $part (split('/', $path)) {
3308                # discard '.' and ''
3309                next if (!$part || $part eq '.');
3310                # handle '..'
3311                if ($part eq '..') {
3312                        if (@path_parts) {
3313                                pop @path_parts;
3314                        } else {
3315                                # link leads outside repository (outside top dir)
3316                                return;
3317                        }
3318                } else {
3319                        push @path_parts, $part;
3320                }
3321        }
3322        $path = join('/', @path_parts);
3323
3324        return $path;
3325}
3326
3327# print tree entry (row of git_tree), but without encompassing <tr> element
3328sub git_print_tree_entry {
3329        my ($t, $basedir, $hash_base, $have_blame) = @_;
3330
3331        my %base_key = ();
3332        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3333
3334        # The format of a table row is: mode list link.  Where mode is
3335        # the mode of the entry, list is the name of the entry, an href,
3336        # and link is the action links of the entry.
3337
3338        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3339        if ($t->{'type'} eq "blob") {
3340                print "<td class=\"list\">" .
3341                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3342                                               file_name=>"$basedir$t->{'name'}", %base_key),
3343                                -class => "list"}, esc_path($t->{'name'}));
3344                if (S_ISLNK(oct $t->{'mode'})) {
3345                        my $link_target = git_get_link_target($t->{'hash'});
3346                        if ($link_target) {
3347                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3348                                if (defined $norm_target) {
3349                                        print " -> " .
3350                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3351                                                                     file_name=>$norm_target),
3352                                                       -title => $norm_target}, esc_path($link_target));
3353                                } else {
3354                                        print " -> " . esc_path($link_target);
3355                                }
3356                        }
3357                }
3358                print "</td>\n";
3359                print "<td class=\"link\">";
3360                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3361                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3362                              "blob");
3363                if ($have_blame) {
3364                        print " | " .
3365                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3366                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3367                                      "blame");
3368                }
3369                if (defined $hash_base) {
3370                        print " | " .
3371                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3372                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3373                                      "history");
3374                }
3375                print " | " .
3376                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3377                                               file_name=>"$basedir$t->{'name'}")},
3378                                "raw");
3379                print "</td>\n";
3380
3381        } elsif ($t->{'type'} eq "tree") {
3382                print "<td class=\"list\">";
3383                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3384                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3385                              esc_path($t->{'name'}));
3386                print "</td>\n";
3387                print "<td class=\"link\">";
3388                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3389                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3390                              "tree");
3391                if (defined $hash_base) {
3392                        print " | " .
3393                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3394                                                     file_name=>"$basedir$t->{'name'}")},
3395                                      "history");
3396                }
3397                print "</td>\n";
3398        } else {
3399                # unknown object: we can only present history for it
3400                # (this includes 'commit' object, i.e. submodule support)
3401                print "<td class=\"list\">" .
3402                      esc_path($t->{'name'}) .
3403                      "</td>\n";
3404                print "<td class=\"link\">";
3405                if (defined $hash_base) {
3406                        print $cgi->a({-href => href(action=>"history",
3407                                                     hash_base=>$hash_base,
3408                                                     file_name=>"$basedir$t->{'name'}")},
3409                                      "history");
3410                }
3411                print "</td>\n";
3412        }
3413}
3414
3415## ......................................................................
3416## functions printing large fragments of HTML
3417
3418# get pre-image filenames for merge (combined) diff
3419sub fill_from_file_info {
3420        my ($diff, @parents) = @_;
3421
3422        $diff->{'from_file'} = [ ];
3423        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3424        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3425                if ($diff->{'status'}[$i] eq 'R' ||
3426                    $diff->{'status'}[$i] eq 'C') {
3427                        $diff->{'from_file'}[$i] =
3428                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3429                }
3430        }
3431
3432        return $diff;
3433}
3434
3435# is current raw difftree line of file deletion
3436sub is_deleted {
3437        my $diffinfo = shift;
3438
3439        return $diffinfo->{'to_id'} eq ('0' x 40);
3440}
3441
3442# does patch correspond to [previous] difftree raw line
3443# $diffinfo  - hashref of parsed raw diff format
3444# $patchinfo - hashref of parsed patch diff format
3445#              (the same keys as in $diffinfo)
3446sub is_patch_split {
3447        my ($diffinfo, $patchinfo) = @_;
3448
3449        return defined $diffinfo && defined $patchinfo
3450                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3451}
3452
3453
3454sub git_difftree_body {
3455        my ($difftree, $hash, @parents) = @_;
3456        my ($parent) = $parents[0];
3457        my ($have_blame) = gitweb_check_feature('blame');
3458        print "<div class=\"list_head\">\n";
3459        if ($#{$difftree} > 10) {
3460                print(($#{$difftree} + 1) . " files changed:\n");
3461        }
3462        print "</div>\n";
3463
3464        print "<table class=\"" .
3465              (@parents > 1 ? "combined " : "") .
3466              "diff_tree\">\n";
3467
3468        # header only for combined diff in 'commitdiff' view
3469        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3470        if ($has_header) {
3471                # table header
3472                print "<thead><tr>\n" .
3473                       "<th></th><th></th>\n"; # filename, patchN link
3474                for (my $i = 0; $i < @parents; $i++) {
3475                        my $par = $parents[$i];
3476                        print "<th>" .
3477                              $cgi->a({-href => href(action=>"commitdiff",
3478                                                     hash=>$hash, hash_parent=>$par),
3479                                       -title => 'commitdiff to parent number ' .
3480                                                  ($i+1) . ': ' . substr($par,0,7)},
3481                                      $i+1) .
3482                              "&nbsp;</th>\n";
3483                }
3484                print "</tr></thead>\n<tbody>\n";
3485        }
3486
3487        my $alternate = 1;
3488        my $patchno = 0;
3489        foreach my $line (@{$difftree}) {
3490                my $diff = parsed_difftree_line($line);
3491
3492                if ($alternate) {
3493                        print "<tr class=\"dark\">\n";
3494                } else {
3495                        print "<tr class=\"light\">\n";
3496                }
3497                $alternate ^= 1;
3498
3499                if (exists $diff->{'nparents'}) { # combined diff
3500
3501                        fill_from_file_info($diff, @parents)
3502                                unless exists $diff->{'from_file'};
3503
3504                        if (!is_deleted($diff)) {
3505                                # file exists in the result (child) commit
3506                                print "<td>" .
3507                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3508                                                             file_name=>$diff->{'to_file'},
3509                                                             hash_base=>$hash),
3510                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3511                                      "</td>\n";
3512                        } else {
3513                                print "<td>" .
3514                                      esc_path($diff->{'to_file'}) .
3515                                      "</td>\n";
3516                        }
3517
3518                        if ($action eq 'commitdiff') {
3519                                # link to patch
3520                                $patchno++;
3521                                print "<td class=\"link\">" .
3522                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3523                                      " | " .
3524                                      "</td>\n";
3525                        }
3526
3527                        my $has_history = 0;
3528                        my $not_deleted = 0;
3529                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3530                                my $hash_parent = $parents[$i];
3531                                my $from_hash = $diff->{'from_id'}[$i];
3532                                my $from_path = $diff->{'from_file'}[$i];
3533                                my $status = $diff->{'status'}[$i];
3534
3535                                $has_history ||= ($status ne 'A');
3536                                $not_deleted ||= ($status ne 'D');
3537
3538                                if ($status eq 'A') {
3539                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3540                                } elsif ($status eq 'D') {
3541                                        print "<td class=\"link\">" .
3542                                              $cgi->a({-href => href(action=>"blob",
3543                                                                     hash_base=>$hash,
3544                                                                     hash=>$from_hash,
3545                                                                     file_name=>$from_path)},
3546                                                      "blob" . ($i+1)) .
3547                                              " | </td>\n";
3548                                } else {
3549                                        if ($diff->{'to_id'} eq $from_hash) {
3550                                                print "<td class=\"link nochange\">";
3551                                        } else {
3552                                                print "<td class=\"link\">";
3553                                        }
3554                                        print $cgi->a({-href => href(action=>"blobdiff",
3555                                                                     hash=>$diff->{'to_id'},
3556                                                                     hash_parent=>$from_hash,
3557                                                                     hash_base=>$hash,
3558                                                                     hash_parent_base=>$hash_parent,
3559                                                                     file_name=>$diff->{'to_file'},
3560                                                                     file_parent=>$from_path)},
3561                                                      "diff" . ($i+1)) .
3562                                              " | </td>\n";
3563                                }
3564                        }
3565
3566                        print "<td class=\"link\">";
3567                        if ($not_deleted) {
3568                                print $cgi->a({-href => href(action=>"blob",
3569                                                             hash=>$diff->{'to_id'},
3570                                                             file_name=>$diff->{'to_file'},
3571                                                             hash_base=>$hash)},
3572                                              "blob");
3573                                print " | " if ($has_history);
3574                        }
3575                        if ($has_history) {
3576                                print $cgi->a({-href => href(action=>"history",
3577                                                             file_name=>$diff->{'to_file'},
3578                                                             hash_base=>$hash)},
3579                                              "history");
3580                        }
3581                        print "</td>\n";
3582
3583                        print "</tr>\n";
3584                        next; # instead of 'else' clause, to avoid extra indent
3585                }
3586                # else ordinary diff
3587
3588                my ($to_mode_oct, $to_mode_str, $to_file_type);
3589                my ($from_mode_oct, $from_mode_str, $from_file_type);
3590                if ($diff->{'to_mode'} ne ('0' x 6)) {
3591                        $to_mode_oct = oct $diff->{'to_mode'};
3592                        if (S_ISREG($to_mode_oct)) { # only for regular file
3593                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3594                        }
3595                        $to_file_type = file_type($diff->{'to_mode'});
3596                }
3597                if ($diff->{'from_mode'} ne ('0' x 6)) {
3598                        $from_mode_oct = oct $diff->{'from_mode'};
3599                        if (S_ISREG($to_mode_oct)) { # only for regular file
3600                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3601                        }
3602                        $from_file_type = file_type($diff->{'from_mode'});
3603                }
3604
3605                if ($diff->{'status'} eq "A") { # created
3606                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3607                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3608                        $mode_chng   .= "]</span>";
3609                        print "<td>";
3610                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3611                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3612                                      -class => "list"}, esc_path($diff->{'file'}));
3613                        print "</td>\n";
3614                        print "<td>$mode_chng</td>\n";
3615                        print "<td class=\"link\">";
3616                        if ($action eq 'commitdiff') {
3617                                # link to patch
3618                                $patchno++;
3619                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3620                                print " | ";
3621                        }
3622                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3623                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3624                                      "blob");
3625                        print "</td>\n";
3626
3627                } elsif ($diff->{'status'} eq "D") { # deleted
3628                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3629                        print "<td>";
3630                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3631                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3632                                       -class => "list"}, esc_path($diff->{'file'}));
3633                        print "</td>\n";
3634                        print "<td>$mode_chng</td>\n";
3635                        print "<td class=\"link\">";
3636                        if ($action eq 'commitdiff') {
3637                                # link to patch
3638                                $patchno++;
3639                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3640                                print " | ";
3641                        }
3642                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3643                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3644                                      "blob") . " | ";
3645                        if ($have_blame) {
3646                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3647                                                             file_name=>$diff->{'file'})},
3648                                              "blame") . " | ";
3649                        }
3650                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3651                                                     file_name=>$diff->{'file'})},
3652                                      "history");
3653                        print "</td>\n";
3654
3655                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3656                        my $mode_chnge = "";
3657                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3658                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3659                                if ($from_file_type ne $to_file_type) {
3660                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3661                                }
3662                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3663                                        if ($from_mode_str && $to_mode_str) {
3664                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3665                                        } elsif ($to_mode_str) {
3666                                                $mode_chnge .= " mode: $to_mode_str";
3667                                        }
3668                                }
3669                                $mode_chnge .= "]</span>\n";
3670                        }
3671                        print "<td>";
3672                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3673                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3674                                      -class => "list"}, esc_path($diff->{'file'}));
3675                        print "</td>\n";
3676                        print "<td>$mode_chnge</td>\n";
3677                        print "<td class=\"link\">";
3678                        if ($action eq 'commitdiff') {
3679                                # link to patch
3680                                $patchno++;
3681                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3682                                      " | ";
3683                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3684                                # "commit" view and modified file (not onlu mode changed)
3685                                print $cgi->a({-href => href(action=>"blobdiff",
3686                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3687                                                             hash_base=>$hash, hash_parent_base=>$parent,
3688                                                             file_name=>$diff->{'file'})},
3689                                              "diff") .
3690                                      " | ";
3691                        }
3692                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3693                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3694                                       "blob") . " | ";
3695                        if ($have_blame) {
3696                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3697                                                             file_name=>$diff->{'file'})},
3698                                              "blame") . " | ";
3699                        }
3700                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3701                                                     file_name=>$diff->{'file'})},
3702                                      "history");
3703                        print "</td>\n";
3704
3705                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3706                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3707                        my $nstatus = $status_name{$diff->{'status'}};
3708                        my $mode_chng = "";
3709                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3710                                # mode also for directories, so we cannot use $to_mode_str
3711                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3712                        }
3713                        print "<td>" .
3714                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3715                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3716                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3717                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3718                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3719                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3720                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3721                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3722                              "<td class=\"link\">";
3723                        if ($action eq 'commitdiff') {
3724                                # link to patch
3725                                $patchno++;
3726                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3727                                      " | ";
3728                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3729                                # "commit" view and modified file (not only pure rename or copy)
3730                                print $cgi->a({-href => href(action=>"blobdiff",
3731                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3732                                                             hash_base=>$hash, hash_parent_base=>$parent,
3733                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3734                                              "diff") .
3735                                      " | ";
3736                        }
3737                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3738                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3739                                      "blob") . " | ";
3740                        if ($have_blame) {
3741                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3742                                                             file_name=>$diff->{'to_file'})},
3743                                              "blame") . " | ";
3744                        }
3745                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3746                                                    file_name=>$diff->{'to_file'})},
3747                                      "history");
3748                        print "</td>\n";
3749
3750                } # we should not encounter Unmerged (U) or Unknown (X) status
3751                print "</tr>\n";
3752        }
3753        print "</tbody>" if $has_header;
3754        print "</table>\n";
3755}
3756
3757sub git_patchset_body {
3758        my ($fd, $difftree, $hash, @hash_parents) = @_;
3759        my ($hash_parent) = $hash_parents[0];
3760
3761        my $is_combined = (@hash_parents > 1);
3762        my $patch_idx = 0;
3763        my $patch_number = 0;
3764        my $patch_line;
3765        my $diffinfo;
3766        my $to_name;
3767        my (%from, %to);
3768
3769        print "<div class=\"patchset\">\n";
3770
3771        # skip to first patch
3772        while ($patch_line = <$fd>) {
3773                chomp $patch_line;
3774
3775                last if ($patch_line =~ m/^diff /);
3776        }
3777
3778 PATCH:
3779        while ($patch_line) {
3780
3781                # parse "git diff" header line
3782                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3783                        # $1 is from_name, which we do not use
3784                        $to_name = unquote($2);
3785                        $to_name =~ s!^b/!!;
3786                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3787                        # $1 is 'cc' or 'combined', which we do not use
3788                        $to_name = unquote($2);
3789                } else {
3790                        $to_name = undef;
3791                }
3792
3793                # check if current patch belong to current raw line
3794                # and parse raw git-diff line if needed
3795                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3796                        # this is continuation of a split patch
3797                        print "<div class=\"patch cont\">\n";
3798                } else {
3799                        # advance raw git-diff output if needed
3800                        $patch_idx++ if defined $diffinfo;
3801
3802                        # read and prepare patch information
3803                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3804
3805                        # compact combined diff output can have some patches skipped
3806                        # find which patch (using pathname of result) we are at now;
3807                        if ($is_combined) {
3808                                while ($to_name ne $diffinfo->{'to_file'}) {
3809                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3810                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3811                                              "</div>\n";  # class="patch"
3812
3813                                        $patch_idx++;
3814                                        $patch_number++;
3815
3816                                        last if $patch_idx > $#$difftree;
3817                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3818                                }
3819                        }
3820
3821                        # modifies %from, %to hashes
3822                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3823
3824                        # this is first patch for raw difftree line with $patch_idx index
3825                        # we index @$difftree array from 0, but number patches from 1
3826                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3827                }
3828
3829                # git diff header
3830                #assert($patch_line =~ m/^diff /) if DEBUG;
3831                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3832                $patch_number++;
3833                # print "git diff" header
3834                print format_git_diff_header_line($patch_line, $diffinfo,
3835                                                  \%from, \%to);
3836
3837                # print extended diff header
3838                print "<div class=\"diff extended_header\">\n";
3839        EXTENDED_HEADER:
3840                while ($patch_line = <$fd>) {
3841                        chomp $patch_line;
3842
3843                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3844
3845                        print format_extended_diff_header_line($patch_line, $diffinfo,
3846                                                               \%from, \%to);
3847                }
3848                print "</div>\n"; # class="diff extended_header"
3849
3850                # from-file/to-file diff header
3851                if (! $patch_line) {
3852                        print "</div>\n"; # class="patch"
3853                        last PATCH;
3854                }
3855                next PATCH if ($patch_line =~ m/^diff /);
3856                #assert($patch_line =~ m/^---/) if DEBUG;
3857
3858                my $last_patch_line = $patch_line;
3859                $patch_line = <$fd>;
3860                chomp $patch_line;
3861                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3862
3863                print format_diff_from_to_header($last_patch_line, $patch_line,
3864                                                 $diffinfo, \%from, \%to,
3865                                                 @hash_parents);
3866
3867                # the patch itself
3868        LINE:
3869                while ($patch_line = <$fd>) {
3870                        chomp $patch_line;
3871
3872                        next PATCH if ($patch_line =~ m/^diff /);
3873
3874                        print format_diff_line($patch_line, \%from, \%to);
3875                }
3876
3877        } continue {
3878                print "</div>\n"; # class="patch"
3879        }
3880
3881        # for compact combined (--cc) format, with chunk and patch simpliciaction
3882        # patchset might be empty, but there might be unprocessed raw lines
3883        for (++$patch_idx if $patch_number > 0;
3884             $patch_idx < @$difftree;
3885             ++$patch_idx) {
3886                # read and prepare patch information
3887                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3888
3889                # generate anchor for "patch" links in difftree / whatchanged part
3890                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3891                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3892                      "</div>\n";  # class="patch"
3893
3894                $patch_number++;
3895        }
3896
3897        if ($patch_number == 0) {
3898                if (@hash_parents > 1) {
3899                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3900                } else {
3901                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3902                }
3903        }
3904
3905        print "</div>\n"; # class="patchset"
3906}
3907
3908# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3909
3910# fills project list info (age, description, owner, forks) for each
3911# project in the list, removing invalid projects from returned list
3912# NOTE: modifies $projlist, but does not remove entries from it
3913sub fill_project_list_info {
3914        my ($projlist, $check_forks) = @_;
3915        my @projects;
3916
3917        my $show_ctags = gitweb_check_feature('ctags');
3918 PROJECT:
3919        foreach my $pr (@$projlist) {
3920                my (@activity) = git_get_last_activity($pr->{'path'});
3921                unless (@activity) {
3922                        next PROJECT;
3923                }
3924                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3925                if (!defined $pr->{'descr'}) {
3926                        my $descr = git_get_project_description($pr->{'path'}) || "";
3927                        $descr = to_utf8($descr);
3928                        $pr->{'descr_long'} = $descr;
3929                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3930                }
3931                if (!defined $pr->{'owner'}) {
3932                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3933                }
3934                if ($check_forks) {
3935                        my $pname = $pr->{'path'};
3936                        if (($pname =~ s/\.git$//) &&
3937                            ($pname !~ /\/$/) &&
3938                            (-d "$projectroot/$pname")) {
3939                                $pr->{'forks'} = "-d $projectroot/$pname";
3940                        }       else {
3941                                $pr->{'forks'} = 0;
3942                        }
3943                }
3944                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3945                push @projects, $pr;
3946        }
3947
3948        return @projects;
3949}
3950
3951# print 'sort by' <th> element, generating 'sort by $name' replay link
3952# if that order is not selected
3953sub print_sort_th {
3954        my ($name, $order, $header) = @_;
3955        $header ||= ucfirst($name);
3956
3957        if ($order eq $name) {
3958                print "<th>$header</th>\n";
3959        } else {
3960                print "<th>" .
3961                      $cgi->a({-href => href(-replay=>1, order=>$name),
3962                               -class => "header"}, $header) .
3963                      "</th>\n";
3964        }
3965}
3966
3967sub git_project_list_body {
3968        # actually uses global variable $project
3969        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3970
3971        my ($check_forks) = gitweb_check_feature('forks');
3972        my @projects = fill_project_list_info($projlist, $check_forks);
3973
3974        $order ||= $default_projects_order;
3975        $from = 0 unless defined $from;
3976        $to = $#projects if (!defined $to || $#projects < $to);
3977
3978        my %order_info = (
3979                project => { key => 'path', type => 'str' },
3980                descr => { key => 'descr_long', type => 'str' },
3981                owner => { key => 'owner', type => 'str' },
3982                age => { key => 'age', type => 'num' }
3983        );
3984        my $oi = $order_info{$order};
3985        if ($oi->{'type'} eq 'str') {
3986                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3987        } else {
3988                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3989        }
3990
3991        my $show_ctags = gitweb_check_feature('ctags');
3992        if ($show_ctags) {
3993                my %ctags;
3994                foreach my $p (@projects) {
3995                        foreach my $ct (keys %{$p->{'ctags'}}) {
3996                                $ctags{$ct} += $p->{'ctags'}->{$ct};
3997                        }
3998                }
3999                my $cloud = git_populate_project_tagcloud(\%ctags);
4000                print git_show_project_tagcloud($cloud, 64);
4001        }
4002
4003        print "<table class=\"project_list\">\n";
4004        unless ($no_header) {
4005                print "<tr>\n";
4006                if ($check_forks) {
4007                        print "<th></th>\n";
4008                }
4009                print_sort_th('project', $order, 'Project');
4010                print_sort_th('descr', $order, 'Description');
4011                print_sort_th('owner', $order, 'Owner');
4012                print_sort_th('age', $order, 'Last Change');
4013                print "<th></th>\n" . # for links
4014                      "</tr>\n";
4015        }
4016        my $alternate = 1;
4017        my $tagfilter = $cgi->param('by_tag');
4018        for (my $i = $from; $i <= $to; $i++) {
4019                my $pr = $projects[$i];
4020
4021                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4022                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4023                        and not $pr->{'descr_long'} =~ /$searchtext/;
4024                # Weed out forks or non-matching entries of search
4025                if ($check_forks) {
4026                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4027                        $forkbase="^$forkbase" if $forkbase;
4028                        next if not $searchtext and not $tagfilter and $show_ctags
4029                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4030                }
4031
4032                if ($alternate) {
4033                        print "<tr class=\"dark\">\n";
4034                } else {
4035                        print "<tr class=\"light\">\n";
4036                }
4037                $alternate ^= 1;
4038                if ($check_forks) {
4039                        print "<td>";
4040                        if ($pr->{'forks'}) {
4041                                print "<!-- $pr->{'forks'} -->\n";
4042                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4043                        }
4044                        print "</td>\n";
4045                }
4046                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4047                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4048                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4049                                        -class => "list", -title => $pr->{'descr_long'}},
4050                                        esc_html($pr->{'descr'})) . "</td>\n" .
4051                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4052                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4053                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4054                      "<td class=\"link\">" .
4055                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4056                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4057                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4058                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4059                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4060                      "</td>\n" .
4061                      "</tr>\n";
4062        }
4063        if (defined $extra) {
4064                print "<tr>\n";
4065                if ($check_forks) {
4066                        print "<td></td>\n";
4067                }
4068                print "<td colspan=\"5\">$extra</td>\n" .
4069                      "</tr>\n";
4070        }
4071        print "</table>\n";
4072}
4073
4074sub git_shortlog_body {
4075        # uses global variable $project
4076        my ($commitlist, $from, $to, $refs, $extra) = @_;
4077
4078        $from = 0 unless defined $from;
4079        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4080
4081        print "<table class=\"shortlog\">\n";
4082        my $alternate = 1;
4083        for (my $i = $from; $i <= $to; $i++) {
4084                my %co = %{$commitlist->[$i]};
4085                my $commit = $co{'id'};
4086                my $ref = format_ref_marker($refs, $commit);
4087                if ($alternate) {
4088                        print "<tr class=\"dark\">\n";
4089                } else {
4090                        print "<tr class=\"light\">\n";
4091                }
4092                $alternate ^= 1;
4093                my $author = chop_and_escape_str($co{'author_name'}, 10);
4094                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4095                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4096                      "<td><i>" . $author . "</i></td>\n" .
4097                      "<td>";
4098                print format_subject_html($co{'title'}, $co{'title_short'},
4099                                          href(action=>"commit", hash=>$commit), $ref);
4100                print "</td>\n" .
4101                      "<td class=\"link\">" .
4102                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4103                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4104                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4105                my $snapshot_links = format_snapshot_links($commit);
4106                if (defined $snapshot_links) {
4107                        print " | " . $snapshot_links;
4108                }
4109                print "</td>\n" .
4110                      "</tr>\n";
4111        }
4112        if (defined $extra) {
4113                print "<tr>\n" .
4114                      "<td colspan=\"4\">$extra</td>\n" .
4115                      "</tr>\n";
4116        }
4117        print "</table>\n";
4118}
4119
4120sub git_history_body {
4121        # Warning: assumes constant type (blob or tree) during history
4122        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4123
4124        $from = 0 unless defined $from;
4125        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4126
4127        print "<table class=\"history\">\n";
4128        my $alternate = 1;
4129        for (my $i = $from; $i <= $to; $i++) {
4130                my %co = %{$commitlist->[$i]};
4131                if (!%co) {
4132                        next;
4133                }
4134                my $commit = $co{'id'};
4135
4136                my $ref = format_ref_marker($refs, $commit);
4137
4138                if ($alternate) {
4139                        print "<tr class=\"dark\">\n";
4140                } else {
4141                        print "<tr class=\"light\">\n";
4142                }
4143                $alternate ^= 1;
4144        # shortlog uses      chop_str($co{'author_name'}, 10)
4145                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4146                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4147                      "<td><i>" . $author . "</i></td>\n" .
4148                      "<td>";
4149                # originally git_history used chop_str($co{'title'}, 50)
4150                print format_subject_html($co{'title'}, $co{'title_short'},
4151                                          href(action=>"commit", hash=>$commit), $ref);
4152                print "</td>\n" .
4153                      "<td class=\"link\">" .
4154                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4155                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4156
4157                if ($ftype eq 'blob') {
4158                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4159                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4160                        if (defined $blob_current && defined $blob_parent &&
4161                                        $blob_current ne $blob_parent) {
4162                                print " | " .
4163                                        $cgi->a({-href => href(action=>"blobdiff",
4164                                                               hash=>$blob_current, hash_parent=>$blob_parent,
4165                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
4166                                                               file_name=>$file_name)},
4167                                                "diff to current");
4168                        }
4169                }
4170                print "</td>\n" .
4171                      "</tr>\n";
4172        }
4173        if (defined $extra) {
4174                print "<tr>\n" .
4175                      "<td colspan=\"4\">$extra</td>\n" .
4176                      "</tr>\n";
4177        }
4178        print "</table>\n";
4179}
4180
4181sub git_tags_body {
4182        # uses global variable $project
4183        my ($taglist, $from, $to, $extra) = @_;
4184        $from = 0 unless defined $from;
4185        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4186
4187        print "<table class=\"tags\">\n";
4188        my $alternate = 1;
4189        for (my $i = $from; $i <= $to; $i++) {
4190                my $entry = $taglist->[$i];
4191                my %tag = %$entry;
4192                my $comment = $tag{'subject'};
4193                my $comment_short;
4194                if (defined $comment) {
4195                        $comment_short = chop_str($comment, 30, 5);
4196                }
4197                if ($alternate) {
4198                        print "<tr class=\"dark\">\n";
4199                } else {
4200                        print "<tr class=\"light\">\n";
4201                }
4202                $alternate ^= 1;
4203                if (defined $tag{'age'}) {
4204                        print "<td><i>$tag{'age'}</i></td>\n";
4205                } else {
4206                        print "<td></td>\n";
4207                }
4208                print "<td>" .
4209                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4210                               -class => "list name"}, esc_html($tag{'name'})) .
4211                      "</td>\n" .
4212                      "<td>";
4213                if (defined $comment) {
4214                        print format_subject_html($comment, $comment_short,
4215                                                  href(action=>"tag", hash=>$tag{'id'}));
4216                }
4217                print "</td>\n" .
4218                      "<td class=\"selflink\">";
4219                if ($tag{'type'} eq "tag") {
4220                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4221                } else {
4222                        print "&nbsp;";
4223                }
4224                print "</td>\n" .
4225                      "<td class=\"link\">" . " | " .
4226                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4227                if ($tag{'reftype'} eq "commit") {
4228                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4229                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4230                } elsif ($tag{'reftype'} eq "blob") {
4231                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4232                }
4233                print "</td>\n" .
4234                      "</tr>";
4235        }
4236        if (defined $extra) {
4237                print "<tr>\n" .
4238                      "<td colspan=\"5\">$extra</td>\n" .
4239                      "</tr>\n";
4240        }
4241        print "</table>\n";
4242}
4243
4244sub git_heads_body {
4245        # uses global variable $project
4246        my ($headlist, $head, $from, $to, $extra) = @_;
4247        $from = 0 unless defined $from;
4248        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4249
4250        print "<table class=\"heads\">\n";
4251        my $alternate = 1;
4252        for (my $i = $from; $i <= $to; $i++) {
4253                my $entry = $headlist->[$i];
4254                my %ref = %$entry;
4255                my $curr = $ref{'id'} eq $head;
4256                if ($alternate) {
4257                        print "<tr class=\"dark\">\n";
4258                } else {
4259                        print "<tr class=\"light\">\n";
4260                }
4261                $alternate ^= 1;
4262                print "<td><i>$ref{'age'}</i></td>\n" .
4263                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4264                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4265                               -class => "list name"},esc_html($ref{'name'})) .
4266                      "</td>\n" .
4267                      "<td class=\"link\">" .
4268                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4269                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4270                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4271                      "</td>\n" .
4272                      "</tr>";
4273        }
4274        if (defined $extra) {
4275                print "<tr>\n" .
4276                      "<td colspan=\"3\">$extra</td>\n" .
4277                      "</tr>\n";
4278        }
4279        print "</table>\n";
4280}
4281
4282sub git_search_grep_body {
4283        my ($commitlist, $from, $to, $extra) = @_;
4284        $from = 0 unless defined $from;
4285        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4286
4287        print "<table class=\"commit_search\">\n";
4288        my $alternate = 1;
4289        for (my $i = $from; $i <= $to; $i++) {
4290                my %co = %{$commitlist->[$i]};
4291                if (!%co) {
4292                        next;
4293                }
4294                my $commit = $co{'id'};
4295                if ($alternate) {
4296                        print "<tr class=\"dark\">\n";
4297                } else {
4298                        print "<tr class=\"light\">\n";
4299                }
4300                $alternate ^= 1;
4301                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4302                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4303                      "<td><i>" . $author . "</i></td>\n" .
4304                      "<td>" .
4305                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4306                               -class => "list subject"},
4307                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4308                my $comment = $co{'comment'};
4309                foreach my $line (@$comment) {
4310                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4311                                my ($lead, $match, $trail) = ($1, $2, $3);
4312                                $match = chop_str($match, 70, 5, 'center');
4313                                my $contextlen = int((80 - length($match))/2);
4314                                $contextlen = 30 if ($contextlen > 30);
4315                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4316                                $trail = chop_str($trail, $contextlen, 10, 'right');
4317
4318                                $lead  = esc_html($lead);
4319                                $match = esc_html($match);
4320                                $trail = esc_html($trail);
4321
4322                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4323                        }
4324                }
4325                print "</td>\n" .
4326                      "<td class=\"link\">" .
4327                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4328                      " | " .
4329                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4330                      " | " .
4331                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4332                print "</td>\n" .
4333                      "</tr>\n";
4334        }
4335        if (defined $extra) {
4336                print "<tr>\n" .
4337                      "<td colspan=\"3\">$extra</td>\n" .
4338                      "</tr>\n";
4339        }
4340        print "</table>\n";
4341}
4342
4343## ======================================================================
4344## ======================================================================
4345## actions
4346
4347sub git_project_list {
4348        my $order = $input_params{'order'};
4349        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4350                die_error(400, "Unknown order parameter");
4351        }
4352
4353        my @list = git_get_projects_list();
4354        if (!@list) {
4355                die_error(404, "No projects found");
4356        }
4357
4358        git_header_html();
4359        if (-f $home_text) {
4360                print "<div class=\"index_include\">\n";
4361                open (my $fd, $home_text);
4362                print <$fd>;
4363                close $fd;
4364                print "</div>\n";
4365        }
4366        print $cgi->startform(-method => "get") .
4367              "<p class=\"projsearch\">Search:\n" .
4368              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4369              "</p>" .
4370              $cgi->end_form() . "\n";
4371        git_project_list_body(\@list, $order);
4372        git_footer_html();
4373}
4374
4375sub git_forks {
4376        my $order = $input_params{'order'};
4377        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4378                die_error(400, "Unknown order parameter");
4379        }
4380
4381        my @list = git_get_projects_list($project);
4382        if (!@list) {
4383                die_error(404, "No forks found");
4384        }
4385
4386        git_header_html();
4387        git_print_page_nav('','');
4388        git_print_header_div('summary', "$project forks");
4389        git_project_list_body(\@list, $order);
4390        git_footer_html();
4391}
4392
4393sub git_project_index {
4394        my @projects = git_get_projects_list($project);
4395
4396        print $cgi->header(
4397                -type => 'text/plain',
4398                -charset => 'utf-8',
4399                -content_disposition => 'inline; filename="index.aux"');
4400
4401        foreach my $pr (@projects) {
4402                if (!exists $pr->{'owner'}) {
4403                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4404                }
4405
4406                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4407                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4408                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4409                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4410                $path  =~ s/ /\+/g;
4411                $owner =~ s/ /\+/g;
4412
4413                print "$path $owner\n";
4414        }
4415}
4416
4417sub git_summary {
4418        my $descr = git_get_project_description($project) || "none";
4419        my %co = parse_commit("HEAD");
4420        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4421        my $head = $co{'id'};
4422
4423        my $owner = git_get_project_owner($project);
4424
4425        my $refs = git_get_references();
4426        # These get_*_list functions return one more to allow us to see if
4427        # there are more ...
4428        my @taglist  = git_get_tags_list(16);
4429        my @headlist = git_get_heads_list(16);
4430        my @forklist;
4431        my ($check_forks) = gitweb_check_feature('forks');
4432
4433        if ($check_forks) {
4434                @forklist = git_get_projects_list($project);
4435        }
4436
4437        git_header_html();
4438        git_print_page_nav('summary','', $head);
4439
4440        print "<div class=\"title\">&nbsp;</div>\n";
4441        print "<table class=\"projects_list\">\n" .
4442              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4443              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4444        if (defined $cd{'rfc2822'}) {
4445                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4446        }
4447
4448        # use per project git URL list in $projectroot/$project/cloneurl
4449        # or make project git URL from git base URL and project name
4450        my $url_tag = "URL";
4451        my @url_list = git_get_project_url_list($project);
4452        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4453        foreach my $git_url (@url_list) {
4454                next unless $git_url;
4455                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4456                $url_tag = "";
4457        }
4458
4459        # Tag cloud
4460        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4461        if ($show_ctags) {
4462                my $ctags = git_get_project_ctags($project);
4463                my $cloud = git_populate_project_tagcloud($ctags);
4464                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4465                print "</td>\n<td>" unless %$ctags;
4466                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4467                print "</td>\n<td>" if %$ctags;
4468                print git_show_project_tagcloud($cloud, 48);
4469                print "</td></tr>";
4470        }
4471
4472        print "</table>\n";
4473
4474        if (-s "$projectroot/$project/README.html") {
4475                if (open my $fd, "$projectroot/$project/README.html") {
4476                        print "<div class=\"title\">readme</div>\n" .
4477                              "<div class=\"readme\">\n";
4478                        print $_ while (<$fd>);
4479                        print "\n</div>\n"; # class="readme"
4480                        close $fd;
4481                }
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}