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