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