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