gitweb / gitweb.perlon commit Merge branch 'gb/gitweb-snapshot-pathinfo' (ac538e5)
   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 inster 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).
 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        while (@actions) {
3089                my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
3090                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3091                # munch munch
3092                $link =~ s#%n#$project#g;
3093                $link =~ s#%f#$git_dir#g;
3094                $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
3095                $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
3096                $arg{$label}{'_href'} = $link;
3097        }
3098
3099        print "<div class=\"page_nav\">\n" .
3100                (join " | ",
3101                 map { $_ eq $current ?
3102                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3103                 } @navs);
3104        print "<br/>\n$extra<br/>\n" .
3105              "</div>\n";
3106}
3107
3108sub format_paging_nav {
3109        my ($action, $hash, $head, $page, $has_next_link) = @_;
3110        my $paging_nav;
3111
3112
3113        if ($hash ne $head || $page) {
3114                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3115        } else {
3116                $paging_nav .= "HEAD";
3117        }
3118
3119        if ($page > 0) {
3120                $paging_nav .= " &sdot; " .
3121                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
3122                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3123        } else {
3124                $paging_nav .= " &sdot; prev";
3125        }
3126
3127        if ($has_next_link) {
3128                $paging_nav .= " &sdot; " .
3129                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
3130                                 -accesskey => "n", -title => "Alt-n"}, "next");
3131        } else {
3132                $paging_nav .= " &sdot; next";
3133        }
3134
3135        return $paging_nav;
3136}
3137
3138## ......................................................................
3139## functions printing or outputting HTML: div
3140
3141sub git_print_header_div {
3142        my ($action, $title, $hash, $hash_base) = @_;
3143        my %args = ();
3144
3145        $args{'action'} = $action;
3146        $args{'hash'} = $hash if $hash;
3147        $args{'hash_base'} = $hash_base if $hash_base;
3148
3149        print "<div class=\"header\">\n" .
3150              $cgi->a({-href => href(%args), -class => "title"},
3151              $title ? $title : $action) .
3152              "\n</div>\n";
3153}
3154
3155#sub git_print_authorship (\%) {
3156sub git_print_authorship {
3157        my $co = shift;
3158
3159        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3160        print "<div class=\"author_date\">" .
3161              esc_html($co->{'author_name'}) .
3162              " [$ad{'rfc2822'}";
3163        if ($ad{'hour_local'} < 6) {
3164                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3165                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3166        } else {
3167                printf(" (%02d:%02d %s)",
3168                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3169        }
3170        print "]</div>\n";
3171}
3172
3173sub git_print_page_path {
3174        my $name = shift;
3175        my $type = shift;
3176        my $hb = shift;
3177
3178
3179        print "<div class=\"page_path\">";
3180        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3181                      -title => 'tree root'}, to_utf8("[$project]"));
3182        print " / ";
3183        if (defined $name) {
3184                my @dirname = split '/', $name;
3185                my $basename = pop @dirname;
3186                my $fullname = '';
3187
3188                foreach my $dir (@dirname) {
3189                        $fullname .= ($fullname ? '/' : '') . $dir;
3190                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3191                                                     hash_base=>$hb),
3192                                      -title => $fullname}, esc_path($dir));
3193                        print " / ";
3194                }
3195                if (defined $type && $type eq 'blob') {
3196                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3197                                                     hash_base=>$hb),
3198                                      -title => $name}, esc_path($basename));
3199                } elsif (defined $type && $type eq 'tree') {
3200                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3201                                                     hash_base=>$hb),
3202                                      -title => $name}, esc_path($basename));
3203                        print " / ";
3204                } else {
3205                        print esc_path($basename);
3206                }
3207        }
3208        print "<br/></div>\n";
3209}
3210
3211# sub git_print_log (\@;%) {
3212sub git_print_log ($;%) {
3213        my $log = shift;
3214        my %opts = @_;
3215
3216        if ($opts{'-remove_title'}) {
3217                # remove title, i.e. first line of log
3218                shift @$log;
3219        }
3220        # remove leading empty lines
3221        while (defined $log->[0] && $log->[0] eq "") {
3222                shift @$log;
3223        }
3224
3225        # print log
3226        my $signoff = 0;
3227        my $empty = 0;
3228        foreach my $line (@$log) {
3229                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3230                        $signoff = 1;
3231                        $empty = 0;
3232                        if (! $opts{'-remove_signoff'}) {
3233                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3234                                next;
3235                        } else {
3236                                # remove signoff lines
3237                                next;
3238                        }
3239                } else {
3240                        $signoff = 0;
3241                }
3242
3243                # print only one empty line
3244                # do not print empty line after signoff
3245                if ($line eq "") {
3246                        next if ($empty || $signoff);
3247                        $empty = 1;
3248                } else {
3249                        $empty = 0;
3250                }
3251
3252                print format_log_line_html($line) . "<br/>\n";
3253        }
3254
3255        if ($opts{'-final_empty_line'}) {
3256                # end with single empty line
3257                print "<br/>\n" unless $empty;
3258        }
3259}
3260
3261# return link target (what link points to)
3262sub git_get_link_target {
3263        my $hash = shift;
3264        my $link_target;
3265
3266        # read link
3267        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3268                or return;
3269        {
3270                local $/;
3271                $link_target = <$fd>;
3272        }
3273        close $fd
3274                or return;
3275
3276        return $link_target;
3277}
3278
3279# given link target, and the directory (basedir) the link is in,
3280# return target of link relative to top directory (top tree);
3281# return undef if it is not possible (including absolute links).
3282sub normalize_link_target {
3283        my ($link_target, $basedir, $hash_base) = @_;
3284
3285        # we can normalize symlink target only if $hash_base is provided
3286        return unless $hash_base;
3287
3288        # absolute symlinks (beginning with '/') cannot be normalized
3289        return if (substr($link_target, 0, 1) eq '/');
3290
3291        # normalize link target to path from top (root) tree (dir)
3292        my $path;
3293        if ($basedir) {
3294                $path = $basedir . '/' . $link_target;
3295        } else {
3296                # we are in top (root) tree (dir)
3297                $path = $link_target;
3298        }
3299
3300        # remove //, /./, and /../
3301        my @path_parts;
3302        foreach my $part (split('/', $path)) {
3303                # discard '.' and ''
3304                next if (!$part || $part eq '.');
3305                # handle '..'
3306                if ($part eq '..') {
3307                        if (@path_parts) {
3308                                pop @path_parts;
3309                        } else {
3310                                # link leads outside repository (outside top dir)
3311                                return;
3312                        }
3313                } else {
3314                        push @path_parts, $part;
3315                }
3316        }
3317        $path = join('/', @path_parts);
3318
3319        return $path;
3320}
3321
3322# print tree entry (row of git_tree), but without encompassing <tr> element
3323sub git_print_tree_entry {
3324        my ($t, $basedir, $hash_base, $have_blame) = @_;
3325
3326        my %base_key = ();
3327        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3328
3329        # The format of a table row is: mode list link.  Where mode is
3330        # the mode of the entry, list is the name of the entry, an href,
3331        # and link is the action links of the entry.
3332
3333        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3334        if ($t->{'type'} eq "blob") {
3335                print "<td class=\"list\">" .
3336                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3337                                               file_name=>"$basedir$t->{'name'}", %base_key),
3338                                -class => "list"}, esc_path($t->{'name'}));
3339                if (S_ISLNK(oct $t->{'mode'})) {
3340                        my $link_target = git_get_link_target($t->{'hash'});
3341                        if ($link_target) {
3342                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3343                                if (defined $norm_target) {
3344                                        print " -> " .
3345                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3346                                                                     file_name=>$norm_target),
3347                                                       -title => $norm_target}, esc_path($link_target));
3348                                } else {
3349                                        print " -> " . esc_path($link_target);
3350                                }
3351                        }
3352                }
3353                print "</td>\n";
3354                print "<td class=\"link\">";
3355                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3356                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3357                              "blob");
3358                if ($have_blame) {
3359                        print " | " .
3360                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3361                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3362                                      "blame");
3363                }
3364                if (defined $hash_base) {
3365                        print " | " .
3366                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3367                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3368                                      "history");
3369                }
3370                print " | " .
3371                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3372                                               file_name=>"$basedir$t->{'name'}")},
3373                                "raw");
3374                print "</td>\n";
3375
3376        } elsif ($t->{'type'} eq "tree") {
3377                print "<td class=\"list\">";
3378                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3379                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3380                              esc_path($t->{'name'}));
3381                print "</td>\n";
3382                print "<td class=\"link\">";
3383                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3384                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3385                              "tree");
3386                if (defined $hash_base) {
3387                        print " | " .
3388                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3389                                                     file_name=>"$basedir$t->{'name'}")},
3390                                      "history");
3391                }
3392                print "</td>\n";
3393        } else {
3394                # unknown object: we can only present history for it
3395                # (this includes 'commit' object, i.e. submodule support)
3396                print "<td class=\"list\">" .
3397                      esc_path($t->{'name'}) .
3398                      "</td>\n";
3399                print "<td class=\"link\">";
3400                if (defined $hash_base) {
3401                        print $cgi->a({-href => href(action=>"history",
3402                                                     hash_base=>$hash_base,
3403                                                     file_name=>"$basedir$t->{'name'}")},
3404                                      "history");
3405                }
3406                print "</td>\n";
3407        }
3408}
3409
3410## ......................................................................
3411## functions printing large fragments of HTML
3412
3413# get pre-image filenames for merge (combined) diff
3414sub fill_from_file_info {
3415        my ($diff, @parents) = @_;
3416
3417        $diff->{'from_file'} = [ ];
3418        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3419        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3420                if ($diff->{'status'}[$i] eq 'R' ||
3421                    $diff->{'status'}[$i] eq 'C') {
3422                        $diff->{'from_file'}[$i] =
3423                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3424                }
3425        }
3426
3427        return $diff;
3428}
3429
3430# is current raw difftree line of file deletion
3431sub is_deleted {
3432        my $diffinfo = shift;
3433
3434        return $diffinfo->{'to_id'} eq ('0' x 40);
3435}
3436
3437# does patch correspond to [previous] difftree raw line
3438# $diffinfo  - hashref of parsed raw diff format
3439# $patchinfo - hashref of parsed patch diff format
3440#              (the same keys as in $diffinfo)
3441sub is_patch_split {
3442        my ($diffinfo, $patchinfo) = @_;
3443
3444        return defined $diffinfo && defined $patchinfo
3445                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3446}
3447
3448
3449sub git_difftree_body {
3450        my ($difftree, $hash, @parents) = @_;
3451        my ($parent) = $parents[0];
3452        my ($have_blame) = gitweb_check_feature('blame');
3453        print "<div class=\"list_head\">\n";
3454        if ($#{$difftree} > 10) {
3455                print(($#{$difftree} + 1) . " files changed:\n");
3456        }
3457        print "</div>\n";
3458
3459        print "<table class=\"" .
3460              (@parents > 1 ? "combined " : "") .
3461              "diff_tree\">\n";
3462
3463        # header only for combined diff in 'commitdiff' view
3464        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3465        if ($has_header) {
3466                # table header
3467                print "<thead><tr>\n" .
3468                       "<th></th><th></th>\n"; # filename, patchN link
3469                for (my $i = 0; $i < @parents; $i++) {
3470                        my $par = $parents[$i];
3471                        print "<th>" .
3472                              $cgi->a({-href => href(action=>"commitdiff",
3473                                                     hash=>$hash, hash_parent=>$par),
3474                                       -title => 'commitdiff to parent number ' .
3475                                                  ($i+1) . ': ' . substr($par,0,7)},
3476                                      $i+1) .
3477                              "&nbsp;</th>\n";
3478                }
3479                print "</tr></thead>\n<tbody>\n";
3480        }
3481
3482        my $alternate = 1;
3483        my $patchno = 0;
3484        foreach my $line (@{$difftree}) {
3485                my $diff = parsed_difftree_line($line);
3486
3487                if ($alternate) {
3488                        print "<tr class=\"dark\">\n";
3489                } else {
3490                        print "<tr class=\"light\">\n";
3491                }
3492                $alternate ^= 1;
3493
3494                if (exists $diff->{'nparents'}) { # combined diff
3495
3496                        fill_from_file_info($diff, @parents)
3497                                unless exists $diff->{'from_file'};
3498
3499                        if (!is_deleted($diff)) {
3500                                # file exists in the result (child) commit
3501                                print "<td>" .
3502                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3503                                                             file_name=>$diff->{'to_file'},
3504                                                             hash_base=>$hash),
3505                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3506                                      "</td>\n";
3507                        } else {
3508                                print "<td>" .
3509                                      esc_path($diff->{'to_file'}) .
3510                                      "</td>\n";
3511                        }
3512
3513                        if ($action eq 'commitdiff') {
3514                                # link to patch
3515                                $patchno++;
3516                                print "<td class=\"link\">" .
3517                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3518                                      " | " .
3519                                      "</td>\n";
3520                        }
3521
3522                        my $has_history = 0;
3523                        my $not_deleted = 0;
3524                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3525                                my $hash_parent = $parents[$i];
3526                                my $from_hash = $diff->{'from_id'}[$i];
3527                                my $from_path = $diff->{'from_file'}[$i];
3528                                my $status = $diff->{'status'}[$i];
3529
3530                                $has_history ||= ($status ne 'A');
3531                                $not_deleted ||= ($status ne 'D');
3532
3533                                if ($status eq 'A') {
3534                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3535                                } elsif ($status eq 'D') {
3536                                        print "<td class=\"link\">" .
3537                                              $cgi->a({-href => href(action=>"blob",
3538                                                                     hash_base=>$hash,
3539                                                                     hash=>$from_hash,
3540                                                                     file_name=>$from_path)},
3541                                                      "blob" . ($i+1)) .
3542                                              " | </td>\n";
3543                                } else {
3544                                        if ($diff->{'to_id'} eq $from_hash) {
3545                                                print "<td class=\"link nochange\">";
3546                                        } else {
3547                                                print "<td class=\"link\">";
3548                                        }
3549                                        print $cgi->a({-href => href(action=>"blobdiff",
3550                                                                     hash=>$diff->{'to_id'},
3551                                                                     hash_parent=>$from_hash,
3552                                                                     hash_base=>$hash,
3553                                                                     hash_parent_base=>$hash_parent,
3554                                                                     file_name=>$diff->{'to_file'},
3555                                                                     file_parent=>$from_path)},
3556                                                      "diff" . ($i+1)) .
3557                                              " | </td>\n";
3558                                }
3559                        }
3560
3561                        print "<td class=\"link\">";
3562                        if ($not_deleted) {
3563                                print $cgi->a({-href => href(action=>"blob",
3564                                                             hash=>$diff->{'to_id'},
3565                                                             file_name=>$diff->{'to_file'},
3566                                                             hash_base=>$hash)},
3567                                              "blob");
3568                                print " | " if ($has_history);
3569                        }
3570                        if ($has_history) {
3571                                print $cgi->a({-href => href(action=>"history",
3572                                                             file_name=>$diff->{'to_file'},
3573                                                             hash_base=>$hash)},
3574                                              "history");
3575                        }
3576                        print "</td>\n";
3577
3578                        print "</tr>\n";
3579                        next; # instead of 'else' clause, to avoid extra indent
3580                }
3581                # else ordinary diff
3582
3583                my ($to_mode_oct, $to_mode_str, $to_file_type);
3584                my ($from_mode_oct, $from_mode_str, $from_file_type);
3585                if ($diff->{'to_mode'} ne ('0' x 6)) {
3586                        $to_mode_oct = oct $diff->{'to_mode'};
3587                        if (S_ISREG($to_mode_oct)) { # only for regular file
3588                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3589                        }
3590                        $to_file_type = file_type($diff->{'to_mode'});
3591                }
3592                if ($diff->{'from_mode'} ne ('0' x 6)) {
3593                        $from_mode_oct = oct $diff->{'from_mode'};
3594                        if (S_ISREG($to_mode_oct)) { # only for regular file
3595                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3596                        }
3597                        $from_file_type = file_type($diff->{'from_mode'});
3598                }
3599
3600                if ($diff->{'status'} eq "A") { # created
3601                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3602                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3603                        $mode_chng   .= "]</span>";
3604                        print "<td>";
3605                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3606                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3607                                      -class => "list"}, esc_path($diff->{'file'}));
3608                        print "</td>\n";
3609                        print "<td>$mode_chng</td>\n";
3610                        print "<td class=\"link\">";
3611                        if ($action eq 'commitdiff') {
3612                                # link to patch
3613                                $patchno++;
3614                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3615                                print " | ";
3616                        }
3617                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3618                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3619                                      "blob");
3620                        print "</td>\n";
3621
3622                } elsif ($diff->{'status'} eq "D") { # deleted
3623                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3624                        print "<td>";
3625                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3626                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3627                                       -class => "list"}, esc_path($diff->{'file'}));
3628                        print "</td>\n";
3629                        print "<td>$mode_chng</td>\n";
3630                        print "<td class=\"link\">";
3631                        if ($action eq 'commitdiff') {
3632                                # link to patch
3633                                $patchno++;
3634                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3635                                print " | ";
3636                        }
3637                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3638                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3639                                      "blob") . " | ";
3640                        if ($have_blame) {
3641                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3642                                                             file_name=>$diff->{'file'})},
3643                                              "blame") . " | ";
3644                        }
3645                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3646                                                     file_name=>$diff->{'file'})},
3647                                      "history");
3648                        print "</td>\n";
3649
3650                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3651                        my $mode_chnge = "";
3652                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3653                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3654                                if ($from_file_type ne $to_file_type) {
3655                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3656                                }
3657                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3658                                        if ($from_mode_str && $to_mode_str) {
3659                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3660                                        } elsif ($to_mode_str) {
3661                                                $mode_chnge .= " mode: $to_mode_str";
3662                                        }
3663                                }
3664                                $mode_chnge .= "]</span>\n";
3665                        }
3666                        print "<td>";
3667                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3668                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3669                                      -class => "list"}, esc_path($diff->{'file'}));
3670                        print "</td>\n";
3671                        print "<td>$mode_chnge</td>\n";
3672                        print "<td class=\"link\">";
3673                        if ($action eq 'commitdiff') {
3674                                # link to patch
3675                                $patchno++;
3676                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3677                                      " | ";
3678                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3679                                # "commit" view and modified file (not onlu mode changed)
3680                                print $cgi->a({-href => href(action=>"blobdiff",
3681                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3682                                                             hash_base=>$hash, hash_parent_base=>$parent,
3683                                                             file_name=>$diff->{'file'})},
3684                                              "diff") .
3685                                      " | ";
3686                        }
3687                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3688                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3689                                       "blob") . " | ";
3690                        if ($have_blame) {
3691                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3692                                                             file_name=>$diff->{'file'})},
3693                                              "blame") . " | ";
3694                        }
3695                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3696                                                     file_name=>$diff->{'file'})},
3697                                      "history");
3698                        print "</td>\n";
3699
3700                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3701                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3702                        my $nstatus = $status_name{$diff->{'status'}};
3703                        my $mode_chng = "";
3704                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3705                                # mode also for directories, so we cannot use $to_mode_str
3706                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3707                        }
3708                        print "<td>" .
3709                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3710                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3711                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3712                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3713                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3714                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3715                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3716                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3717                              "<td class=\"link\">";
3718                        if ($action eq 'commitdiff') {
3719                                # link to patch
3720                                $patchno++;
3721                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3722                                      " | ";
3723                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3724                                # "commit" view and modified file (not only pure rename or copy)
3725                                print $cgi->a({-href => href(action=>"blobdiff",
3726                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3727                                                             hash_base=>$hash, hash_parent_base=>$parent,
3728                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3729                                              "diff") .
3730                                      " | ";
3731                        }
3732                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3733                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3734                                      "blob") . " | ";
3735                        if ($have_blame) {
3736                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3737                                                             file_name=>$diff->{'to_file'})},
3738                                              "blame") . " | ";
3739                        }
3740                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3741                                                    file_name=>$diff->{'to_file'})},
3742                                      "history");
3743                        print "</td>\n";
3744
3745                } # we should not encounter Unmerged (U) or Unknown (X) status
3746                print "</tr>\n";
3747        }
3748        print "</tbody>" if $has_header;
3749        print "</table>\n";
3750}
3751
3752sub git_patchset_body {
3753        my ($fd, $difftree, $hash, @hash_parents) = @_;
3754        my ($hash_parent) = $hash_parents[0];
3755
3756        my $is_combined = (@hash_parents > 1);
3757        my $patch_idx = 0;
3758        my $patch_number = 0;
3759        my $patch_line;
3760        my $diffinfo;
3761        my $to_name;
3762        my (%from, %to);
3763
3764        print "<div class=\"patchset\">\n";
3765
3766        # skip to first patch
3767        while ($patch_line = <$fd>) {
3768                chomp $patch_line;
3769
3770                last if ($patch_line =~ m/^diff /);
3771        }
3772
3773 PATCH:
3774        while ($patch_line) {
3775
3776                # parse "git diff" header line
3777                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3778                        # $1 is from_name, which we do not use
3779                        $to_name = unquote($2);
3780                        $to_name =~ s!^b/!!;
3781                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3782                        # $1 is 'cc' or 'combined', which we do not use
3783                        $to_name = unquote($2);
3784                } else {
3785                        $to_name = undef;
3786                }
3787
3788                # check if current patch belong to current raw line
3789                # and parse raw git-diff line if needed
3790                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3791                        # this is continuation of a split patch
3792                        print "<div class=\"patch cont\">\n";
3793                } else {
3794                        # advance raw git-diff output if needed
3795                        $patch_idx++ if defined $diffinfo;
3796
3797                        # read and prepare patch information
3798                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3799
3800                        # compact combined diff output can have some patches skipped
3801                        # find which patch (using pathname of result) we are at now;
3802                        if ($is_combined) {
3803                                while ($to_name ne $diffinfo->{'to_file'}) {
3804                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3805                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3806                                              "</div>\n";  # class="patch"
3807
3808                                        $patch_idx++;
3809                                        $patch_number++;
3810
3811                                        last if $patch_idx > $#$difftree;
3812                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3813                                }
3814                        }
3815
3816                        # modifies %from, %to hashes
3817                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3818
3819                        # this is first patch for raw difftree line with $patch_idx index
3820                        # we index @$difftree array from 0, but number patches from 1
3821                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3822                }
3823
3824                # git diff header
3825                #assert($patch_line =~ m/^diff /) if DEBUG;
3826                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3827                $patch_number++;
3828                # print "git diff" header
3829                print format_git_diff_header_line($patch_line, $diffinfo,
3830                                                  \%from, \%to);
3831
3832                # print extended diff header
3833                print "<div class=\"diff extended_header\">\n";
3834        EXTENDED_HEADER:
3835                while ($patch_line = <$fd>) {
3836                        chomp $patch_line;
3837
3838                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3839
3840                        print format_extended_diff_header_line($patch_line, $diffinfo,
3841                                                               \%from, \%to);
3842                }
3843                print "</div>\n"; # class="diff extended_header"
3844
3845                # from-file/to-file diff header
3846                if (! $patch_line) {
3847                        print "</div>\n"; # class="patch"
3848                        last PATCH;
3849                }
3850                next PATCH if ($patch_line =~ m/^diff /);
3851                #assert($patch_line =~ m/^---/) if DEBUG;
3852
3853                my $last_patch_line = $patch_line;
3854                $patch_line = <$fd>;
3855                chomp $patch_line;
3856                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3857
3858                print format_diff_from_to_header($last_patch_line, $patch_line,
3859                                                 $diffinfo, \%from, \%to,
3860                                                 @hash_parents);
3861
3862                # the patch itself
3863        LINE:
3864                while ($patch_line = <$fd>) {
3865                        chomp $patch_line;
3866
3867                        next PATCH if ($patch_line =~ m/^diff /);
3868
3869                        print format_diff_line($patch_line, \%from, \%to);
3870                }
3871
3872        } continue {
3873                print "</div>\n"; # class="patch"
3874        }
3875
3876        # for compact combined (--cc) format, with chunk and patch simpliciaction
3877        # patchset might be empty, but there might be unprocessed raw lines
3878        for (++$patch_idx if $patch_number > 0;
3879             $patch_idx < @$difftree;
3880             ++$patch_idx) {
3881                # read and prepare patch information
3882                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3883
3884                # generate anchor for "patch" links in difftree / whatchanged part
3885                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3886                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3887                      "</div>\n";  # class="patch"
3888
3889                $patch_number++;
3890        }
3891
3892        if ($patch_number == 0) {
3893                if (@hash_parents > 1) {
3894                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3895                } else {
3896                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3897                }
3898        }
3899
3900        print "</div>\n"; # class="patchset"
3901}
3902
3903# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3904
3905# fills project list info (age, description, owner, forks) for each
3906# project in the list, removing invalid projects from returned list
3907# NOTE: modifies $projlist, but does not remove entries from it
3908sub fill_project_list_info {
3909        my ($projlist, $check_forks) = @_;
3910        my @projects;
3911
3912        my $show_ctags = gitweb_check_feature('ctags');
3913 PROJECT:
3914        foreach my $pr (@$projlist) {
3915                my (@activity) = git_get_last_activity($pr->{'path'});
3916                unless (@activity) {
3917                        next PROJECT;
3918                }
3919                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3920                if (!defined $pr->{'descr'}) {
3921                        my $descr = git_get_project_description($pr->{'path'}) || "";
3922                        $descr = to_utf8($descr);
3923                        $pr->{'descr_long'} = $descr;
3924                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3925                }
3926                if (!defined $pr->{'owner'}) {
3927                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3928                }
3929                if ($check_forks) {
3930                        my $pname = $pr->{'path'};
3931                        if (($pname =~ s/\.git$//) &&
3932                            ($pname !~ /\/$/) &&
3933                            (-d "$projectroot/$pname")) {
3934                                $pr->{'forks'} = "-d $projectroot/$pname";
3935                        }       else {
3936                                $pr->{'forks'} = 0;
3937                        }
3938                }
3939                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3940                push @projects, $pr;
3941        }
3942
3943        return @projects;
3944}
3945
3946# print 'sort by' <th> element, generating 'sort by $name' replay link
3947# if that order is not selected
3948sub print_sort_th {
3949        my ($name, $order, $header) = @_;
3950        $header ||= ucfirst($name);
3951
3952        if ($order eq $name) {
3953                print "<th>$header</th>\n";
3954        } else {
3955                print "<th>" .
3956                      $cgi->a({-href => href(-replay=>1, order=>$name),
3957                               -class => "header"}, $header) .
3958                      "</th>\n";
3959        }
3960}
3961
3962sub git_project_list_body {
3963        # actually uses global variable $project
3964        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3965
3966        my ($check_forks) = gitweb_check_feature('forks');
3967        my @projects = fill_project_list_info($projlist, $check_forks);
3968
3969        $order ||= $default_projects_order;
3970        $from = 0 unless defined $from;
3971        $to = $#projects if (!defined $to || $#projects < $to);
3972
3973        my %order_info = (
3974                project => { key => 'path', type => 'str' },
3975                descr => { key => 'descr_long', type => 'str' },
3976                owner => { key => 'owner', type => 'str' },
3977                age => { key => 'age', type => 'num' }
3978        );
3979        my $oi = $order_info{$order};
3980        if ($oi->{'type'} eq 'str') {
3981                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3982        } else {
3983                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3984        }
3985
3986        my $show_ctags = gitweb_check_feature('ctags');
3987        if ($show_ctags) {
3988                my %ctags;
3989                foreach my $p (@projects) {
3990                        foreach my $ct (keys %{$p->{'ctags'}}) {
3991                                $ctags{$ct} += $p->{'ctags'}->{$ct};
3992                        }
3993                }
3994                my $cloud = git_populate_project_tagcloud(\%ctags);
3995                print git_show_project_tagcloud($cloud, 64);
3996        }
3997
3998        print "<table class=\"project_list\">\n";
3999        unless ($no_header) {
4000                print "<tr>\n";
4001                if ($check_forks) {
4002                        print "<th></th>\n";
4003                }
4004                print_sort_th('project', $order, 'Project');
4005                print_sort_th('descr', $order, 'Description');
4006                print_sort_th('owner', $order, 'Owner');
4007                print_sort_th('age', $order, 'Last Change');
4008                print "<th></th>\n" . # for links
4009                      "</tr>\n";
4010        }
4011        my $alternate = 1;
4012        my $tagfilter = $cgi->param('by_tag');
4013        for (my $i = $from; $i <= $to; $i++) {
4014                my $pr = $projects[$i];
4015
4016                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4017                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4018                        and not $pr->{'descr_long'} =~ /$searchtext/;
4019                # Weed out forks or non-matching entries of search
4020                if ($check_forks) {
4021                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4022                        $forkbase="^$forkbase" if $forkbase;
4023                        next if not $searchtext and not $tagfilter and $show_ctags
4024                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4025                }
4026
4027                if ($alternate) {
4028                        print "<tr class=\"dark\">\n";
4029                } else {
4030                        print "<tr class=\"light\">\n";
4031                }
4032                $alternate ^= 1;
4033                if ($check_forks) {
4034                        print "<td>";
4035                        if ($pr->{'forks'}) {
4036                                print "<!-- $pr->{'forks'} -->\n";
4037                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4038                        }
4039                        print "</td>\n";
4040                }
4041                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4042                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4043                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4044                                        -class => "list", -title => $pr->{'descr_long'}},
4045                                        esc_html($pr->{'descr'})) . "</td>\n" .
4046                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4047                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4048                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4049                      "<td class=\"link\">" .
4050                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4051                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4052                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4053                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4054                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4055                      "</td>\n" .
4056                      "</tr>\n";
4057        }
4058        if (defined $extra) {
4059                print "<tr>\n";
4060                if ($check_forks) {
4061                        print "<td></td>\n";
4062                }
4063                print "<td colspan=\"5\">$extra</td>\n" .
4064                      "</tr>\n";
4065        }
4066        print "</table>\n";
4067}
4068
4069sub git_shortlog_body {
4070        # uses global variable $project
4071        my ($commitlist, $from, $to, $refs, $extra) = @_;
4072
4073        $from = 0 unless defined $from;
4074        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4075
4076        print "<table class=\"shortlog\">\n";
4077        my $alternate = 1;
4078        for (my $i = $from; $i <= $to; $i++) {
4079                my %co = %{$commitlist->[$i]};
4080                my $commit = $co{'id'};
4081                my $ref = format_ref_marker($refs, $commit);
4082                if ($alternate) {
4083                        print "<tr class=\"dark\">\n";
4084                } else {
4085                        print "<tr class=\"light\">\n";
4086                }
4087                $alternate ^= 1;
4088                my $author = chop_and_escape_str($co{'author_name'}, 10);
4089                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4090                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4091                      "<td><i>" . $author . "</i></td>\n" .
4092                      "<td>";
4093                print format_subject_html($co{'title'}, $co{'title_short'},
4094                                          href(action=>"commit", hash=>$commit), $ref);
4095                print "</td>\n" .
4096                      "<td class=\"link\">" .
4097                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4098                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4099                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4100                my $snapshot_links = format_snapshot_links($commit);
4101                if (defined $snapshot_links) {
4102                        print " | " . $snapshot_links;
4103                }
4104                print "</td>\n" .
4105                      "</tr>\n";
4106        }
4107        if (defined $extra) {
4108                print "<tr>\n" .
4109                      "<td colspan=\"4\">$extra</td>\n" .
4110                      "</tr>\n";
4111        }
4112        print "</table>\n";
4113}
4114
4115sub git_history_body {
4116        # Warning: assumes constant type (blob or tree) during history
4117        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4118
4119        $from = 0 unless defined $from;
4120        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4121
4122        print "<table class=\"history\">\n";
4123        my $alternate = 1;
4124        for (my $i = $from; $i <= $to; $i++) {
4125                my %co = %{$commitlist->[$i]};
4126                if (!%co) {
4127                        next;
4128                }
4129                my $commit = $co{'id'};
4130
4131                my $ref = format_ref_marker($refs, $commit);
4132
4133                if ($alternate) {
4134                        print "<tr class=\"dark\">\n";
4135                } else {
4136                        print "<tr class=\"light\">\n";
4137                }
4138                $alternate ^= 1;
4139        # shortlog uses      chop_str($co{'author_name'}, 10)
4140                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4141                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4142                      "<td><i>" . $author . "</i></td>\n" .
4143                      "<td>";
4144                # originally git_history used chop_str($co{'title'}, 50)
4145                print format_subject_html($co{'title'}, $co{'title_short'},
4146                                          href(action=>"commit", hash=>$commit), $ref);
4147                print "</td>\n" .
4148                      "<td class=\"link\">" .
4149                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4150                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4151
4152                if ($ftype eq 'blob') {
4153                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4154                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4155                        if (defined $blob_current && defined $blob_parent &&
4156                                        $blob_current ne $blob_parent) {
4157                                print " | " .
4158                                        $cgi->a({-href => href(action=>"blobdiff",
4159                                                               hash=>$blob_current, hash_parent=>$blob_parent,
4160                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
4161                                                               file_name=>$file_name)},
4162                                                "diff to current");
4163                        }
4164                }
4165                print "</td>\n" .
4166                      "</tr>\n";
4167        }
4168        if (defined $extra) {
4169                print "<tr>\n" .
4170                      "<td colspan=\"4\">$extra</td>\n" .
4171                      "</tr>\n";
4172        }
4173        print "</table>\n";
4174}
4175
4176sub git_tags_body {
4177        # uses global variable $project
4178        my ($taglist, $from, $to, $extra) = @_;
4179        $from = 0 unless defined $from;
4180        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4181
4182        print "<table class=\"tags\">\n";
4183        my $alternate = 1;
4184        for (my $i = $from; $i <= $to; $i++) {
4185                my $entry = $taglist->[$i];
4186                my %tag = %$entry;
4187                my $comment = $tag{'subject'};
4188                my $comment_short;
4189                if (defined $comment) {
4190                        $comment_short = chop_str($comment, 30, 5);
4191                }
4192                if ($alternate) {
4193                        print "<tr class=\"dark\">\n";
4194                } else {
4195                        print "<tr class=\"light\">\n";
4196                }
4197                $alternate ^= 1;
4198                if (defined $tag{'age'}) {
4199                        print "<td><i>$tag{'age'}</i></td>\n";
4200                } else {
4201                        print "<td></td>\n";
4202                }
4203                print "<td>" .
4204                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4205                               -class => "list name"}, esc_html($tag{'name'})) .
4206                      "</td>\n" .
4207                      "<td>";
4208                if (defined $comment) {
4209                        print format_subject_html($comment, $comment_short,
4210                                                  href(action=>"tag", hash=>$tag{'id'}));
4211                }
4212                print "</td>\n" .
4213                      "<td class=\"selflink\">";
4214                if ($tag{'type'} eq "tag") {
4215                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4216                } else {
4217                        print "&nbsp;";
4218                }
4219                print "</td>\n" .
4220                      "<td class=\"link\">" . " | " .
4221                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4222                if ($tag{'reftype'} eq "commit") {
4223                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4224                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4225                } elsif ($tag{'reftype'} eq "blob") {
4226                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4227                }
4228                print "</td>\n" .
4229                      "</tr>";
4230        }
4231        if (defined $extra) {
4232                print "<tr>\n" .
4233                      "<td colspan=\"5\">$extra</td>\n" .
4234                      "</tr>\n";
4235        }
4236        print "</table>\n";
4237}
4238
4239sub git_heads_body {
4240        # uses global variable $project
4241        my ($headlist, $head, $from, $to, $extra) = @_;
4242        $from = 0 unless defined $from;
4243        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4244
4245        print "<table class=\"heads\">\n";
4246        my $alternate = 1;
4247        for (my $i = $from; $i <= $to; $i++) {
4248                my $entry = $headlist->[$i];
4249                my %ref = %$entry;
4250                my $curr = $ref{'id'} eq $head;
4251                if ($alternate) {
4252                        print "<tr class=\"dark\">\n";
4253                } else {
4254                        print "<tr class=\"light\">\n";
4255                }
4256                $alternate ^= 1;
4257                print "<td><i>$ref{'age'}</i></td>\n" .
4258                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4259                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4260                               -class => "list name"},esc_html($ref{'name'})) .
4261                      "</td>\n" .
4262                      "<td class=\"link\">" .
4263                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4264                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4265                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4266                      "</td>\n" .
4267                      "</tr>";
4268        }
4269        if (defined $extra) {
4270                print "<tr>\n" .
4271                      "<td colspan=\"3\">$extra</td>\n" .
4272                      "</tr>\n";
4273        }
4274        print "</table>\n";
4275}
4276
4277sub git_search_grep_body {
4278        my ($commitlist, $from, $to, $extra) = @_;
4279        $from = 0 unless defined $from;
4280        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4281
4282        print "<table class=\"commit_search\">\n";
4283        my $alternate = 1;
4284        for (my $i = $from; $i <= $to; $i++) {
4285                my %co = %{$commitlist->[$i]};
4286                if (!%co) {
4287                        next;
4288                }
4289                my $commit = $co{'id'};
4290                if ($alternate) {
4291                        print "<tr class=\"dark\">\n";
4292                } else {
4293                        print "<tr class=\"light\">\n";
4294                }
4295                $alternate ^= 1;
4296                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4297                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4298                      "<td><i>" . $author . "</i></td>\n" .
4299                      "<td>" .
4300                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4301                               -class => "list subject"},
4302                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4303                my $comment = $co{'comment'};
4304                foreach my $line (@$comment) {
4305                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4306                                my ($lead, $match, $trail) = ($1, $2, $3);
4307                                $match = chop_str($match, 70, 5, 'center');
4308                                my $contextlen = int((80 - length($match))/2);
4309                                $contextlen = 30 if ($contextlen > 30);
4310                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4311                                $trail = chop_str($trail, $contextlen, 10, 'right');
4312
4313                                $lead  = esc_html($lead);
4314                                $match = esc_html($match);
4315                                $trail = esc_html($trail);
4316
4317                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4318                        }
4319                }
4320                print "</td>\n" .
4321                      "<td class=\"link\">" .
4322                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4323                      " | " .
4324                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4325                      " | " .
4326                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4327                print "</td>\n" .
4328                      "</tr>\n";
4329        }
4330        if (defined $extra) {
4331                print "<tr>\n" .
4332                      "<td colspan=\"3\">$extra</td>\n" .
4333                      "</tr>\n";
4334        }
4335        print "</table>\n";
4336}
4337
4338## ======================================================================
4339## ======================================================================
4340## actions
4341
4342sub git_project_list {
4343        my $order = $input_params{'order'};
4344        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4345                die_error(400, "Unknown order parameter");
4346        }
4347
4348        my @list = git_get_projects_list();
4349        if (!@list) {
4350                die_error(404, "No projects found");
4351        }
4352
4353        git_header_html();
4354        if (-f $home_text) {
4355                print "<div class=\"index_include\">\n";
4356                open (my $fd, $home_text);
4357                print <$fd>;
4358                close $fd;
4359                print "</div>\n";
4360        }
4361        print $cgi->startform(-method => "get") .
4362              "<p class=\"projsearch\">Search:\n" .
4363              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4364              "</p>" .
4365              $cgi->end_form() . "\n";
4366        git_project_list_body(\@list, $order);
4367        git_footer_html();
4368}
4369
4370sub git_forks {
4371        my $order = $input_params{'order'};
4372        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4373                die_error(400, "Unknown order parameter");
4374        }
4375
4376        my @list = git_get_projects_list($project);
4377        if (!@list) {
4378                die_error(404, "No forks found");
4379        }
4380
4381        git_header_html();
4382        git_print_page_nav('','');
4383        git_print_header_div('summary', "$project forks");
4384        git_project_list_body(\@list, $order);
4385        git_footer_html();
4386}
4387
4388sub git_project_index {
4389        my @projects = git_get_projects_list($project);
4390
4391        print $cgi->header(
4392                -type => 'text/plain',
4393                -charset => 'utf-8',
4394                -content_disposition => 'inline; filename="index.aux"');
4395
4396        foreach my $pr (@projects) {
4397                if (!exists $pr->{'owner'}) {
4398                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4399                }
4400
4401                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4402                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4403                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4404                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4405                $path  =~ s/ /\+/g;
4406                $owner =~ s/ /\+/g;
4407
4408                print "$path $owner\n";
4409        }
4410}
4411
4412sub git_summary {
4413        my $descr = git_get_project_description($project) || "none";
4414        my %co = parse_commit("HEAD");
4415        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4416        my $head = $co{'id'};
4417
4418        my $owner = git_get_project_owner($project);
4419
4420        my $refs = git_get_references();
4421        # These get_*_list functions return one more to allow us to see if
4422        # there are more ...
4423        my @taglist  = git_get_tags_list(16);
4424        my @headlist = git_get_heads_list(16);
4425        my @forklist;
4426        my ($check_forks) = gitweb_check_feature('forks');
4427
4428        if ($check_forks) {
4429                @forklist = git_get_projects_list($project);
4430        }
4431
4432        git_header_html();
4433        git_print_page_nav('summary','', $head);
4434
4435        print "<div class=\"title\">&nbsp;</div>\n";
4436        print "<table class=\"projects_list\">\n" .
4437              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4438              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4439        if (defined $cd{'rfc2822'}) {
4440                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4441        }
4442
4443        # use per project git URL list in $projectroot/$project/cloneurl
4444        # or make project git URL from git base URL and project name
4445        my $url_tag = "URL";
4446        my @url_list = git_get_project_url_list($project);
4447        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4448        foreach my $git_url (@url_list) {
4449                next unless $git_url;
4450                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4451                $url_tag = "";
4452        }
4453
4454        # Tag cloud
4455        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4456        if ($show_ctags) {
4457                my $ctags = git_get_project_ctags($project);
4458                my $cloud = git_populate_project_tagcloud($ctags);
4459                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4460                print "</td>\n<td>" unless %$ctags;
4461                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4462                print "</td>\n<td>" if %$ctags;
4463                print git_show_project_tagcloud($cloud, 48);
4464                print "</td></tr>";
4465        }
4466
4467        print "</table>\n";
4468
4469        if (-s "$projectroot/$project/README.html") {
4470                if (open my $fd, "$projectroot/$project/README.html") {
4471                        print "<div class=\"title\">readme</div>\n" .
4472                              "<div class=\"readme\">\n";
4473                        print $_ while (<$fd>);
4474                        print "\n</div>\n"; # class="readme"
4475                        close $fd;
4476                }
4477        }
4478
4479        # we need to request one more than 16 (0..15) to check if
4480        # those 16 are all
4481        my @commitlist = $head ? parse_commits($head, 17) : ();
4482        if (@commitlist) {
4483                git_print_header_div('shortlog');
4484                git_shortlog_body(\@commitlist, 0, 15, $refs,
4485                                  $#commitlist <=  15 ? undef :
4486                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4487        }
4488
4489        if (@taglist) {
4490                git_print_header_div('tags');
4491                git_tags_body(\@taglist, 0, 15,
4492                              $#taglist <=  15 ? undef :
4493                              $cgi->a({-href => href(action=>"tags")}, "..."));
4494        }
4495
4496        if (@headlist) {
4497                git_print_header_div('heads');
4498                git_heads_body(\@headlist, $head, 0, 15,
4499                               $#headlist <= 15 ? undef :
4500                               $cgi->a({-href => href(action=>"heads")}, "..."));
4501        }
4502
4503        if (@forklist) {
4504                git_print_header_div('forks');
4505                git_project_list_body(\@forklist, 'age', 0, 15,
4506                                      $#forklist <= 15 ? undef :
4507                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4508                                      'no_header');
4509        }
4510
4511        git_footer_html();
4512}
4513
4514sub git_tag {
4515        my $head = git_get_head_hash($project);
4516        git_header_html();
4517        git_print_page_nav('','', $head,undef,$head);
4518        my %tag = parse_tag($hash);
4519
4520        if (! %tag) {
4521                die_error(404, "Unknown tag object");
4522        }
4523
4524        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4525        print "<div class=\"title_text\">\n" .
4526              "<table class=\"object_header\">\n" .
4527              "<tr>\n" .
4528              "<td>object</td>\n" .
4529              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4530                               $tag{'object'}) . "</td>\n" .
4531              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4532                                              $tag{'type'}) . "</td>\n" .
4533              "</tr>\n";
4534        if (defined($tag{'author'})) {
4535                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4536                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4537                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4538                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4539                        "</td></tr>\n";
4540        }
4541        print "</table>\n\n" .
4542              "</div>\n";
4543        print "<div class=\"page_body\">";
4544        my $comment = $tag{'comment'};
4545        foreach my $line (@$comment) {
4546                chomp $line;
4547                print esc_html($line, -nbsp=>1) . "<br/>\n";
4548        }
4549        print "</div>\n";
4550        git_footer_html();
4551}
4552
4553sub git_blame {
4554        my $fd;
4555        my $ftype;
4556
4557        gitweb_check_feature('blame')
4558            or die_error(403, "Blame view not allowed");
4559
4560        die_error(400, "No file name given") unless $file_name;
4561        $hash_base ||= git_get_head_hash($project);
4562        die_error(404, "Couldn't find base commit") unless ($hash_base);
4563        my %co = parse_commit($hash_base)
4564                or die_error(404, "Commit not found");
4565        if (!defined $hash) {
4566                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4567                        or die_error(404, "Error looking up file");
4568        }
4569        $ftype = git_get_type($hash);
4570        if ($ftype !~ "blob") {
4571                die_error(400, "Object is not a blob");
4572        }
4573        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4574              $file_name, $hash_base)
4575                or die_error(500, "Open git-blame failed");
4576        git_header_html();
4577        my $formats_nav =
4578                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4579                        "blob") .
4580                " | " .
4581                $cgi->a({-href => href(action=>"history", -replay=>1)},
4582                        "history") .
4583                " | " .
4584                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4585                        "HEAD");
4586        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4587        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4588        git_print_page_path($file_name, $ftype, $hash_base);
4589        my @rev_color = (qw(light2 dark2));
4590        my $num_colors = scalar(@rev_color);
4591        my $current_color = 0;
4592        my $last_rev;
4593        print <<HTML;
4594<div class="page_body">
4595<table class="blame">
4596<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4597HTML
4598        my %metainfo = ();
4599        while (1) {
4600                $_ = <$fd>;
4601                last unless defined $_;
4602                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4603                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4604                if (!exists $metainfo{$full_rev}) {
4605                        $metainfo{$full_rev} = {};
4606                }
4607                my $meta = $metainfo{$full_rev};
4608                while (<$fd>) {
4609                        last if (s/^\t//);
4610                        if (/^(\S+) (.*)$/) {
4611                                $meta->{$1} = $2;
4612                        }
4613                }
4614                my $data = $_;
4615                chomp $data;
4616                my $rev = substr($full_rev, 0, 8);
4617                my $author = $meta->{'author'};
4618                my %date = parse_date($meta->{'author-time'},
4619                                      $meta->{'author-tz'});
4620                my $date = $date{'iso-tz'};
4621                if ($group_size) {
4622                        $current_color = ++$current_color % $num_colors;
4623                }
4624                print "<tr class=\"$rev_color[$current_color]\">\n";
4625                if ($group_size) {
4626                        print "<td class=\"sha1\"";
4627                        print " title=\"". esc_html($author) . ", $date\"";
4628                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4629                        print ">";
4630                        print $cgi->a({-href => href(action=>"commit",
4631                                                     hash=>$full_rev,
4632                                                     file_name=>$file_name)},
4633                                      esc_html($rev));
4634                        print "</td>\n";
4635                }
4636                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4637                        or die_error(500, "Open git-rev-parse failed");
4638                my $parent_commit = <$dd>;
4639                close $dd;
4640                chomp($parent_commit);
4641                my $blamed = href(action => 'blame',
4642                                  file_name => $meta->{'filename'},
4643                                  hash_base => $parent_commit);
4644                print "<td class=\"linenr\">";
4645                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4646                                -id => "l$lineno",
4647                                -class => "linenr" },
4648                              esc_html($lineno));
4649                print "</td>";
4650                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4651                print "</tr>\n";
4652        }
4653        print "</table>\n";
4654        print "</div>";
4655        close $fd
4656                or print "Reading blob failed\n";
4657        git_footer_html();
4658}
4659
4660sub git_tags {
4661        my $head = git_get_head_hash($project);
4662        git_header_html();
4663        git_print_page_nav('','', $head,undef,$head);
4664        git_print_header_div('summary', $project);
4665
4666        my @tagslist = git_get_tags_list();
4667        if (@tagslist) {
4668                git_tags_body(\@tagslist);
4669        }
4670        git_footer_html();
4671}
4672
4673sub git_heads {
4674        my $head = git_get_head_hash($project);
4675        git_header_html();
4676        git_print_page_nav('','', $head,undef,$head);
4677        git_print_header_div('summary', $project);
4678
4679        my @headslist = git_get_heads_list();
4680        if (@headslist) {
4681                git_heads_body(\@headslist, $head);
4682        }
4683        git_footer_html();
4684}
4685
4686sub git_blob_plain {
4687        my $type = shift;
4688        my $expires;
4689
4690        if (!defined $hash) {
4691                if (defined $file_name) {
4692                        my $base = $hash_base || git_get_head_hash($project);
4693                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4694                                or die_error(404, "Cannot find file");
4695                } else {
4696                        die_error(400, "No file name defined");
4697                }
4698        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4699                # blobs defined by non-textual hash id's can be cached
4700                $expires = "+1d";
4701        }
4702
4703        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4704                or die_error(500, "Open git-cat-file blob '$hash' failed");
4705
4706        # content-type (can include charset)
4707        $type = blob_contenttype($fd, $file_name, $type);
4708
4709        # "save as" filename, even when no $file_name is given
4710        my $save_as = "$hash";
4711        if (defined $file_name) {
4712                $save_as = $file_name;
4713        } elsif ($type =~ m/^text\//) {
4714                $save_as .= '.txt';
4715        }
4716
4717        print $cgi->header(
4718                -type => $type,
4719                -expires => $expires,
4720                -content_disposition => 'inline; filename="' . $save_as . '"');
4721        undef $/;
4722        binmode STDOUT, ':raw';
4723        print <$fd>;
4724        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4725        $/ = "\n";
4726        close $fd;
4727}
4728
4729sub git_blob {
4730        my $expires;
4731
4732        if (!defined $hash) {
4733                if (defined $file_name) {
4734                        my $base = $hash_base || git_get_head_hash($project);
4735                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4736                                or die_error(404, "Cannot find file");
4737                } else {
4738                        die_error(400, "No file name defined");
4739                }
4740        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4741                # blobs defined by non-textual hash id's can be cached
4742                $expires = "+1d";
4743        }
4744
4745        my ($have_blame) = gitweb_check_feature('blame');
4746        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4747                or die_error(500, "Couldn't cat $file_name, $hash");
4748        my $mimetype = blob_mimetype($fd, $file_name);
4749        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4750                close $fd;
4751                return git_blob_plain($mimetype);
4752        }
4753        # we can have blame only for text/* mimetype
4754        $have_blame &&= ($mimetype =~ m!^text/!);
4755
4756        git_header_html(undef, $expires);
4757        my $formats_nav = '';
4758        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4759                if (defined $file_name) {
4760                        if ($have_blame) {
4761                                $formats_nav .=
4762                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4763                                                "blame") .
4764                                        " | ";
4765                        }
4766                        $formats_nav .=
4767                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4768                                        "history") .
4769                                " | " .
4770                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4771                                        "raw") .
4772                                " | " .
4773                                $cgi->a({-href => href(action=>"blob",
4774                                                       hash_base=>"HEAD", file_name=>$file_name)},
4775                                        "HEAD");
4776                } else {
4777                        $formats_nav .=
4778                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4779                                        "raw");
4780                }
4781                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4782                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4783        } else {
4784                print "<div class=\"page_nav\">\n" .
4785                      "<br/><br/></div>\n" .
4786                      "<div class=\"title\">$hash</div>\n";
4787        }
4788        git_print_page_path($file_name, "blob", $hash_base);
4789        print "<div class=\"page_body\">\n";
4790        if ($mimetype =~ m!^image/!) {
4791                print qq!<img type="$mimetype"!;
4792                if ($file_name) {
4793                        print qq! alt="$file_name" title="$file_name"!;
4794                }
4795                print qq! src="! .
4796                      href(action=>"blob_plain", hash=>$hash,
4797                           hash_base=>$hash_base, file_name=>$file_name) .
4798                      qq!" />\n!;
4799        } else {
4800                my $nr;
4801                while (my $line = <$fd>) {
4802                        chomp $line;
4803                        $nr++;
4804                        $line = untabify($line);
4805                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4806                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4807                }
4808        }
4809        close $fd
4810                or print "Reading blob failed.\n";
4811        print "</div>";
4812        git_footer_html();
4813}
4814
4815sub git_tree {
4816        if (!defined $hash_base) {
4817                $hash_base = "HEAD";
4818        }
4819        if (!defined $hash) {
4820                if (defined $file_name) {
4821                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4822                } else {
4823                        $hash = $hash_base;
4824                }
4825        }
4826        die_error(404, "No such tree") unless defined($hash);
4827        $/ = "\0";
4828        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4829                or die_error(500, "Open git-ls-tree failed");
4830        my @entries = map { chomp; $_ } <$fd>;
4831        close $fd or die_error(404, "Reading tree failed");
4832        $/ = "\n";
4833
4834        my $refs = git_get_references();
4835        my $ref = format_ref_marker($refs, $hash_base);
4836        git_header_html();
4837        my $basedir = '';
4838        my ($have_blame) = gitweb_check_feature('blame');
4839        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4840                my @views_nav = ();
4841                if (defined $file_name) {
4842                        push @views_nav,
4843                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4844                                        "history"),
4845                                $cgi->a({-href => href(action=>"tree",
4846                                                       hash_base=>"HEAD", file_name=>$file_name)},
4847                                        "HEAD"),
4848                }
4849                my $snapshot_links = format_snapshot_links($hash);
4850                if (defined $snapshot_links) {
4851                        # FIXME: Should be available when we have no hash base as well.
4852                        push @views_nav, $snapshot_links;
4853                }
4854                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4855                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4856        } else {
4857                undef $hash_base;
4858                print "<div class=\"page_nav\">\n";
4859                print "<br/><br/></div>\n";
4860                print "<div class=\"title\">$hash</div>\n";
4861        }
4862        if (defined $file_name) {
4863                $basedir = $file_name;
4864                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4865                        $basedir .= '/';
4866                }
4867                git_print_page_path($file_name, 'tree', $hash_base);
4868        }
4869        print "<div class=\"page_body\">\n";
4870        print "<table class=\"tree\">\n";
4871        my $alternate = 1;
4872        # '..' (top directory) link if possible
4873        if (defined $hash_base &&
4874            defined $file_name && $file_name =~ m![^/]+$!) {
4875                if ($alternate) {
4876                        print "<tr class=\"dark\">\n";
4877                } else {
4878                        print "<tr class=\"light\">\n";
4879                }
4880                $alternate ^= 1;
4881
4882                my $up = $file_name;
4883                $up =~ s!/?[^/]+$!!;
4884                undef $up unless $up;
4885                # based on git_print_tree_entry
4886                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4887                print '<td class="list">';
4888                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4889                                             file_name=>$up)},
4890                              "..");
4891                print "</td>\n";
4892                print "<td class=\"link\"></td>\n";
4893
4894                print "</tr>\n";
4895        }
4896        foreach my $line (@entries) {
4897                my %t = parse_ls_tree_line($line, -z => 1);
4898
4899                if ($alternate) {
4900                        print "<tr class=\"dark\">\n";
4901                } else {
4902                        print "<tr class=\"light\">\n";
4903                }
4904                $alternate ^= 1;
4905
4906                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4907
4908                print "</tr>\n";
4909        }
4910        print "</table>\n" .
4911              "</div>";
4912        git_footer_html();
4913}
4914
4915sub git_snapshot {
4916        my $format = $input_params{'snapshot_format'};
4917        if (!@snapshot_fmts) {
4918                die_error(403, "Snapshots not allowed");
4919        }
4920        # default to first supported snapshot format
4921        $format ||= $snapshot_fmts[0];
4922        if ($format !~ m/^[a-z0-9]+$/) {
4923                die_error(400, "Invalid snapshot format parameter");
4924        } elsif (!exists($known_snapshot_formats{$format})) {
4925                die_error(400, "Unknown snapshot format");
4926        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
4927                die_error(403, "Unsupported snapshot format");
4928        }
4929
4930        if (!defined $hash) {
4931                $hash = git_get_head_hash($project);
4932        }
4933
4934        my $name = $project;
4935        $name =~ s,([^/])/*\.git$,$1,;
4936        $name = basename($name);
4937        my $filename = to_utf8($name);
4938        $name =~ s/\047/\047\\\047\047/g;
4939        my $cmd;
4940        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4941        $cmd = quote_command(
4942                git_cmd(), 'archive',
4943                "--format=$known_snapshot_formats{$format}{'format'}",
4944                "--prefix=$name/", $hash);
4945        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4946                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4947        }
4948
4949        print $cgi->header(
4950                -type => $known_snapshot_formats{$format}{'type'},
4951                -content_disposition => 'inline; filename="' . "$filename" . '"',
4952                -status => '200 OK');
4953
4954        open my $fd, "-|", $cmd
4955                or die_error(500, "Execute git-archive failed");
4956        binmode STDOUT, ':raw';
4957        print <$fd>;
4958        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4959        close $fd;
4960}
4961
4962sub git_log {
4963        my $head = git_get_head_hash($project);
4964        if (!defined $hash) {
4965                $hash = $head;
4966        }
4967        if (!defined $page) {
4968                $page = 0;
4969        }
4970        my $refs = git_get_references();
4971
4972        my @commitlist = parse_commits($hash, 101, (100 * $page));
4973
4974        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4975
4976        git_header_html();
4977        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4978
4979        if (!@commitlist) {
4980                my %co = parse_commit($hash);
4981
4982                git_print_header_div('summary', $project);
4983                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4984        }
4985        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4986        for (my $i = 0; $i <= $to; $i++) {
4987                my %co = %{$commitlist[$i]};
4988                next if !%co;
4989                my $commit = $co{'id'};
4990                my $ref = format_ref_marker($refs, $commit);
4991                my %ad = parse_date($co{'author_epoch'});
4992                git_print_header_div('commit',
4993                               "<span class=\"age\">$co{'age_string'}</span>" .
4994                               esc_html($co{'title'}) . $ref,
4995                               $commit);
4996                print "<div class=\"title_text\">\n" .
4997                      "<div class=\"log_link\">\n" .
4998                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4999                      " | " .
5000                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5001                      " | " .
5002                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5003                      "<br/>\n" .
5004                      "</div>\n" .
5005                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
5006                      "</div>\n";
5007
5008                print "<div class=\"log_body\">\n";
5009                git_print_log($co{'comment'}, -final_empty_line=> 1);
5010                print "</div>\n";
5011        }
5012        if ($#commitlist >= 100) {
5013                print "<div class=\"page_nav\">\n";
5014                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5015                               -accesskey => "n", -title => "Alt-n"}, "next");
5016                print "</div>\n";
5017        }
5018        git_footer_html();
5019}
5020
5021sub git_commit {
5022        $hash ||= $hash_base || "HEAD";
5023        my %co = parse_commit($hash)
5024            or die_error(404, "Unknown commit object");
5025        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5026        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
5027
5028        my $parent  = $co{'parent'};
5029        my $parents = $co{'parents'}; # listref
5030
5031        # we need to prepare $formats_nav before any parameter munging
5032        my $formats_nav;
5033        if (!defined $parent) {
5034                # --root commitdiff
5035                $formats_nav .= '(initial)';
5036        } elsif (@$parents == 1) {
5037                # single parent commit
5038                $formats_nav .=
5039                        '(parent: ' .
5040                        $cgi->a({-href => href(action=>"commit",
5041                                               hash=>$parent)},
5042                                esc_html(substr($parent, 0, 7))) .
5043                        ')';
5044        } else {
5045                # merge commit
5046                $formats_nav .=
5047                        '(merge: ' .
5048                        join(' ', map {
5049                                $cgi->a({-href => href(action=>"commit",
5050                                                       hash=>$_)},
5051                                        esc_html(substr($_, 0, 7)));
5052                        } @$parents ) .
5053                        ')';
5054        }
5055
5056        if (!defined $parent) {
5057                $parent = "--root";
5058        }
5059        my @difftree;
5060        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5061                @diff_opts,
5062                (@$parents <= 1 ? $parent : '-c'),
5063                $hash, "--"
5064                or die_error(500, "Open git-diff-tree failed");
5065        @difftree = map { chomp; $_ } <$fd>;
5066        close $fd or die_error(404, "Reading git-diff-tree failed");
5067
5068        # non-textual hash id's can be cached
5069        my $expires;
5070        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5071                $expires = "+1d";
5072        }
5073        my $refs = git_get_references();
5074        my $ref = format_ref_marker($refs, $co{'id'});
5075
5076        git_header_html(undef, $expires);
5077        git_print_page_nav('commit', '',
5078                           $hash, $co{'tree'}, $hash,
5079                           $formats_nav);
5080
5081        if (defined $co{'parent'}) {
5082                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5083        } else {
5084                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5085        }
5086        print "<div class=\"title_text\">\n" .
5087              "<table class=\"object_header\">\n";
5088        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5089              "<tr>" .
5090              "<td></td><td> $ad{'rfc2822'}";
5091        if ($ad{'hour_local'} < 6) {
5092                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5093                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5094        } else {
5095                printf(" (%02d:%02d %s)",
5096                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5097        }
5098        print "</td>" .
5099              "</tr>\n";
5100        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5101        print "<tr><td></td><td> $cd{'rfc2822'}" .
5102              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5103              "</td></tr>\n";
5104        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5105        print "<tr>" .
5106              "<td>tree</td>" .
5107              "<td class=\"sha1\">" .
5108              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5109                       class => "list"}, $co{'tree'}) .
5110              "</td>" .
5111              "<td class=\"link\">" .
5112              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5113                      "tree");
5114        my $snapshot_links = format_snapshot_links($hash);
5115        if (defined $snapshot_links) {
5116                print " | " . $snapshot_links;
5117        }
5118        print "</td>" .
5119              "</tr>\n";
5120
5121        foreach my $par (@$parents) {
5122                print "<tr>" .
5123                      "<td>parent</td>" .
5124                      "<td class=\"sha1\">" .
5125                      $cgi->a({-href => href(action=>"commit", hash=>$par),
5126                               class => "list"}, $par) .
5127                      "</td>" .
5128                      "<td class=\"link\">" .
5129                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5130                      " | " .
5131                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5132                      "</td>" .
5133                      "</tr>\n";
5134        }
5135        print "</table>".
5136              "</div>\n";
5137
5138        print "<div class=\"page_body\">\n";
5139        git_print_log($co{'comment'});
5140        print "</div>\n";
5141
5142        git_difftree_body(\@difftree, $hash, @$parents);
5143
5144        git_footer_html();
5145}
5146
5147sub git_object {
5148        # object is defined by:
5149        # - hash or hash_base alone
5150        # - hash_base and file_name
5151        my $type;
5152
5153        # - hash or hash_base alone
5154        if ($hash || ($hash_base && !defined $file_name)) {
5155                my $object_id = $hash || $hash_base;
5156
5157                open my $fd, "-|", quote_command(
5158                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5159                        or die_error(404, "Object does not exist");
5160                $type = <$fd>;
5161                chomp $type;
5162                close $fd
5163                        or die_error(404, "Object does not exist");
5164
5165        # - hash_base and file_name
5166        } elsif ($hash_base && defined $file_name) {
5167                $file_name =~ s,/+$,,;
5168
5169                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5170                        or die_error(404, "Base object does not exist");
5171
5172                # here errors should not hapen
5173                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5174                        or die_error(500, "Open git-ls-tree failed");
5175                my $line = <$fd>;
5176                close $fd;
5177
5178                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5179                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5180                        die_error(404, "File or directory for given base does not exist");
5181                }
5182                $type = $2;
5183                $hash = $3;
5184        } else {
5185                die_error(400, "Not enough information to find object");
5186        }
5187
5188        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5189                                          hash=>$hash, hash_base=>$hash_base,
5190                                          file_name=>$file_name),
5191                             -status => '302 Found');
5192}
5193
5194sub git_blobdiff {
5195        my $format = shift || 'html';
5196
5197        my $fd;
5198        my @difftree;
5199        my %diffinfo;
5200        my $expires;
5201
5202        # preparing $fd and %diffinfo for git_patchset_body
5203        # new style URI
5204        if (defined $hash_base && defined $hash_parent_base) {
5205                if (defined $file_name) {
5206                        # read raw output
5207                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5208                                $hash_parent_base, $hash_base,
5209                                "--", (defined $file_parent ? $file_parent : ()), $file_name
5210                                or die_error(500, "Open git-diff-tree failed");
5211                        @difftree = map { chomp; $_ } <$fd>;
5212                        close $fd
5213                                or die_error(404, "Reading git-diff-tree failed");
5214                        @difftree
5215                                or die_error(404, "Blob diff not found");
5216
5217                } elsif (defined $hash &&
5218                         $hash =~ /[0-9a-fA-F]{40}/) {
5219                        # try to find filename from $hash
5220
5221                        # read filtered raw output
5222                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5223                                $hash_parent_base, $hash_base, "--"
5224                                or die_error(500, "Open git-diff-tree failed");
5225                        @difftree =
5226                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5227                                # $hash == to_id
5228                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5229                                map { chomp; $_ } <$fd>;
5230                        close $fd
5231                                or die_error(404, "Reading git-diff-tree failed");
5232                        @difftree
5233                                or die_error(404, "Blob diff not found");
5234
5235                } else {
5236                        die_error(400, "Missing one of the blob diff parameters");
5237                }
5238
5239                if (@difftree > 1) {
5240                        die_error(400, "Ambiguous blob diff specification");
5241                }
5242
5243                %diffinfo = parse_difftree_raw_line($difftree[0]);
5244                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5245                $file_name   ||= $diffinfo{'to_file'};
5246
5247                $hash_parent ||= $diffinfo{'from_id'};
5248                $hash        ||= $diffinfo{'to_id'};
5249
5250                # non-textual hash id's can be cached
5251                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5252                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5253                        $expires = '+1d';
5254                }
5255
5256                # open patch output
5257                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5258                        '-p', ($format eq 'html' ? "--full-index" : ()),
5259                        $hash_parent_base, $hash_base,
5260                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5261                        or die_error(500, "Open git-diff-tree failed");
5262        }
5263
5264        # old/legacy style URI
5265        if (!%diffinfo && # if new style URI failed
5266            defined $hash && defined $hash_parent) {
5267                # fake git-diff-tree raw output
5268                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5269                $diffinfo{'from_id'} = $hash_parent;
5270                $diffinfo{'to_id'}   = $hash;
5271                if (defined $file_name) {
5272                        if (defined $file_parent) {
5273                                $diffinfo{'status'} = '2';
5274                                $diffinfo{'from_file'} = $file_parent;
5275                                $diffinfo{'to_file'}   = $file_name;
5276                        } else { # assume not renamed
5277                                $diffinfo{'status'} = '1';
5278                                $diffinfo{'from_file'} = $file_name;
5279                                $diffinfo{'to_file'}   = $file_name;
5280                        }
5281                } else { # no filename given
5282                        $diffinfo{'status'} = '2';
5283                        $diffinfo{'from_file'} = $hash_parent;
5284                        $diffinfo{'to_file'}   = $hash;
5285                }
5286
5287                # non-textual hash id's can be cached
5288                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5289                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5290                        $expires = '+1d';
5291                }
5292
5293                # open patch output
5294                open $fd, "-|", git_cmd(), "diff", @diff_opts,
5295                        '-p', ($format eq 'html' ? "--full-index" : ()),
5296                        $hash_parent, $hash, "--"
5297                        or die_error(500, "Open git-diff failed");
5298        } else  {
5299                die_error(400, "Missing one of the blob diff parameters")
5300                        unless %diffinfo;
5301        }
5302
5303        # header
5304        if ($format eq 'html') {
5305                my $formats_nav =
5306                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5307                                "raw");
5308                git_header_html(undef, $expires);
5309                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5310                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5311                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5312                } else {
5313                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5314                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5315                }
5316                if (defined $file_name) {
5317                        git_print_page_path($file_name, "blob", $hash_base);
5318                } else {
5319                        print "<div class=\"page_path\"></div>\n";
5320                }
5321
5322        } elsif ($format eq 'plain') {
5323                print $cgi->header(
5324                        -type => 'text/plain',
5325                        -charset => 'utf-8',
5326                        -expires => $expires,
5327                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5328
5329                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5330
5331        } else {
5332                die_error(400, "Unknown blobdiff format");
5333        }
5334
5335        # patch
5336        if ($format eq 'html') {
5337                print "<div class=\"page_body\">\n";
5338
5339                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5340                close $fd;
5341
5342                print "</div>\n"; # class="page_body"
5343                git_footer_html();
5344
5345        } else {
5346                while (my $line = <$fd>) {
5347                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5348                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5349
5350                        print $line;
5351
5352                        last if $line =~ m!^\+\+\+!;
5353                }
5354                local $/ = undef;
5355                print <$fd>;
5356                close $fd;
5357        }
5358}
5359
5360sub git_blobdiff_plain {
5361        git_blobdiff('plain');
5362}
5363
5364sub git_commitdiff {
5365        my $format = shift || 'html';
5366        $hash ||= $hash_base || "HEAD";
5367        my %co = parse_commit($hash)
5368            or die_error(404, "Unknown commit object");
5369
5370        # choose format for commitdiff for merge
5371        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5372                $hash_parent = '--cc';
5373        }
5374        # we need to prepare $formats_nav before almost any parameter munging
5375        my $formats_nav;
5376        if ($format eq 'html') {
5377                $formats_nav =
5378                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5379                                "raw");
5380
5381                if (defined $hash_parent &&
5382                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5383                        # commitdiff with two commits given
5384                        my $hash_parent_short = $hash_parent;
5385                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5386                                $hash_parent_short = substr($hash_parent, 0, 7);
5387                        }
5388                        $formats_nav .=
5389                                ' (from';
5390                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5391                                if ($co{'parents'}[$i] eq $hash_parent) {
5392                                        $formats_nav .= ' parent ' . ($i+1);
5393                                        last;
5394                                }
5395                        }
5396                        $formats_nav .= ': ' .
5397                                $cgi->a({-href => href(action=>"commitdiff",
5398                                                       hash=>$hash_parent)},
5399                                        esc_html($hash_parent_short)) .
5400                                ')';
5401                } elsif (!$co{'parent'}) {
5402                        # --root commitdiff
5403                        $formats_nav .= ' (initial)';
5404                } elsif (scalar @{$co{'parents'}} == 1) {
5405                        # single parent commit
5406                        $formats_nav .=
5407                                ' (parent: ' .
5408                                $cgi->a({-href => href(action=>"commitdiff",
5409                                                       hash=>$co{'parent'})},
5410                                        esc_html(substr($co{'parent'}, 0, 7))) .
5411                                ')';
5412                } else {
5413                        # merge commit
5414                        if ($hash_parent eq '--cc') {
5415                                $formats_nav .= ' | ' .
5416                                        $cgi->a({-href => href(action=>"commitdiff",
5417                                                               hash=>$hash, hash_parent=>'-c')},
5418                                                'combined');
5419                        } else { # $hash_parent eq '-c'
5420                                $formats_nav .= ' | ' .
5421                                        $cgi->a({-href => href(action=>"commitdiff",
5422                                                               hash=>$hash, hash_parent=>'--cc')},
5423                                                'compact');
5424                        }
5425                        $formats_nav .=
5426                                ' (merge: ' .
5427                                join(' ', map {
5428                                        $cgi->a({-href => href(action=>"commitdiff",
5429                                                               hash=>$_)},
5430                                                esc_html(substr($_, 0, 7)));
5431                                } @{$co{'parents'}} ) .
5432                                ')';
5433                }
5434        }
5435
5436        my $hash_parent_param = $hash_parent;
5437        if (!defined $hash_parent_param) {
5438                # --cc for multiple parents, --root for parentless
5439                $hash_parent_param =
5440                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5441        }
5442
5443        # read commitdiff
5444        my $fd;
5445        my @difftree;
5446        if ($format eq 'html') {
5447                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5448                        "--no-commit-id", "--patch-with-raw", "--full-index",
5449                        $hash_parent_param, $hash, "--"
5450                        or die_error(500, "Open git-diff-tree failed");
5451
5452                while (my $line = <$fd>) {
5453                        chomp $line;
5454                        # empty line ends raw part of diff-tree output
5455                        last unless $line;
5456                        push @difftree, scalar parse_difftree_raw_line($line);
5457                }
5458
5459        } elsif ($format eq 'plain') {
5460                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5461                        '-p', $hash_parent_param, $hash, "--"
5462                        or die_error(500, "Open git-diff-tree failed");
5463
5464        } else {
5465                die_error(400, "Unknown commitdiff format");
5466        }
5467
5468        # non-textual hash id's can be cached
5469        my $expires;
5470        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5471                $expires = "+1d";
5472        }
5473
5474        # write commit message
5475        if ($format eq 'html') {
5476                my $refs = git_get_references();
5477                my $ref = format_ref_marker($refs, $co{'id'});
5478
5479                git_header_html(undef, $expires);
5480                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5481                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5482                git_print_authorship(\%co);
5483                print "<div class=\"page_body\">\n";
5484                if (@{$co{'comment'}} > 1) {
5485                        print "<div class=\"log\">\n";
5486                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5487                        print "</div>\n"; # class="log"
5488                }
5489
5490        } elsif ($format eq 'plain') {
5491                my $refs = git_get_references("tags");
5492                my $tagname = git_get_rev_name_tags($hash);
5493                my $filename = basename($project) . "-$hash.patch";
5494
5495                print $cgi->header(
5496                        -type => 'text/plain',
5497                        -charset => 'utf-8',
5498                        -expires => $expires,
5499                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5500                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5501                print "From: " . to_utf8($co{'author'}) . "\n";
5502                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5503                print "Subject: " . to_utf8($co{'title'}) . "\n";
5504
5505                print "X-Git-Tag: $tagname\n" if $tagname;
5506                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5507
5508                foreach my $line (@{$co{'comment'}}) {
5509                        print to_utf8($line) . "\n";
5510                }
5511                print "---\n\n";
5512        }
5513
5514        # write patch
5515        if ($format eq 'html') {
5516                my $use_parents = !defined $hash_parent ||
5517                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5518                git_difftree_body(\@difftree, $hash,
5519                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5520                print "<br/>\n";
5521
5522                git_patchset_body($fd, \@difftree, $hash,
5523                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5524                close $fd;
5525                print "</div>\n"; # class="page_body"
5526                git_footer_html();
5527
5528        } elsif ($format eq 'plain') {
5529                local $/ = undef;
5530                print <$fd>;
5531                close $fd
5532                        or print "Reading git-diff-tree failed\n";
5533        }
5534}
5535
5536sub git_commitdiff_plain {
5537        git_commitdiff('plain');
5538}
5539
5540sub git_history {
5541        if (!defined $hash_base) {
5542                $hash_base = git_get_head_hash($project);
5543        }
5544        if (!defined $page) {
5545                $page = 0;
5546        }
5547        my $ftype;
5548        my %co = parse_commit($hash_base)
5549            or die_error(404, "Unknown commit object");
5550
5551        my $refs = git_get_references();
5552        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5553
5554        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5555                                       $file_name, "--full-history")
5556            or die_error(404, "No such file or directory on given branch");
5557
5558        if (!defined $hash && defined $file_name) {
5559                # some commits could have deleted file in question,
5560                # and not have it in tree, but one of them has to have it
5561                for (my $i = 0; $i <= @commitlist; $i++) {
5562                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5563                        last if defined $hash;
5564                }
5565        }
5566        if (defined $hash) {
5567                $ftype = git_get_type($hash);
5568        }
5569        if (!defined $ftype) {
5570                die_error(500, "Unknown type of object");
5571        }
5572
5573        my $paging_nav = '';
5574        if ($page > 0) {
5575                $paging_nav .=
5576                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5577                                               file_name=>$file_name)},
5578                                "first");
5579                $paging_nav .= " &sdot; " .
5580                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5581                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5582        } else {
5583                $paging_nav .= "first";
5584                $paging_nav .= " &sdot; prev";
5585        }
5586        my $next_link = '';
5587        if ($#commitlist >= 100) {
5588                $next_link =
5589                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5590                                 -accesskey => "n", -title => "Alt-n"}, "next");
5591                $paging_nav .= " &sdot; $next_link";
5592        } else {
5593                $paging_nav .= " &sdot; next";
5594        }
5595
5596        git_header_html();
5597        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5598        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5599        git_print_page_path($file_name, $ftype, $hash_base);
5600
5601        git_history_body(\@commitlist, 0, 99,
5602                         $refs, $hash_base, $ftype, $next_link);
5603
5604        git_footer_html();
5605}
5606
5607sub git_search {
5608        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5609        if (!defined $searchtext) {
5610                die_error(400, "Text field is empty");
5611        }
5612        if (!defined $hash) {
5613                $hash = git_get_head_hash($project);
5614        }
5615        my %co = parse_commit($hash);
5616        if (!%co) {
5617                die_error(404, "Unknown commit object");
5618        }
5619        if (!defined $page) {
5620                $page = 0;
5621        }
5622
5623        $searchtype ||= 'commit';
5624        if ($searchtype eq 'pickaxe') {
5625                # pickaxe may take all resources of your box and run for several minutes
5626                # with every query - so decide by yourself how public you make this feature
5627                gitweb_check_feature('pickaxe')
5628                    or die_error(403, "Pickaxe is disabled");
5629        }
5630        if ($searchtype eq 'grep') {
5631                gitweb_check_feature('grep')
5632                    or die_error(403, "Grep is disabled");
5633        }
5634
5635        git_header_html();
5636
5637        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5638                my $greptype;
5639                if ($searchtype eq 'commit') {
5640                        $greptype = "--grep=";
5641                } elsif ($searchtype eq 'author') {
5642                        $greptype = "--author=";
5643                } elsif ($searchtype eq 'committer') {
5644                        $greptype = "--committer=";
5645                }
5646                $greptype .= $searchtext;
5647                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5648                                               $greptype, '--regexp-ignore-case',
5649                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5650
5651                my $paging_nav = '';
5652                if ($page > 0) {
5653                        $paging_nav .=
5654                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5655                                                       searchtext=>$searchtext,
5656                                                       searchtype=>$searchtype)},
5657                                        "first");
5658                        $paging_nav .= " &sdot; " .
5659                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5660                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5661                } else {
5662                        $paging_nav .= "first";
5663                        $paging_nav .= " &sdot; prev";
5664                }
5665                my $next_link = '';
5666                if ($#commitlist >= 100) {
5667                        $next_link =
5668                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5669                                         -accesskey => "n", -title => "Alt-n"}, "next");
5670                        $paging_nav .= " &sdot; $next_link";
5671                } else {
5672                        $paging_nav .= " &sdot; next";
5673                }
5674
5675                if ($#commitlist >= 100) {
5676                }
5677
5678                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5679                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5680                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5681        }
5682
5683        if ($searchtype eq 'pickaxe') {
5684                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5685                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5686
5687                print "<table class=\"pickaxe search\">\n";
5688                my $alternate = 1;
5689                $/ = "\n";
5690                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5691                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5692                        ($search_use_regexp ? '--pickaxe-regex' : ());
5693                undef %co;
5694                my @files;
5695                while (my $line = <$fd>) {
5696                        chomp $line;
5697                        next unless $line;
5698
5699                        my %set = parse_difftree_raw_line($line);
5700                        if (defined $set{'commit'}) {
5701                                # finish previous commit
5702                                if (%co) {
5703                                        print "</td>\n" .
5704                                              "<td class=\"link\">" .
5705                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5706                                              " | " .
5707                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5708                                        print "</td>\n" .
5709                                              "</tr>\n";
5710                                }
5711
5712                                if ($alternate) {
5713                                        print "<tr class=\"dark\">\n";
5714                                } else {
5715                                        print "<tr class=\"light\">\n";
5716                                }
5717                                $alternate ^= 1;
5718                                %co = parse_commit($set{'commit'});
5719                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5720                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5721                                      "<td><i>$author</i></td>\n" .
5722                                      "<td>" .
5723                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5724                                              -class => "list subject"},
5725                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5726                        } elsif (defined $set{'to_id'}) {
5727                                next if ($set{'to_id'} =~ m/^0{40}$/);
5728
5729                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5730                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5731                                              -class => "list"},
5732                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5733                                      "<br/>\n";
5734                        }
5735                }
5736                close $fd;
5737
5738                # finish last commit (warning: repetition!)
5739                if (%co) {
5740                        print "</td>\n" .
5741                              "<td class=\"link\">" .
5742                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5743                              " | " .
5744                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5745                        print "</td>\n" .
5746                              "</tr>\n";
5747                }
5748
5749                print "</table>\n";
5750        }
5751
5752        if ($searchtype eq 'grep') {
5753                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5754                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5755
5756                print "<table class=\"grep_search\">\n";
5757                my $alternate = 1;
5758                my $matches = 0;
5759                $/ = "\n";
5760                open my $fd, "-|", git_cmd(), 'grep', '-n',
5761                        $search_use_regexp ? ('-E', '-i') : '-F',
5762                        $searchtext, $co{'tree'};
5763                my $lastfile = '';
5764                while (my $line = <$fd>) {
5765                        chomp $line;
5766                        my ($file, $lno, $ltext, $binary);
5767                        last if ($matches++ > 1000);
5768                        if ($line =~ /^Binary file (.+) matches$/) {
5769                                $file = $1;
5770                                $binary = 1;
5771                        } else {
5772                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5773                        }
5774                        if ($file ne $lastfile) {
5775                                $lastfile and print "</td></tr>\n";
5776                                if ($alternate++) {
5777                                        print "<tr class=\"dark\">\n";
5778                                } else {
5779                                        print "<tr class=\"light\">\n";
5780                                }
5781                                print "<td class=\"list\">".
5782                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5783                                                               file_name=>"$file"),
5784                                                -class => "list"}, esc_path($file));
5785                                print "</td><td>\n";
5786                                $lastfile = $file;
5787                        }
5788                        if ($binary) {
5789                                print "<div class=\"binary\">Binary file</div>\n";
5790                        } else {
5791                                $ltext = untabify($ltext);
5792                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5793                                        $ltext = esc_html($1, -nbsp=>1);
5794                                        $ltext .= '<span class="match">';
5795                                        $ltext .= esc_html($2, -nbsp=>1);
5796                                        $ltext .= '</span>';
5797                                        $ltext .= esc_html($3, -nbsp=>1);
5798                                } else {
5799                                        $ltext = esc_html($ltext, -nbsp=>1);
5800                                }
5801                                print "<div class=\"pre\">" .
5802                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5803                                                               file_name=>"$file").'#l'.$lno,
5804                                                -class => "linenr"}, sprintf('%4i', $lno))
5805                                        . ' ' .  $ltext . "</div>\n";
5806                        }
5807                }
5808                if ($lastfile) {
5809                        print "</td></tr>\n";
5810                        if ($matches > 1000) {
5811                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5812                        }
5813                } else {
5814                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5815                }
5816                close $fd;
5817
5818                print "</table>\n";
5819        }
5820        git_footer_html();
5821}
5822
5823sub git_search_help {
5824        git_header_html();
5825        git_print_page_nav('','', $hash,$hash,$hash);
5826        print <<EOT;
5827<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5828regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5829the pattern entered is recognized as the POSIX extended
5830<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5831insensitive).</p>
5832<dl>
5833<dt><b>commit</b></dt>
5834<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5835EOT
5836        my ($have_grep) = gitweb_check_feature('grep');
5837        if ($have_grep) {
5838                print <<EOT;
5839<dt><b>grep</b></dt>
5840<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5841    a different one) are searched for the given pattern. On large trees, this search can take
5842a while and put some strain on the server, so please use it with some consideration. Note that
5843due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5844case-sensitive.</dd>
5845EOT
5846        }
5847        print <<EOT;
5848<dt><b>author</b></dt>
5849<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5850<dt><b>committer</b></dt>
5851<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5852EOT
5853        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5854        if ($have_pickaxe) {
5855                print <<EOT;
5856<dt><b>pickaxe</b></dt>
5857<dd>All commits that caused the string to appear or disappear from any file (changes that
5858added, removed or "modified" the string) will be listed. This search can take a while and
5859takes a lot of strain on the server, so please use it wisely. Note that since you may be
5860interested even in changes just changing the case as well, this search is case sensitive.</dd>
5861EOT
5862        }
5863        print "</dl>\n";
5864        git_footer_html();
5865}
5866
5867sub git_shortlog {
5868        my $head = git_get_head_hash($project);
5869        if (!defined $hash) {
5870                $hash = $head;
5871        }
5872        if (!defined $page) {
5873                $page = 0;
5874        }
5875        my $refs = git_get_references();
5876
5877        my $commit_hash = $hash;
5878        if (defined $hash_parent) {
5879                $commit_hash = "$hash_parent..$hash";
5880        }
5881        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5882
5883        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5884        my $next_link = '';
5885        if ($#commitlist >= 100) {
5886                $next_link =
5887                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5888                                 -accesskey => "n", -title => "Alt-n"}, "next");
5889        }
5890
5891        git_header_html();
5892        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5893        git_print_header_div('summary', $project);
5894
5895        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5896
5897        git_footer_html();
5898}
5899
5900## ......................................................................
5901## feeds (RSS, Atom; OPML)
5902
5903sub git_feed {
5904        my $format = shift || 'atom';
5905        my ($have_blame) = gitweb_check_feature('blame');
5906
5907        # Atom: http://www.atomenabled.org/developers/syndication/
5908        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5909        if ($format ne 'rss' && $format ne 'atom') {
5910                die_error(400, "Unknown web feed format");
5911        }
5912
5913        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5914        my $head = $hash || 'HEAD';
5915        my @commitlist = parse_commits($head, 150, 0, $file_name);
5916
5917        my %latest_commit;
5918        my %latest_date;
5919        my $content_type = "application/$format+xml";
5920        if (defined $cgi->http('HTTP_ACCEPT') &&
5921                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5922                # browser (feed reader) prefers text/xml
5923                $content_type = 'text/xml';
5924        }
5925        if (defined($commitlist[0])) {
5926                %latest_commit = %{$commitlist[0]};
5927                %latest_date   = parse_date($latest_commit{'author_epoch'});
5928                print $cgi->header(
5929                        -type => $content_type,
5930                        -charset => 'utf-8',
5931                        -last_modified => $latest_date{'rfc2822'});
5932        } else {
5933                print $cgi->header(
5934                        -type => $content_type,
5935                        -charset => 'utf-8');
5936        }
5937
5938        # Optimization: skip generating the body if client asks only
5939        # for Last-Modified date.
5940        return if ($cgi->request_method() eq 'HEAD');
5941
5942        # header variables
5943        my $title = "$site_name - $project/$action";
5944        my $feed_type = 'log';
5945        if (defined $hash) {
5946                $title .= " - '$hash'";
5947                $feed_type = 'branch log';
5948                if (defined $file_name) {
5949                        $title .= " :: $file_name";
5950                        $feed_type = 'history';
5951                }
5952        } elsif (defined $file_name) {
5953                $title .= " - $file_name";
5954                $feed_type = 'history';
5955        }
5956        $title .= " $feed_type";
5957        my $descr = git_get_project_description($project);
5958        if (defined $descr) {
5959                $descr = esc_html($descr);
5960        } else {
5961                $descr = "$project " .
5962                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5963                         " feed";
5964        }
5965        my $owner = git_get_project_owner($project);
5966        $owner = esc_html($owner);
5967
5968        #header
5969        my $alt_url;
5970        if (defined $file_name) {
5971                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5972        } elsif (defined $hash) {
5973                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5974        } else {
5975                $alt_url = href(-full=>1, action=>"summary");
5976        }
5977        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5978        if ($format eq 'rss') {
5979                print <<XML;
5980<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5981<channel>
5982XML
5983                print "<title>$title</title>\n" .
5984                      "<link>$alt_url</link>\n" .
5985                      "<description>$descr</description>\n" .
5986                      "<language>en</language>\n";
5987        } elsif ($format eq 'atom') {
5988                print <<XML;
5989<feed xmlns="http://www.w3.org/2005/Atom">
5990XML
5991                print "<title>$title</title>\n" .
5992                      "<subtitle>$descr</subtitle>\n" .
5993                      '<link rel="alternate" type="text/html" href="' .
5994                      $alt_url . '" />' . "\n" .
5995                      '<link rel="self" type="' . $content_type . '" href="' .
5996                      $cgi->self_url() . '" />' . "\n" .
5997                      "<id>" . href(-full=>1) . "</id>\n" .
5998                      # use project owner for feed author
5999                      "<author><name>$owner</name></author>\n";
6000                if (defined $favicon) {
6001                        print "<icon>" . esc_url($favicon) . "</icon>\n";
6002                }
6003                if (defined $logo_url) {
6004                        # not twice as wide as tall: 72 x 27 pixels
6005                        print "<logo>" . esc_url($logo) . "</logo>\n";
6006                }
6007                if (! %latest_date) {
6008                        # dummy date to keep the feed valid until commits trickle in:
6009                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
6010                } else {
6011                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
6012                }
6013        }
6014
6015        # contents
6016        for (my $i = 0; $i <= $#commitlist; $i++) {
6017                my %co = %{$commitlist[$i]};
6018                my $commit = $co{'id'};
6019                # we read 150, we always show 30 and the ones more recent than 48 hours
6020                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6021                        last;
6022                }
6023                my %cd = parse_date($co{'author_epoch'});
6024
6025                # get list of changed files
6026                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6027                        $co{'parent'} || "--root",
6028                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
6029                        or next;
6030                my @difftree = map { chomp; $_ } <$fd>;
6031                close $fd
6032                        or next;
6033
6034                # print element (entry, item)
6035                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6036                if ($format eq 'rss') {
6037                        print "<item>\n" .
6038                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
6039                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
6040                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6041                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6042                              "<link>$co_url</link>\n" .
6043                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
6044                              "<content:encoded>" .
6045                              "<![CDATA[\n";
6046                } elsif ($format eq 'atom') {
6047                        print "<entry>\n" .
6048                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6049                              "<updated>$cd{'iso-8601'}</updated>\n" .
6050                              "<author>\n" .
6051                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6052                        if ($co{'author_email'}) {
6053                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6054                        }
6055                        print "</author>\n" .
6056                              # use committer for contributor
6057                              "<contributor>\n" .
6058                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6059                        if ($co{'committer_email'}) {
6060                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6061                        }
6062                        print "</contributor>\n" .
6063                              "<published>$cd{'iso-8601'}</published>\n" .
6064                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6065                              "<id>$co_url</id>\n" .
6066                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6067                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6068                }
6069                my $comment = $co{'comment'};
6070                print "<pre>\n";
6071                foreach my $line (@$comment) {
6072                        $line = esc_html($line);
6073                        print "$line\n";
6074                }
6075                print "</pre><ul>\n";
6076                foreach my $difftree_line (@difftree) {
6077                        my %difftree = parse_difftree_raw_line($difftree_line);
6078                        next if !$difftree{'from_id'};
6079
6080                        my $file = $difftree{'file'} || $difftree{'to_file'};
6081
6082                        print "<li>" .
6083                              "[" .
6084                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6085                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6086                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6087                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
6088                                      -title => "diff"}, 'D');
6089                        if ($have_blame) {
6090                                print $cgi->a({-href => href(-full=>1, action=>"blame",
6091                                                             file_name=>$file, hash_base=>$commit),
6092                                              -title => "blame"}, 'B');
6093                        }
6094                        # if this is not a feed of a file history
6095                        if (!defined $file_name || $file_name ne $file) {
6096                                print $cgi->a({-href => href(-full=>1, action=>"history",
6097                                                             file_name=>$file, hash=>$commit),
6098                                              -title => "history"}, 'H');
6099                        }
6100                        $file = esc_path($file);
6101                        print "] ".
6102                              "$file</li>\n";
6103                }
6104                if ($format eq 'rss') {
6105                        print "</ul>]]>\n" .
6106                              "</content:encoded>\n" .
6107                              "</item>\n";
6108                } elsif ($format eq 'atom') {
6109                        print "</ul>\n</div>\n" .
6110                              "</content>\n" .
6111                              "</entry>\n";
6112                }
6113        }
6114
6115        # end of feed
6116        if ($format eq 'rss') {
6117                print "</channel>\n</rss>\n";
6118        }       elsif ($format eq 'atom') {
6119                print "</feed>\n";
6120        }
6121}
6122
6123sub git_rss {
6124        git_feed('rss');
6125}
6126
6127sub git_atom {
6128        git_feed('atom');
6129}
6130
6131sub git_opml {
6132        my @list = git_get_projects_list();
6133
6134        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6135        print <<XML;
6136<?xml version="1.0" encoding="utf-8"?>
6137<opml version="1.0">
6138<head>
6139  <title>$site_name OPML Export</title>
6140</head>
6141<body>
6142<outline text="git RSS feeds">
6143XML
6144
6145        foreach my $pr (@list) {
6146                my %proj = %$pr;
6147                my $head = git_get_head_hash($proj{'path'});
6148                if (!defined $head) {
6149                        next;
6150                }
6151                $git_dir = "$projectroot/$proj{'path'}";
6152                my %co = parse_commit($head);
6153                if (!%co) {
6154                        next;
6155                }
6156
6157                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6158                my $rss  = "$my_url?p=$proj{'path'};a=rss";
6159                my $html = "$my_url?p=$proj{'path'};a=summary";
6160                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6161        }
6162        print <<XML;
6163</outline>
6164</body>
6165</opml>
6166XML
6167}