9d1af7e55703c87168e949c285a97fbce867d3e5
   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        unless (opendir D, "$git_dir/ctags") {
1961                return $ctags;
1962        }
1963        foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
1964                open CT, $_ or next;
1965                my $val = <CT>;
1966                chomp $val;
1967                close CT;
1968                my $ctag = $_; $ctag =~ s#.*/##;
1969                $ctags->{$ctag} = $val;
1970        }
1971        closedir D;
1972        $ctags;
1973}
1974
1975sub git_populate_project_tagcloud {
1976        my $ctags = shift;
1977
1978        # First, merge different-cased tags; tags vote on casing
1979        my %ctags_lc;
1980        foreach (keys %$ctags) {
1981                $ctags_lc{lc $_}->{count} += $ctags->{$_};
1982                if (not $ctags_lc{lc $_}->{topcount}
1983                    or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1984                        $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1985                        $ctags_lc{lc $_}->{topname} = $_;
1986                }
1987        }
1988
1989        my $cloud;
1990        if (eval { require HTML::TagCloud; 1; }) {
1991                $cloud = HTML::TagCloud->new;
1992                foreach (sort keys %ctags_lc) {
1993                        # Pad the title with spaces so that the cloud looks
1994                        # less crammed.
1995                        my $title = $ctags_lc{$_}->{topname};
1996                        $title =~ s/ /&nbsp;/g;
1997                        $title =~ s/^/&nbsp;/g;
1998                        $title =~ s/$/&nbsp;/g;
1999                        $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2000                }
2001        } else {
2002                $cloud = \%ctags_lc;
2003        }
2004        $cloud;
2005}
2006
2007sub git_show_project_tagcloud {
2008        my ($cloud, $count) = @_;
2009        print STDERR ref($cloud)."..\n";
2010        if (ref $cloud eq 'HTML::TagCloud') {
2011                return $cloud->html_and_css($count);
2012        } else {
2013                my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2014                return '<p align="center">' . join (', ', map {
2015                        "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2016                } splice(@tags, 0, $count)) . '</p>';
2017        }
2018}
2019
2020sub git_get_project_url_list {
2021        my $path = shift;
2022
2023        $git_dir = "$projectroot/$path";
2024        open my $fd, "$git_dir/cloneurl"
2025                or return wantarray ?
2026                @{ config_to_multi(git_get_project_config('url')) } :
2027                   config_to_multi(git_get_project_config('url'));
2028        my @git_project_url_list = map { chomp; $_ } <$fd>;
2029        close $fd;
2030
2031        return wantarray ? @git_project_url_list : \@git_project_url_list;
2032}
2033
2034sub git_get_projects_list {
2035        my ($filter) = @_;
2036        my @list;
2037
2038        $filter ||= '';
2039        $filter =~ s/\.git$//;
2040
2041        my ($check_forks) = gitweb_check_feature('forks');
2042
2043        if (-d $projects_list) {
2044                # search in directory
2045                my $dir = $projects_list . ($filter ? "/$filter" : '');
2046                # remove the trailing "/"
2047                $dir =~ s!/+$!!;
2048                my $pfxlen = length("$dir");
2049                my $pfxdepth = ($dir =~ tr!/!!);
2050
2051                File::Find::find({
2052                        follow_fast => 1, # follow symbolic links
2053                        follow_skip => 2, # ignore duplicates
2054                        dangling_symlinks => 0, # ignore dangling symlinks, silently
2055                        wanted => sub {
2056                                # skip project-list toplevel, if we get it.
2057                                return if (m!^[/.]$!);
2058                                # only directories can be git repositories
2059                                return unless (-d $_);
2060                                # don't traverse too deep (Find is super slow on os x)
2061                                if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2062                                        $File::Find::prune = 1;
2063                                        return;
2064                                }
2065
2066                                my $subdir = substr($File::Find::name, $pfxlen + 1);
2067                                # we check related file in $projectroot
2068                                if (check_export_ok("$projectroot/$filter/$subdir")) {
2069                                        push @list, { path => ($filter ? "$filter/" : '') . $subdir };
2070                                        $File::Find::prune = 1;
2071                                }
2072                        },
2073                }, "$dir");
2074
2075        } elsif (-f $projects_list) {
2076                # read from file(url-encoded):
2077                # 'git%2Fgit.git Linus+Torvalds'
2078                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2079                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2080                my %paths;
2081                open my ($fd), $projects_list or return;
2082        PROJECT:
2083                while (my $line = <$fd>) {
2084                        chomp $line;
2085                        my ($path, $owner) = split ' ', $line;
2086                        $path = unescape($path);
2087                        $owner = unescape($owner);
2088                        if (!defined $path) {
2089                                next;
2090                        }
2091                        if ($filter ne '') {
2092                                # looking for forks;
2093                                my $pfx = substr($path, 0, length($filter));
2094                                if ($pfx ne $filter) {
2095                                        next PROJECT;
2096                                }
2097                                my $sfx = substr($path, length($filter));
2098                                if ($sfx !~ /^\/.*\.git$/) {
2099                                        next PROJECT;
2100                                }
2101                        } elsif ($check_forks) {
2102                        PATH:
2103                                foreach my $filter (keys %paths) {
2104                                        # looking for forks;
2105                                        my $pfx = substr($path, 0, length($filter));
2106                                        if ($pfx ne $filter) {
2107                                                next PATH;
2108                                        }
2109                                        my $sfx = substr($path, length($filter));
2110                                        if ($sfx !~ /^\/.*\.git$/) {
2111                                                next PATH;
2112                                        }
2113                                        # is a fork, don't include it in
2114                                        # the list
2115                                        next PROJECT;
2116                                }
2117                        }
2118                        if (check_export_ok("$projectroot/$path")) {
2119                                my $pr = {
2120                                        path => $path,
2121                                        owner => to_utf8($owner),
2122                                };
2123                                push @list, $pr;
2124                                (my $forks_path = $path) =~ s/\.git$//;
2125                                $paths{$forks_path}++;
2126                        }
2127                }
2128                close $fd;
2129        }
2130        return @list;
2131}
2132
2133our $gitweb_project_owner = undef;
2134sub git_get_project_list_from_file {
2135
2136        return if (defined $gitweb_project_owner);
2137
2138        $gitweb_project_owner = {};
2139        # read from file (url-encoded):
2140        # 'git%2Fgit.git Linus+Torvalds'
2141        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2142        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2143        if (-f $projects_list) {
2144                open (my $fd , $projects_list);
2145                while (my $line = <$fd>) {
2146                        chomp $line;
2147                        my ($pr, $ow) = split ' ', $line;
2148                        $pr = unescape($pr);
2149                        $ow = unescape($ow);
2150                        $gitweb_project_owner->{$pr} = to_utf8($ow);
2151                }
2152                close $fd;
2153        }
2154}
2155
2156sub git_get_project_owner {
2157        my $project = shift;
2158        my $owner;
2159
2160        return undef unless $project;
2161        $git_dir = "$projectroot/$project";
2162
2163        if (!defined $gitweb_project_owner) {
2164                git_get_project_list_from_file();
2165        }
2166
2167        if (exists $gitweb_project_owner->{$project}) {
2168                $owner = $gitweb_project_owner->{$project};
2169        }
2170        if (!defined $owner){
2171                $owner = git_get_project_config('owner');
2172        }
2173        if (!defined $owner) {
2174                $owner = get_file_owner("$git_dir");
2175        }
2176
2177        return $owner;
2178}
2179
2180sub git_get_last_activity {
2181        my ($path) = @_;
2182        my $fd;
2183
2184        $git_dir = "$projectroot/$path";
2185        open($fd, "-|", git_cmd(), 'for-each-ref',
2186             '--format=%(committer)',
2187             '--sort=-committerdate',
2188             '--count=1',
2189             'refs/heads') or return;
2190        my $most_recent = <$fd>;
2191        close $fd or return;
2192        if (defined $most_recent &&
2193            $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2194                my $timestamp = $1;
2195                my $age = time - $timestamp;
2196                return ($age, age_string($age));
2197        }
2198        return (undef, undef);
2199}
2200
2201sub git_get_references {
2202        my $type = shift || "";
2203        my %refs;
2204        # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2205        # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2206        open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2207                ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2208                or return;
2209
2210        while (my $line = <$fd>) {
2211                chomp $line;
2212                if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2213                        if (defined $refs{$1}) {
2214                                push @{$refs{$1}}, $2;
2215                        } else {
2216                                $refs{$1} = [ $2 ];
2217                        }
2218                }
2219        }
2220        close $fd or return;
2221        return \%refs;
2222}
2223
2224sub git_get_rev_name_tags {
2225        my $hash = shift || return undef;
2226
2227        open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2228                or return;
2229        my $name_rev = <$fd>;
2230        close $fd;
2231
2232        if ($name_rev =~ m|^$hash tags/(.*)$|) {
2233                return $1;
2234        } else {
2235                # catches also '$hash undefined' output
2236                return undef;
2237        }
2238}
2239
2240## ----------------------------------------------------------------------
2241## parse to hash functions
2242
2243sub parse_date {
2244        my $epoch = shift;
2245        my $tz = shift || "-0000";
2246
2247        my %date;
2248        my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2249        my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2250        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2251        $date{'hour'} = $hour;
2252        $date{'minute'} = $min;
2253        $date{'mday'} = $mday;
2254        $date{'day'} = $days[$wday];
2255        $date{'month'} = $months[$mon];
2256        $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2257                             $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2258        $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2259                             $mday, $months[$mon], $hour ,$min;
2260        $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2261                             1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2262
2263        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2264        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2265        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2266        $date{'hour_local'} = $hour;
2267        $date{'minute_local'} = $min;
2268        $date{'tz_local'} = $tz;
2269        $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2270                                  1900+$year, $mon+1, $mday,
2271                                  $hour, $min, $sec, $tz);
2272        return %date;
2273}
2274
2275sub parse_tag {
2276        my $tag_id = shift;
2277        my %tag;
2278        my @comment;
2279
2280        open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2281        $tag{'id'} = $tag_id;
2282        while (my $line = <$fd>) {
2283                chomp $line;
2284                if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2285                        $tag{'object'} = $1;
2286                } elsif ($line =~ m/^type (.+)$/) {
2287                        $tag{'type'} = $1;
2288                } elsif ($line =~ m/^tag (.+)$/) {
2289                        $tag{'name'} = $1;
2290                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2291                        $tag{'author'} = $1;
2292                        $tag{'epoch'} = $2;
2293                        $tag{'tz'} = $3;
2294                } elsif ($line =~ m/--BEGIN/) {
2295                        push @comment, $line;
2296                        last;
2297                } elsif ($line eq "") {
2298                        last;
2299                }
2300        }
2301        push @comment, <$fd>;
2302        $tag{'comment'} = \@comment;
2303        close $fd or return;
2304        if (!defined $tag{'name'}) {
2305                return
2306        };
2307        return %tag
2308}
2309
2310sub parse_commit_text {
2311        my ($commit_text, $withparents) = @_;
2312        my @commit_lines = split '\n', $commit_text;
2313        my %co;
2314
2315        pop @commit_lines; # Remove '\0'
2316
2317        if (! @commit_lines) {
2318                return;
2319        }
2320
2321        my $header = shift @commit_lines;
2322        if ($header !~ m/^[0-9a-fA-F]{40}/) {
2323                return;
2324        }
2325        ($co{'id'}, my @parents) = split ' ', $header;
2326        while (my $line = shift @commit_lines) {
2327                last if $line eq "\n";
2328                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2329                        $co{'tree'} = $1;
2330                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2331                        push @parents, $1;
2332                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2333                        $co{'author'} = $1;
2334                        $co{'author_epoch'} = $2;
2335                        $co{'author_tz'} = $3;
2336                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2337                                $co{'author_name'}  = $1;
2338                                $co{'author_email'} = $2;
2339                        } else {
2340                                $co{'author_name'} = $co{'author'};
2341                        }
2342                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2343                        $co{'committer'} = $1;
2344                        $co{'committer_epoch'} = $2;
2345                        $co{'committer_tz'} = $3;
2346                        $co{'committer_name'} = $co{'committer'};
2347                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2348                                $co{'committer_name'}  = $1;
2349                                $co{'committer_email'} = $2;
2350                        } else {
2351                                $co{'committer_name'} = $co{'committer'};
2352                        }
2353                }
2354        }
2355        if (!defined $co{'tree'}) {
2356                return;
2357        };
2358        $co{'parents'} = \@parents;
2359        $co{'parent'} = $parents[0];
2360
2361        foreach my $title (@commit_lines) {
2362                $title =~ s/^    //;
2363                if ($title ne "") {
2364                        $co{'title'} = chop_str($title, 80, 5);
2365                        # remove leading stuff of merges to make the interesting part visible
2366                        if (length($title) > 50) {
2367                                $title =~ s/^Automatic //;
2368                                $title =~ s/^merge (of|with) /Merge ... /i;
2369                                if (length($title) > 50) {
2370                                        $title =~ s/(http|rsync):\/\///;
2371                                }
2372                                if (length($title) > 50) {
2373                                        $title =~ s/(master|www|rsync)\.//;
2374                                }
2375                                if (length($title) > 50) {
2376                                        $title =~ s/kernel.org:?//;
2377                                }
2378                                if (length($title) > 50) {
2379                                        $title =~ s/\/pub\/scm//;
2380                                }
2381                        }
2382                        $co{'title_short'} = chop_str($title, 50, 5);
2383                        last;
2384                }
2385        }
2386        if (! defined $co{'title'} || $co{'title'} eq "") {
2387                $co{'title'} = $co{'title_short'} = '(no commit message)';
2388        }
2389        # remove added spaces
2390        foreach my $line (@commit_lines) {
2391                $line =~ s/^    //;
2392        }
2393        $co{'comment'} = \@commit_lines;
2394
2395        my $age = time - $co{'committer_epoch'};
2396        $co{'age'} = $age;
2397        $co{'age_string'} = age_string($age);
2398        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2399        if ($age > 60*60*24*7*2) {
2400                $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2401                $co{'age_string_age'} = $co{'age_string'};
2402        } else {
2403                $co{'age_string_date'} = $co{'age_string'};
2404                $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2405        }
2406        return %co;
2407}
2408
2409sub parse_commit {
2410        my ($commit_id) = @_;
2411        my %co;
2412
2413        local $/ = "\0";
2414
2415        open my $fd, "-|", git_cmd(), "rev-list",
2416                "--parents",
2417                "--header",
2418                "--max-count=1",
2419                $commit_id,
2420                "--",
2421                or die_error(500, "Open git-rev-list failed");
2422        %co = parse_commit_text(<$fd>, 1);
2423        close $fd;
2424
2425        return %co;
2426}
2427
2428sub parse_commits {
2429        my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2430        my @cos;
2431
2432        $maxcount ||= 1;
2433        $skip ||= 0;
2434
2435        local $/ = "\0";
2436
2437        open my $fd, "-|", git_cmd(), "rev-list",
2438                "--header",
2439                @args,
2440                ("--max-count=" . $maxcount),
2441                ("--skip=" . $skip),
2442                @extra_options,
2443                $commit_id,
2444                "--",
2445                ($filename ? ($filename) : ())
2446                or die_error(500, "Open git-rev-list failed");
2447        while (my $line = <$fd>) {
2448                my %co = parse_commit_text($line);
2449                push @cos, \%co;
2450        }
2451        close $fd;
2452
2453        return wantarray ? @cos : \@cos;
2454}
2455
2456# parse line of git-diff-tree "raw" output
2457sub parse_difftree_raw_line {
2458        my $line = shift;
2459        my %res;
2460
2461        # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2462        # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2463        if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2464                $res{'from_mode'} = $1;
2465                $res{'to_mode'} = $2;
2466                $res{'from_id'} = $3;
2467                $res{'to_id'} = $4;
2468                $res{'status'} = $5;
2469                $res{'similarity'} = $6;
2470                if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2471                        ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2472                } else {
2473                        $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2474                }
2475        }
2476        # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2477        # combined diff (for merge commit)
2478        elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2479                $res{'nparents'}  = length($1);
2480                $res{'from_mode'} = [ split(' ', $2) ];
2481                $res{'to_mode'} = pop @{$res{'from_mode'}};
2482                $res{'from_id'} = [ split(' ', $3) ];
2483                $res{'to_id'} = pop @{$res{'from_id'}};
2484                $res{'status'} = [ split('', $4) ];
2485                $res{'to_file'} = unquote($5);
2486        }
2487        # 'c512b523472485aef4fff9e57b229d9d243c967f'
2488        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2489                $res{'commit'} = $1;
2490        }
2491
2492        return wantarray ? %res : \%res;
2493}
2494
2495# wrapper: return parsed line of git-diff-tree "raw" output
2496# (the argument might be raw line, or parsed info)
2497sub parsed_difftree_line {
2498        my $line_or_ref = shift;
2499
2500        if (ref($line_or_ref) eq "HASH") {
2501                # pre-parsed (or generated by hand)
2502                return $line_or_ref;
2503        } else {
2504                return parse_difftree_raw_line($line_or_ref);
2505        }
2506}
2507
2508# parse line of git-ls-tree output
2509sub parse_ls_tree_line ($;%) {
2510        my $line = shift;
2511        my %opts = @_;
2512        my %res;
2513
2514        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2515        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2516
2517        $res{'mode'} = $1;
2518        $res{'type'} = $2;
2519        $res{'hash'} = $3;
2520        if ($opts{'-z'}) {
2521                $res{'name'} = $4;
2522        } else {
2523                $res{'name'} = unquote($4);
2524        }
2525
2526        return wantarray ? %res : \%res;
2527}
2528
2529# generates _two_ hashes, references to which are passed as 2 and 3 argument
2530sub parse_from_to_diffinfo {
2531        my ($diffinfo, $from, $to, @parents) = @_;
2532
2533        if ($diffinfo->{'nparents'}) {
2534                # combined diff
2535                $from->{'file'} = [];
2536                $from->{'href'} = [];
2537                fill_from_file_info($diffinfo, @parents)
2538                        unless exists $diffinfo->{'from_file'};
2539                for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2540                        $from->{'file'}[$i] =
2541                                defined $diffinfo->{'from_file'}[$i] ?
2542                                        $diffinfo->{'from_file'}[$i] :
2543                                        $diffinfo->{'to_file'};
2544                        if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2545                                $from->{'href'}[$i] = href(action=>"blob",
2546                                                           hash_base=>$parents[$i],
2547                                                           hash=>$diffinfo->{'from_id'}[$i],
2548                                                           file_name=>$from->{'file'}[$i]);
2549                        } else {
2550                                $from->{'href'}[$i] = undef;
2551                        }
2552                }
2553        } else {
2554                # ordinary (not combined) diff
2555                $from->{'file'} = $diffinfo->{'from_file'};
2556                if ($diffinfo->{'status'} ne "A") { # not new (added) file
2557                        $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2558                                               hash=>$diffinfo->{'from_id'},
2559                                               file_name=>$from->{'file'});
2560                } else {
2561                        delete $from->{'href'};
2562                }
2563        }
2564
2565        $to->{'file'} = $diffinfo->{'to_file'};
2566        if (!is_deleted($diffinfo)) { # file exists in result
2567                $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2568                                     hash=>$diffinfo->{'to_id'},
2569                                     file_name=>$to->{'file'});
2570        } else {
2571                delete $to->{'href'};
2572        }
2573}
2574
2575## ......................................................................
2576## parse to array of hashes functions
2577
2578sub git_get_heads_list {
2579        my $limit = shift;
2580        my @headslist;
2581
2582        open my $fd, '-|', git_cmd(), 'for-each-ref',
2583                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2584                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2585                'refs/heads'
2586                or return;
2587        while (my $line = <$fd>) {
2588                my %ref_item;
2589
2590                chomp $line;
2591                my ($refinfo, $committerinfo) = split(/\0/, $line);
2592                my ($hash, $name, $title) = split(' ', $refinfo, 3);
2593                my ($committer, $epoch, $tz) =
2594                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2595                $ref_item{'fullname'}  = $name;
2596                $name =~ s!^refs/heads/!!;
2597
2598                $ref_item{'name'}  = $name;
2599                $ref_item{'id'}    = $hash;
2600                $ref_item{'title'} = $title || '(no commit message)';
2601                $ref_item{'epoch'} = $epoch;
2602                if ($epoch) {
2603                        $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2604                } else {
2605                        $ref_item{'age'} = "unknown";
2606                }
2607
2608                push @headslist, \%ref_item;
2609        }
2610        close $fd;
2611
2612        return wantarray ? @headslist : \@headslist;
2613}
2614
2615sub git_get_tags_list {
2616        my $limit = shift;
2617        my @tagslist;
2618
2619        open my $fd, '-|', git_cmd(), 'for-each-ref',
2620                ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2621                '--format=%(objectname) %(objecttype) %(refname) '.
2622                '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2623                'refs/tags'
2624                or return;
2625        while (my $line = <$fd>) {
2626                my %ref_item;
2627
2628                chomp $line;
2629                my ($refinfo, $creatorinfo) = split(/\0/, $line);
2630                my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2631                my ($creator, $epoch, $tz) =
2632                        ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2633                $ref_item{'fullname'} = $name;
2634                $name =~ s!^refs/tags/!!;
2635
2636                $ref_item{'type'} = $type;
2637                $ref_item{'id'} = $id;
2638                $ref_item{'name'} = $name;
2639                if ($type eq "tag") {
2640                        $ref_item{'subject'} = $title;
2641                        $ref_item{'reftype'} = $reftype;
2642                        $ref_item{'refid'}   = $refid;
2643                } else {
2644                        $ref_item{'reftype'} = $type;
2645                        $ref_item{'refid'}   = $id;
2646                }
2647
2648                if ($type eq "tag" || $type eq "commit") {
2649                        $ref_item{'epoch'} = $epoch;
2650                        if ($epoch) {
2651                                $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2652                        } else {
2653                                $ref_item{'age'} = "unknown";
2654                        }
2655                }
2656
2657                push @tagslist, \%ref_item;
2658        }
2659        close $fd;
2660
2661        return wantarray ? @tagslist : \@tagslist;
2662}
2663
2664## ----------------------------------------------------------------------
2665## filesystem-related functions
2666
2667sub get_file_owner {
2668        my $path = shift;
2669
2670        my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2671        my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2672        if (!defined $gcos) {
2673                return undef;
2674        }
2675        my $owner = $gcos;
2676        $owner =~ s/[,;].*$//;
2677        return to_utf8($owner);
2678}
2679
2680## ......................................................................
2681## mimetype related functions
2682
2683sub mimetype_guess_file {
2684        my $filename = shift;
2685        my $mimemap = shift;
2686        -r $mimemap or return undef;
2687
2688        my %mimemap;
2689        open(MIME, $mimemap) or return undef;
2690        while (<MIME>) {
2691                next if m/^#/; # skip comments
2692                my ($mime, $exts) = split(/\t+/);
2693                if (defined $exts) {
2694                        my @exts = split(/\s+/, $exts);
2695                        foreach my $ext (@exts) {
2696                                $mimemap{$ext} = $mime;
2697                        }
2698                }
2699        }
2700        close(MIME);
2701
2702        $filename =~ /\.([^.]*)$/;
2703        return $mimemap{$1};
2704}
2705
2706sub mimetype_guess {
2707        my $filename = shift;
2708        my $mime;
2709        $filename =~ /\./ or return undef;
2710
2711        if ($mimetypes_file) {
2712                my $file = $mimetypes_file;
2713                if ($file !~ m!^/!) { # if it is relative path
2714                        # it is relative to project
2715                        $file = "$projectroot/$project/$file";
2716                }
2717                $mime = mimetype_guess_file($filename, $file);
2718        }
2719        $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2720        return $mime;
2721}
2722
2723sub blob_mimetype {
2724        my $fd = shift;
2725        my $filename = shift;
2726
2727        if ($filename) {
2728                my $mime = mimetype_guess($filename);
2729                $mime and return $mime;
2730        }
2731
2732        # just in case
2733        return $default_blob_plain_mimetype unless $fd;
2734
2735        if (-T $fd) {
2736                return 'text/plain';
2737        } elsif (! $filename) {
2738                return 'application/octet-stream';
2739        } elsif ($filename =~ m/\.png$/i) {
2740                return 'image/png';
2741        } elsif ($filename =~ m/\.gif$/i) {
2742                return 'image/gif';
2743        } elsif ($filename =~ m/\.jpe?g$/i) {
2744                return 'image/jpeg';
2745        } else {
2746                return 'application/octet-stream';
2747        }
2748}
2749
2750sub blob_contenttype {
2751        my ($fd, $file_name, $type) = @_;
2752
2753        $type ||= blob_mimetype($fd, $file_name);
2754        if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2755                $type .= "; charset=$default_text_plain_charset";
2756        }
2757
2758        return $type;
2759}
2760
2761## ======================================================================
2762## functions printing HTML: header, footer, error page
2763
2764sub git_header_html {
2765        my $status = shift || "200 OK";
2766        my $expires = shift;
2767
2768        my $title = "$site_name";
2769        if (defined $project) {
2770                $title .= " - " . to_utf8($project);
2771                if (defined $action) {
2772                        $title .= "/$action";
2773                        if (defined $file_name) {
2774                                $title .= " - " . esc_path($file_name);
2775                                if ($action eq "tree" && $file_name !~ m|/$|) {
2776                                        $title .= "/";
2777                                }
2778                        }
2779                }
2780        }
2781        my $content_type;
2782        # require explicit support from the UA if we are to send the page as
2783        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2784        # we have to do this because MSIE sometimes globs '*/*', pretending to
2785        # support xhtml+xml but choking when it gets what it asked for.
2786        if (defined $cgi->http('HTTP_ACCEPT') &&
2787            $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2788            $cgi->Accept('application/xhtml+xml') != 0) {
2789                $content_type = 'application/xhtml+xml';
2790        } else {
2791                $content_type = 'text/html';
2792        }
2793        print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2794                           -status=> $status, -expires => $expires);
2795        my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2796        print <<EOF;
2797<?xml version="1.0" encoding="utf-8"?>
2798<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2799<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2800<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2801<!-- git core binaries version $git_version -->
2802<head>
2803<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2804<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2805<meta name="robots" content="index, nofollow"/>
2806<title>$title</title>
2807EOF
2808# print out each stylesheet that exist
2809        if (defined $stylesheet) {
2810#provides backwards capability for those people who define style sheet in a config file
2811                print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2812        } else {
2813                foreach my $stylesheet (@stylesheets) {
2814                        next unless $stylesheet;
2815                        print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2816                }
2817        }
2818        if (defined $project) {
2819                my %href_params = get_feed_info();
2820                if (!exists $href_params{'-title'}) {
2821                        $href_params{'-title'} = 'log';
2822                }
2823
2824                foreach my $format qw(RSS Atom) {
2825                        my $type = lc($format);
2826                        my %link_attr = (
2827                                '-rel' => 'alternate',
2828                                '-title' => "$project - $href_params{'-title'} - $format feed",
2829                                '-type' => "application/$type+xml"
2830                        );
2831
2832                        $href_params{'action'} = $type;
2833                        $link_attr{'-href'} = href(%href_params);
2834                        print "<link ".
2835                              "rel=\"$link_attr{'-rel'}\" ".
2836                              "title=\"$link_attr{'-title'}\" ".
2837                              "href=\"$link_attr{'-href'}\" ".
2838                              "type=\"$link_attr{'-type'}\" ".
2839                              "/>\n";
2840
2841                        $href_params{'extra_options'} = '--no-merges';
2842                        $link_attr{'-href'} = href(%href_params);
2843                        $link_attr{'-title'} .= ' (no merges)';
2844                        print "<link ".
2845                              "rel=\"$link_attr{'-rel'}\" ".
2846                              "title=\"$link_attr{'-title'}\" ".
2847                              "href=\"$link_attr{'-href'}\" ".
2848                              "type=\"$link_attr{'-type'}\" ".
2849                              "/>\n";
2850                }
2851
2852        } else {
2853                printf('<link rel="alternate" title="%s projects list" '.
2854                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
2855                       $site_name, href(project=>undef, action=>"project_index"));
2856                printf('<link rel="alternate" title="%s projects feeds" '.
2857                       'href="%s" type="text/x-opml" />'."\n",
2858                       $site_name, href(project=>undef, action=>"opml"));
2859        }
2860        if (defined $favicon) {
2861                print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2862        }
2863
2864        print "</head>\n" .
2865              "<body>\n";
2866
2867        if (-f $site_header) {
2868                open (my $fd, $site_header);
2869                print <$fd>;
2870                close $fd;
2871        }
2872
2873        print "<div class=\"page_header\">\n" .
2874              $cgi->a({-href => esc_url($logo_url),
2875                       -title => $logo_label},
2876                      qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2877        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2878        if (defined $project) {
2879                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2880                if (defined $action) {
2881                        print " / $action";
2882                }
2883                print "\n";
2884        }
2885        print "</div>\n";
2886
2887        my ($have_search) = gitweb_check_feature('search');
2888        if (defined $project && $have_search) {
2889                if (!defined $searchtext) {
2890                        $searchtext = "";
2891                }
2892                my $search_hash;
2893                if (defined $hash_base) {
2894                        $search_hash = $hash_base;
2895                } elsif (defined $hash) {
2896                        $search_hash = $hash;
2897                } else {
2898                        $search_hash = "HEAD";
2899                }
2900                my $action = $my_uri;
2901                my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2902                if ($use_pathinfo) {
2903                        $action .= "/".esc_url($project);
2904                }
2905                print $cgi->startform(-method => "get", -action => $action) .
2906                      "<div class=\"search\">\n" .
2907                      (!$use_pathinfo &&
2908                      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2909                      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2910                      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2911                      $cgi->popup_menu(-name => 'st', -default => 'commit',
2912                                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2913                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2914                      " search:\n",
2915                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2916                      "<span title=\"Extended regular expression\">" .
2917                      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2918                                     -checked => $search_use_regexp) .
2919                      "</span>" .
2920                      "</div>" .
2921                      $cgi->end_form() . "\n";
2922        }
2923}
2924
2925sub git_footer_html {
2926        my $feed_class = 'rss_logo';
2927
2928        print "<div class=\"page_footer\">\n";
2929        if (defined $project) {
2930                my $descr = git_get_project_description($project);
2931                if (defined $descr) {
2932                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2933                }
2934
2935                my %href_params = get_feed_info();
2936                if (!%href_params) {
2937                        $feed_class .= ' generic';
2938                }
2939                $href_params{'-title'} ||= 'log';
2940
2941                foreach my $format qw(RSS Atom) {
2942                        $href_params{'action'} = lc($format);
2943                        print $cgi->a({-href => href(%href_params),
2944                                      -title => "$href_params{'-title'} $format feed",
2945                                      -class => $feed_class}, $format)."\n";
2946                }
2947
2948        } else {
2949                print $cgi->a({-href => href(project=>undef, action=>"opml"),
2950                              -class => $feed_class}, "OPML") . " ";
2951                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2952                              -class => $feed_class}, "TXT") . "\n";
2953        }
2954        print "</div>\n"; # class="page_footer"
2955
2956        if (-f $site_footer) {
2957                open (my $fd, $site_footer);
2958                print <$fd>;
2959                close $fd;
2960        }
2961
2962        print "</body>\n" .
2963              "</html>";
2964}
2965
2966# die_error(<http_status_code>, <error_message>)
2967# Example: die_error(404, 'Hash not found')
2968# By convention, use the following status codes (as defined in RFC 2616):
2969# 400: Invalid or missing CGI parameters, or
2970#      requested object exists but has wrong type.
2971# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2972#      this server or project.
2973# 404: Requested object/revision/project doesn't exist.
2974# 500: The server isn't configured properly, or
2975#      an internal error occurred (e.g. failed assertions caused by bugs), or
2976#      an unknown error occurred (e.g. the git binary died unexpectedly).
2977sub die_error {
2978        my $status = shift || 500;
2979        my $error = shift || "Internal server error";
2980
2981        my %http_responses = (400 => '400 Bad Request',
2982                              403 => '403 Forbidden',
2983                              404 => '404 Not Found',
2984                              500 => '500 Internal Server Error');
2985        git_header_html($http_responses{$status});
2986        print <<EOF;
2987<div class="page_body">
2988<br /><br />
2989$status - $error
2990<br />
2991</div>
2992EOF
2993        git_footer_html();
2994        exit;
2995}
2996
2997## ----------------------------------------------------------------------
2998## functions printing or outputting HTML: navigation
2999
3000sub git_print_page_nav {
3001        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3002        $extra = '' if !defined $extra; # pager or formats
3003
3004        my @navs = qw(summary shortlog log commit commitdiff tree);
3005        if ($suppress) {
3006                @navs = grep { $_ ne $suppress } @navs;
3007        }
3008
3009        my %arg = map { $_ => {action=>$_} } @navs;
3010        if (defined $head) {
3011                for (qw(commit commitdiff)) {
3012                        $arg{$_}{'hash'} = $head;
3013                }
3014                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3015                        for (qw(shortlog log)) {
3016                                $arg{$_}{'hash'} = $head;
3017                        }
3018                }
3019        }
3020
3021        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3022        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3023
3024        my @actions = gitweb_check_feature('actions');
3025        while (@actions) {
3026                my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
3027                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3028                # munch munch
3029                $link =~ s#%n#$project#g;
3030                $link =~ s#%f#$git_dir#g;
3031                $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
3032                $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
3033                $arg{$label}{'_href'} = $link;
3034        }
3035
3036        print "<div class=\"page_nav\">\n" .
3037                (join " | ",
3038                 map { $_ eq $current ?
3039                       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3040                 } @navs);
3041        print "<br/>\n$extra<br/>\n" .
3042              "</div>\n";
3043}
3044
3045sub format_paging_nav {
3046        my ($action, $hash, $head, $page, $has_next_link) = @_;
3047        my $paging_nav;
3048
3049
3050        if ($hash ne $head || $page) {
3051                $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3052        } else {
3053                $paging_nav .= "HEAD";
3054        }
3055
3056        if ($page > 0) {
3057                $paging_nav .= " &sdot; " .
3058                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
3059                                 -accesskey => "p", -title => "Alt-p"}, "prev");
3060        } else {
3061                $paging_nav .= " &sdot; prev";
3062        }
3063
3064        if ($has_next_link) {
3065                $paging_nav .= " &sdot; " .
3066                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
3067                                 -accesskey => "n", -title => "Alt-n"}, "next");
3068        } else {
3069                $paging_nav .= " &sdot; next";
3070        }
3071
3072        return $paging_nav;
3073}
3074
3075## ......................................................................
3076## functions printing or outputting HTML: div
3077
3078sub git_print_header_div {
3079        my ($action, $title, $hash, $hash_base) = @_;
3080        my %args = ();
3081
3082        $args{'action'} = $action;
3083        $args{'hash'} = $hash if $hash;
3084        $args{'hash_base'} = $hash_base if $hash_base;
3085
3086        print "<div class=\"header\">\n" .
3087              $cgi->a({-href => href(%args), -class => "title"},
3088              $title ? $title : $action) .
3089              "\n</div>\n";
3090}
3091
3092#sub git_print_authorship (\%) {
3093sub git_print_authorship {
3094        my $co = shift;
3095
3096        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3097        print "<div class=\"author_date\">" .
3098              esc_html($co->{'author_name'}) .
3099              " [$ad{'rfc2822'}";
3100        if ($ad{'hour_local'} < 6) {
3101                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3102                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3103        } else {
3104                printf(" (%02d:%02d %s)",
3105                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3106        }
3107        print "]</div>\n";
3108}
3109
3110sub git_print_page_path {
3111        my $name = shift;
3112        my $type = shift;
3113        my $hb = shift;
3114
3115
3116        print "<div class=\"page_path\">";
3117        print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3118                      -title => 'tree root'}, to_utf8("[$project]"));
3119        print " / ";
3120        if (defined $name) {
3121                my @dirname = split '/', $name;
3122                my $basename = pop @dirname;
3123                my $fullname = '';
3124
3125                foreach my $dir (@dirname) {
3126                        $fullname .= ($fullname ? '/' : '') . $dir;
3127                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3128                                                     hash_base=>$hb),
3129                                      -title => $fullname}, esc_path($dir));
3130                        print " / ";
3131                }
3132                if (defined $type && $type eq 'blob') {
3133                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3134                                                     hash_base=>$hb),
3135                                      -title => $name}, esc_path($basename));
3136                } elsif (defined $type && $type eq 'tree') {
3137                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3138                                                     hash_base=>$hb),
3139                                      -title => $name}, esc_path($basename));
3140                        print " / ";
3141                } else {
3142                        print esc_path($basename);
3143                }
3144        }
3145        print "<br/></div>\n";
3146}
3147
3148# sub git_print_log (\@;%) {
3149sub git_print_log ($;%) {
3150        my $log = shift;
3151        my %opts = @_;
3152
3153        if ($opts{'-remove_title'}) {
3154                # remove title, i.e. first line of log
3155                shift @$log;
3156        }
3157        # remove leading empty lines
3158        while (defined $log->[0] && $log->[0] eq "") {
3159                shift @$log;
3160        }
3161
3162        # print log
3163        my $signoff = 0;
3164        my $empty = 0;
3165        foreach my $line (@$log) {
3166                if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3167                        $signoff = 1;
3168                        $empty = 0;
3169                        if (! $opts{'-remove_signoff'}) {
3170                                print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3171                                next;
3172                        } else {
3173                                # remove signoff lines
3174                                next;
3175                        }
3176                } else {
3177                        $signoff = 0;
3178                }
3179
3180                # print only one empty line
3181                # do not print empty line after signoff
3182                if ($line eq "") {
3183                        next if ($empty || $signoff);
3184                        $empty = 1;
3185                } else {
3186                        $empty = 0;
3187                }
3188
3189                print format_log_line_html($line) . "<br/>\n";
3190        }
3191
3192        if ($opts{'-final_empty_line'}) {
3193                # end with single empty line
3194                print "<br/>\n" unless $empty;
3195        }
3196}
3197
3198# return link target (what link points to)
3199sub git_get_link_target {
3200        my $hash = shift;
3201        my $link_target;
3202
3203        # read link
3204        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3205                or return;
3206        {
3207                local $/;
3208                $link_target = <$fd>;
3209        }
3210        close $fd
3211                or return;
3212
3213        return $link_target;
3214}
3215
3216# given link target, and the directory (basedir) the link is in,
3217# return target of link relative to top directory (top tree);
3218# return undef if it is not possible (including absolute links).
3219sub normalize_link_target {
3220        my ($link_target, $basedir, $hash_base) = @_;
3221
3222        # we can normalize symlink target only if $hash_base is provided
3223        return unless $hash_base;
3224
3225        # absolute symlinks (beginning with '/') cannot be normalized
3226        return if (substr($link_target, 0, 1) eq '/');
3227
3228        # normalize link target to path from top (root) tree (dir)
3229        my $path;
3230        if ($basedir) {
3231                $path = $basedir . '/' . $link_target;
3232        } else {
3233                # we are in top (root) tree (dir)
3234                $path = $link_target;
3235        }
3236
3237        # remove //, /./, and /../
3238        my @path_parts;
3239        foreach my $part (split('/', $path)) {
3240                # discard '.' and ''
3241                next if (!$part || $part eq '.');
3242                # handle '..'
3243                if ($part eq '..') {
3244                        if (@path_parts) {
3245                                pop @path_parts;
3246                        } else {
3247                                # link leads outside repository (outside top dir)
3248                                return;
3249                        }
3250                } else {
3251                        push @path_parts, $part;
3252                }
3253        }
3254        $path = join('/', @path_parts);
3255
3256        return $path;
3257}
3258
3259# print tree entry (row of git_tree), but without encompassing <tr> element
3260sub git_print_tree_entry {
3261        my ($t, $basedir, $hash_base, $have_blame) = @_;
3262
3263        my %base_key = ();
3264        $base_key{'hash_base'} = $hash_base if defined $hash_base;
3265
3266        # The format of a table row is: mode list link.  Where mode is
3267        # the mode of the entry, list is the name of the entry, an href,
3268        # and link is the action links of the entry.
3269
3270        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3271        if ($t->{'type'} eq "blob") {
3272                print "<td class=\"list\">" .
3273                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3274                                               file_name=>"$basedir$t->{'name'}", %base_key),
3275                                -class => "list"}, esc_path($t->{'name'}));
3276                if (S_ISLNK(oct $t->{'mode'})) {
3277                        my $link_target = git_get_link_target($t->{'hash'});
3278                        if ($link_target) {
3279                                my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3280                                if (defined $norm_target) {
3281                                        print " -> " .
3282                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3283                                                                     file_name=>$norm_target),
3284                                                       -title => $norm_target}, esc_path($link_target));
3285                                } else {
3286                                        print " -> " . esc_path($link_target);
3287                                }
3288                        }
3289                }
3290                print "</td>\n";
3291                print "<td class=\"link\">";
3292                print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3293                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3294                              "blob");
3295                if ($have_blame) {
3296                        print " | " .
3297                              $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3298                                                     file_name=>"$basedir$t->{'name'}", %base_key)},
3299                                      "blame");
3300                }
3301                if (defined $hash_base) {
3302                        print " | " .
3303                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3304                                                     hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3305                                      "history");
3306                }
3307                print " | " .
3308                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3309                                               file_name=>"$basedir$t->{'name'}")},
3310                                "raw");
3311                print "</td>\n";
3312
3313        } elsif ($t->{'type'} eq "tree") {
3314                print "<td class=\"list\">";
3315                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3316                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3317                              esc_path($t->{'name'}));
3318                print "</td>\n";
3319                print "<td class=\"link\">";
3320                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3321                                             file_name=>"$basedir$t->{'name'}", %base_key)},
3322                              "tree");
3323                if (defined $hash_base) {
3324                        print " | " .
3325                              $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3326                                                     file_name=>"$basedir$t->{'name'}")},
3327                                      "history");
3328                }
3329                print "</td>\n";
3330        } else {
3331                # unknown object: we can only present history for it
3332                # (this includes 'commit' object, i.e. submodule support)
3333                print "<td class=\"list\">" .
3334                      esc_path($t->{'name'}) .
3335                      "</td>\n";
3336                print "<td class=\"link\">";
3337                if (defined $hash_base) {
3338                        print $cgi->a({-href => href(action=>"history",
3339                                                     hash_base=>$hash_base,
3340                                                     file_name=>"$basedir$t->{'name'}")},
3341                                      "history");
3342                }
3343                print "</td>\n";
3344        }
3345}
3346
3347## ......................................................................
3348## functions printing large fragments of HTML
3349
3350# get pre-image filenames for merge (combined) diff
3351sub fill_from_file_info {
3352        my ($diff, @parents) = @_;
3353
3354        $diff->{'from_file'} = [ ];
3355        $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3356        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3357                if ($diff->{'status'}[$i] eq 'R' ||
3358                    $diff->{'status'}[$i] eq 'C') {
3359                        $diff->{'from_file'}[$i] =
3360                                git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3361                }
3362        }
3363
3364        return $diff;
3365}
3366
3367# is current raw difftree line of file deletion
3368sub is_deleted {
3369        my $diffinfo = shift;
3370
3371        return $diffinfo->{'to_id'} eq ('0' x 40);
3372}
3373
3374# does patch correspond to [previous] difftree raw line
3375# $diffinfo  - hashref of parsed raw diff format
3376# $patchinfo - hashref of parsed patch diff format
3377#              (the same keys as in $diffinfo)
3378sub is_patch_split {
3379        my ($diffinfo, $patchinfo) = @_;
3380
3381        return defined $diffinfo && defined $patchinfo
3382                && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3383}
3384
3385
3386sub git_difftree_body {
3387        my ($difftree, $hash, @parents) = @_;
3388        my ($parent) = $parents[0];
3389        my ($have_blame) = gitweb_check_feature('blame');
3390        print "<div class=\"list_head\">\n";
3391        if ($#{$difftree} > 10) {
3392                print(($#{$difftree} + 1) . " files changed:\n");
3393        }
3394        print "</div>\n";
3395
3396        print "<table class=\"" .
3397              (@parents > 1 ? "combined " : "") .
3398              "diff_tree\">\n";
3399
3400        # header only for combined diff in 'commitdiff' view
3401        my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3402        if ($has_header) {
3403                # table header
3404                print "<thead><tr>\n" .
3405                       "<th></th><th></th>\n"; # filename, patchN link
3406                for (my $i = 0; $i < @parents; $i++) {
3407                        my $par = $parents[$i];
3408                        print "<th>" .
3409                              $cgi->a({-href => href(action=>"commitdiff",
3410                                                     hash=>$hash, hash_parent=>$par),
3411                                       -title => 'commitdiff to parent number ' .
3412                                                  ($i+1) . ': ' . substr($par,0,7)},
3413                                      $i+1) .
3414                              "&nbsp;</th>\n";
3415                }
3416                print "</tr></thead>\n<tbody>\n";
3417        }
3418
3419        my $alternate = 1;
3420        my $patchno = 0;
3421        foreach my $line (@{$difftree}) {
3422                my $diff = parsed_difftree_line($line);
3423
3424                if ($alternate) {
3425                        print "<tr class=\"dark\">\n";
3426                } else {
3427                        print "<tr class=\"light\">\n";
3428                }
3429                $alternate ^= 1;
3430
3431                if (exists $diff->{'nparents'}) { # combined diff
3432
3433                        fill_from_file_info($diff, @parents)
3434                                unless exists $diff->{'from_file'};
3435
3436                        if (!is_deleted($diff)) {
3437                                # file exists in the result (child) commit
3438                                print "<td>" .
3439                                      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3440                                                             file_name=>$diff->{'to_file'},
3441                                                             hash_base=>$hash),
3442                                              -class => "list"}, esc_path($diff->{'to_file'})) .
3443                                      "</td>\n";
3444                        } else {
3445                                print "<td>" .
3446                                      esc_path($diff->{'to_file'}) .
3447                                      "</td>\n";
3448                        }
3449
3450                        if ($action eq 'commitdiff') {
3451                                # link to patch
3452                                $patchno++;
3453                                print "<td class=\"link\">" .
3454                                      $cgi->a({-href => "#patch$patchno"}, "patch") .
3455                                      " | " .
3456                                      "</td>\n";
3457                        }
3458
3459                        my $has_history = 0;
3460                        my $not_deleted = 0;
3461                        for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3462                                my $hash_parent = $parents[$i];
3463                                my $from_hash = $diff->{'from_id'}[$i];
3464                                my $from_path = $diff->{'from_file'}[$i];
3465                                my $status = $diff->{'status'}[$i];
3466
3467                                $has_history ||= ($status ne 'A');
3468                                $not_deleted ||= ($status ne 'D');
3469
3470                                if ($status eq 'A') {
3471                                        print "<td  class=\"link\" align=\"right\"> | </td>\n";
3472                                } elsif ($status eq 'D') {
3473                                        print "<td class=\"link\">" .
3474                                              $cgi->a({-href => href(action=>"blob",
3475                                                                     hash_base=>$hash,
3476                                                                     hash=>$from_hash,
3477                                                                     file_name=>$from_path)},
3478                                                      "blob" . ($i+1)) .
3479                                              " | </td>\n";
3480                                } else {
3481                                        if ($diff->{'to_id'} eq $from_hash) {
3482                                                print "<td class=\"link nochange\">";
3483                                        } else {
3484                                                print "<td class=\"link\">";
3485                                        }
3486                                        print $cgi->a({-href => href(action=>"blobdiff",
3487                                                                     hash=>$diff->{'to_id'},
3488                                                                     hash_parent=>$from_hash,
3489                                                                     hash_base=>$hash,
3490                                                                     hash_parent_base=>$hash_parent,
3491                                                                     file_name=>$diff->{'to_file'},
3492                                                                     file_parent=>$from_path)},
3493                                                      "diff" . ($i+1)) .
3494                                              " | </td>\n";
3495                                }
3496                        }
3497
3498                        print "<td class=\"link\">";
3499                        if ($not_deleted) {
3500                                print $cgi->a({-href => href(action=>"blob",
3501                                                             hash=>$diff->{'to_id'},
3502                                                             file_name=>$diff->{'to_file'},
3503                                                             hash_base=>$hash)},
3504                                              "blob");
3505                                print " | " if ($has_history);
3506                        }
3507                        if ($has_history) {
3508                                print $cgi->a({-href => href(action=>"history",
3509                                                             file_name=>$diff->{'to_file'},
3510                                                             hash_base=>$hash)},
3511                                              "history");
3512                        }
3513                        print "</td>\n";
3514
3515                        print "</tr>\n";
3516                        next; # instead of 'else' clause, to avoid extra indent
3517                }
3518                # else ordinary diff
3519
3520                my ($to_mode_oct, $to_mode_str, $to_file_type);
3521                my ($from_mode_oct, $from_mode_str, $from_file_type);
3522                if ($diff->{'to_mode'} ne ('0' x 6)) {
3523                        $to_mode_oct = oct $diff->{'to_mode'};
3524                        if (S_ISREG($to_mode_oct)) { # only for regular file
3525                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3526                        }
3527                        $to_file_type = file_type($diff->{'to_mode'});
3528                }
3529                if ($diff->{'from_mode'} ne ('0' x 6)) {
3530                        $from_mode_oct = oct $diff->{'from_mode'};
3531                        if (S_ISREG($to_mode_oct)) { # only for regular file
3532                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3533                        }
3534                        $from_file_type = file_type($diff->{'from_mode'});
3535                }
3536
3537                if ($diff->{'status'} eq "A") { # created
3538                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3539                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3540                        $mode_chng   .= "]</span>";
3541                        print "<td>";
3542                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3543                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3544                                      -class => "list"}, esc_path($diff->{'file'}));
3545                        print "</td>\n";
3546                        print "<td>$mode_chng</td>\n";
3547                        print "<td class=\"link\">";
3548                        if ($action eq 'commitdiff') {
3549                                # link to patch
3550                                $patchno++;
3551                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3552                                print " | ";
3553                        }
3554                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3555                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3556                                      "blob");
3557                        print "</td>\n";
3558
3559                } elsif ($diff->{'status'} eq "D") { # deleted
3560                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3561                        print "<td>";
3562                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3563                                                     hash_base=>$parent, file_name=>$diff->{'file'}),
3564                                       -class => "list"}, esc_path($diff->{'file'}));
3565                        print "</td>\n";
3566                        print "<td>$mode_chng</td>\n";
3567                        print "<td class=\"link\">";
3568                        if ($action eq 'commitdiff') {
3569                                # link to patch
3570                                $patchno++;
3571                                print $cgi->a({-href => "#patch$patchno"}, "patch");
3572                                print " | ";
3573                        }
3574                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3575                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
3576                                      "blob") . " | ";
3577                        if ($have_blame) {
3578                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3579                                                             file_name=>$diff->{'file'})},
3580                                              "blame") . " | ";
3581                        }
3582                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3583                                                     file_name=>$diff->{'file'})},
3584                                      "history");
3585                        print "</td>\n";
3586
3587                } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3588                        my $mode_chnge = "";
3589                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3590                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3591                                if ($from_file_type ne $to_file_type) {
3592                                        $mode_chnge .= " from $from_file_type to $to_file_type";
3593                                }
3594                                if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3595                                        if ($from_mode_str && $to_mode_str) {
3596                                                $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3597                                        } elsif ($to_mode_str) {
3598                                                $mode_chnge .= " mode: $to_mode_str";
3599                                        }
3600                                }
3601                                $mode_chnge .= "]</span>\n";
3602                        }
3603                        print "<td>";
3604                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3605                                                     hash_base=>$hash, file_name=>$diff->{'file'}),
3606                                      -class => "list"}, esc_path($diff->{'file'}));
3607                        print "</td>\n";
3608                        print "<td>$mode_chnge</td>\n";
3609                        print "<td class=\"link\">";
3610                        if ($action eq 'commitdiff') {
3611                                # link to patch
3612                                $patchno++;
3613                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3614                                      " | ";
3615                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3616                                # "commit" view and modified file (not onlu mode changed)
3617                                print $cgi->a({-href => href(action=>"blobdiff",
3618                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3619                                                             hash_base=>$hash, hash_parent_base=>$parent,
3620                                                             file_name=>$diff->{'file'})},
3621                                              "diff") .
3622                                      " | ";
3623                        }
3624                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3625                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
3626                                       "blob") . " | ";
3627                        if ($have_blame) {
3628                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3629                                                             file_name=>$diff->{'file'})},
3630                                              "blame") . " | ";
3631                        }
3632                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3633                                                     file_name=>$diff->{'file'})},
3634                                      "history");
3635                        print "</td>\n";
3636
3637                } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3638                        my %status_name = ('R' => 'moved', 'C' => 'copied');
3639                        my $nstatus = $status_name{$diff->{'status'}};
3640                        my $mode_chng = "";
3641                        if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3642                                # mode also for directories, so we cannot use $to_mode_str
3643                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3644                        }
3645                        print "<td>" .
3646                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3647                                                     hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3648                                      -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3649                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3650                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3651                                                     hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3652                                      -class => "list"}, esc_path($diff->{'from_file'})) .
3653                              " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3654                              "<td class=\"link\">";
3655                        if ($action eq 'commitdiff') {
3656                                # link to patch
3657                                $patchno++;
3658                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
3659                                      " | ";
3660                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3661                                # "commit" view and modified file (not only pure rename or copy)
3662                                print $cgi->a({-href => href(action=>"blobdiff",
3663                                                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3664                                                             hash_base=>$hash, hash_parent_base=>$parent,
3665                                                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3666                                              "diff") .
3667                                      " | ";
3668                        }
3669                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3670                                                     hash_base=>$parent, file_name=>$diff->{'to_file'})},
3671                                      "blob") . " | ";
3672                        if ($have_blame) {
3673                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3674                                                             file_name=>$diff->{'to_file'})},
3675                                              "blame") . " | ";
3676                        }
3677                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3678                                                    file_name=>$diff->{'to_file'})},
3679                                      "history");
3680                        print "</td>\n";
3681
3682                } # we should not encounter Unmerged (U) or Unknown (X) status
3683                print "</tr>\n";
3684        }
3685        print "</tbody>" if $has_header;
3686        print "</table>\n";
3687}
3688
3689sub git_patchset_body {
3690        my ($fd, $difftree, $hash, @hash_parents) = @_;
3691        my ($hash_parent) = $hash_parents[0];
3692
3693        my $is_combined = (@hash_parents > 1);
3694        my $patch_idx = 0;
3695        my $patch_number = 0;
3696        my $patch_line;
3697        my $diffinfo;
3698        my $to_name;
3699        my (%from, %to);
3700
3701        print "<div class=\"patchset\">\n";
3702
3703        # skip to first patch
3704        while ($patch_line = <$fd>) {
3705                chomp $patch_line;
3706
3707                last if ($patch_line =~ m/^diff /);
3708        }
3709
3710 PATCH:
3711        while ($patch_line) {
3712
3713                # parse "git diff" header line
3714                if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3715                        # $1 is from_name, which we do not use
3716                        $to_name = unquote($2);
3717                        $to_name =~ s!^b/!!;
3718                } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3719                        # $1 is 'cc' or 'combined', which we do not use
3720                        $to_name = unquote($2);
3721                } else {
3722                        $to_name = undef;
3723                }
3724
3725                # check if current patch belong to current raw line
3726                # and parse raw git-diff line if needed
3727                if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3728                        # this is continuation of a split patch
3729                        print "<div class=\"patch cont\">\n";
3730                } else {
3731                        # advance raw git-diff output if needed
3732                        $patch_idx++ if defined $diffinfo;
3733
3734                        # read and prepare patch information
3735                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3736
3737                        # compact combined diff output can have some patches skipped
3738                        # find which patch (using pathname of result) we are at now;
3739                        if ($is_combined) {
3740                                while ($to_name ne $diffinfo->{'to_file'}) {
3741                                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3742                                              format_diff_cc_simplified($diffinfo, @hash_parents) .
3743                                              "</div>\n";  # class="patch"
3744
3745                                        $patch_idx++;
3746                                        $patch_number++;
3747
3748                                        last if $patch_idx > $#$difftree;
3749                                        $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3750                                }
3751                        }
3752
3753                        # modifies %from, %to hashes
3754                        parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3755
3756                        # this is first patch for raw difftree line with $patch_idx index
3757                        # we index @$difftree array from 0, but number patches from 1
3758                        print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3759                }
3760
3761                # git diff header
3762                #assert($patch_line =~ m/^diff /) if DEBUG;
3763                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3764                $patch_number++;
3765                # print "git diff" header
3766                print format_git_diff_header_line($patch_line, $diffinfo,
3767                                                  \%from, \%to);
3768
3769                # print extended diff header
3770                print "<div class=\"diff extended_header\">\n";
3771        EXTENDED_HEADER:
3772                while ($patch_line = <$fd>) {
3773                        chomp $patch_line;
3774
3775                        last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3776
3777                        print format_extended_diff_header_line($patch_line, $diffinfo,
3778                                                               \%from, \%to);
3779                }
3780                print "</div>\n"; # class="diff extended_header"
3781
3782                # from-file/to-file diff header
3783                if (! $patch_line) {
3784                        print "</div>\n"; # class="patch"
3785                        last PATCH;
3786                }
3787                next PATCH if ($patch_line =~ m/^diff /);
3788                #assert($patch_line =~ m/^---/) if DEBUG;
3789
3790                my $last_patch_line = $patch_line;
3791                $patch_line = <$fd>;
3792                chomp $patch_line;
3793                #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3794
3795                print format_diff_from_to_header($last_patch_line, $patch_line,
3796                                                 $diffinfo, \%from, \%to,
3797                                                 @hash_parents);
3798
3799                # the patch itself
3800        LINE:
3801                while ($patch_line = <$fd>) {
3802                        chomp $patch_line;
3803
3804                        next PATCH if ($patch_line =~ m/^diff /);
3805
3806                        print format_diff_line($patch_line, \%from, \%to);
3807                }
3808
3809        } continue {
3810                print "</div>\n"; # class="patch"
3811        }
3812
3813        # for compact combined (--cc) format, with chunk and patch simpliciaction
3814        # patchset might be empty, but there might be unprocessed raw lines
3815        for (++$patch_idx if $patch_number > 0;
3816             $patch_idx < @$difftree;
3817             ++$patch_idx) {
3818                # read and prepare patch information
3819                $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3820
3821                # generate anchor for "patch" links in difftree / whatchanged part
3822                print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3823                      format_diff_cc_simplified($diffinfo, @hash_parents) .
3824                      "</div>\n";  # class="patch"
3825
3826                $patch_number++;
3827        }
3828
3829        if ($patch_number == 0) {
3830                if (@hash_parents > 1) {
3831                        print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3832                } else {
3833                        print "<div class=\"diff nodifferences\">No differences found</div>\n";
3834                }
3835        }
3836
3837        print "</div>\n"; # class="patchset"
3838}
3839
3840# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3841
3842# fills project list info (age, description, owner, forks) for each
3843# project in the list, removing invalid projects from returned list
3844# NOTE: modifies $projlist, but does not remove entries from it
3845sub fill_project_list_info {
3846        my ($projlist, $check_forks) = @_;
3847        my @projects;
3848
3849        my $show_ctags = gitweb_check_feature('ctags');
3850 PROJECT:
3851        foreach my $pr (@$projlist) {
3852                my (@activity) = git_get_last_activity($pr->{'path'});
3853                unless (@activity) {
3854                        next PROJECT;
3855                }
3856                ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3857                if (!defined $pr->{'descr'}) {
3858                        my $descr = git_get_project_description($pr->{'path'}) || "";
3859                        $descr = to_utf8($descr);
3860                        $pr->{'descr_long'} = $descr;
3861                        $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3862                }
3863                if (!defined $pr->{'owner'}) {
3864                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3865                }
3866                if ($check_forks) {
3867                        my $pname = $pr->{'path'};
3868                        if (($pname =~ s/\.git$//) &&
3869                            ($pname !~ /\/$/) &&
3870                            (-d "$projectroot/$pname")) {
3871                                $pr->{'forks'} = "-d $projectroot/$pname";
3872                        }       else {
3873                                $pr->{'forks'} = 0;
3874                        }
3875                }
3876                $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3877                push @projects, $pr;
3878        }
3879
3880        return @projects;
3881}
3882
3883# print 'sort by' <th> element, generating 'sort by $name' replay link
3884# if that order is not selected
3885sub print_sort_th {
3886        my ($name, $order, $header) = @_;
3887        $header ||= ucfirst($name);
3888
3889        if ($order eq $name) {
3890                print "<th>$header</th>\n";
3891        } else {
3892                print "<th>" .
3893                      $cgi->a({-href => href(-replay=>1, order=>$name),
3894                               -class => "header"}, $header) .
3895                      "</th>\n";
3896        }
3897}
3898
3899sub git_project_list_body {
3900        # actually uses global variable $project
3901        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3902
3903        my ($check_forks) = gitweb_check_feature('forks');
3904        my @projects = fill_project_list_info($projlist, $check_forks);
3905
3906        $order ||= $default_projects_order;
3907        $from = 0 unless defined $from;
3908        $to = $#projects if (!defined $to || $#projects < $to);
3909
3910        my %order_info = (
3911                project => { key => 'path', type => 'str' },
3912                descr => { key => 'descr_long', type => 'str' },
3913                owner => { key => 'owner', type => 'str' },
3914                age => { key => 'age', type => 'num' }
3915        );
3916        my $oi = $order_info{$order};
3917        if ($oi->{'type'} eq 'str') {
3918                @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3919        } else {
3920                @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3921        }
3922
3923        my $show_ctags = gitweb_check_feature('ctags');
3924        if ($show_ctags) {
3925                my %ctags;
3926                foreach my $p (@projects) {
3927                        foreach my $ct (keys %{$p->{'ctags'}}) {
3928                                $ctags{$ct} += $p->{'ctags'}->{$ct};
3929                        }
3930                }
3931                my $cloud = git_populate_project_tagcloud(\%ctags);
3932                print git_show_project_tagcloud($cloud, 64);
3933        }
3934
3935        print "<table class=\"project_list\">\n";
3936        unless ($no_header) {
3937                print "<tr>\n";
3938                if ($check_forks) {
3939                        print "<th></th>\n";
3940                }
3941                print_sort_th('project', $order, 'Project');
3942                print_sort_th('descr', $order, 'Description');
3943                print_sort_th('owner', $order, 'Owner');
3944                print_sort_th('age', $order, 'Last Change');
3945                print "<th></th>\n" . # for links
3946                      "</tr>\n";
3947        }
3948        my $alternate = 1;
3949        my $tagfilter = $cgi->param('by_tag');
3950        for (my $i = $from; $i <= $to; $i++) {
3951                my $pr = $projects[$i];
3952
3953                next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3954                next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3955                        and not $pr->{'descr_long'} =~ /$searchtext/;
3956                # Weed out forks or non-matching entries of search
3957                if ($check_forks) {
3958                        my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3959                        $forkbase="^$forkbase" if $forkbase;
3960                        next if not $searchtext and not $tagfilter and $show_ctags
3961                                and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3962                }
3963
3964                if ($alternate) {
3965                        print "<tr class=\"dark\">\n";
3966                } else {
3967                        print "<tr class=\"light\">\n";
3968                }
3969                $alternate ^= 1;
3970                if ($check_forks) {
3971                        print "<td>";
3972                        if ($pr->{'forks'}) {
3973                                print "<!-- $pr->{'forks'} -->\n";
3974                                print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3975                        }
3976                        print "</td>\n";
3977                }
3978                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3979                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3980                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3981                                        -class => "list", -title => $pr->{'descr_long'}},
3982                                        esc_html($pr->{'descr'})) . "</td>\n" .
3983                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3984                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3985                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3986                      "<td class=\"link\">" .
3987                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3988                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3989                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3990                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3991                      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3992                      "</td>\n" .
3993                      "</tr>\n";
3994        }
3995        if (defined $extra) {
3996                print "<tr>\n";
3997                if ($check_forks) {
3998                        print "<td></td>\n";
3999                }
4000                print "<td colspan=\"5\">$extra</td>\n" .
4001                      "</tr>\n";
4002        }
4003        print "</table>\n";
4004}
4005
4006sub git_shortlog_body {
4007        # uses global variable $project
4008        my ($commitlist, $from, $to, $refs, $extra) = @_;
4009
4010        $from = 0 unless defined $from;
4011        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4012
4013        print "<table class=\"shortlog\">\n";
4014        my $alternate = 1;
4015        for (my $i = $from; $i <= $to; $i++) {
4016                my %co = %{$commitlist->[$i]};
4017                my $commit = $co{'id'};
4018                my $ref = format_ref_marker($refs, $commit);
4019                if ($alternate) {
4020                        print "<tr class=\"dark\">\n";
4021                } else {
4022                        print "<tr class=\"light\">\n";
4023                }
4024                $alternate ^= 1;
4025                my $author = chop_and_escape_str($co{'author_name'}, 10);
4026                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4027                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4028                      "<td><i>" . $author . "</i></td>\n" .
4029                      "<td>";
4030                print format_subject_html($co{'title'}, $co{'title_short'},
4031                                          href(action=>"commit", hash=>$commit), $ref);
4032                print "</td>\n" .
4033                      "<td class=\"link\">" .
4034                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4035                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4036                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4037                my $snapshot_links = format_snapshot_links($commit);
4038                if (defined $snapshot_links) {
4039                        print " | " . $snapshot_links;
4040                }
4041                print "</td>\n" .
4042                      "</tr>\n";
4043        }
4044        if (defined $extra) {
4045                print "<tr>\n" .
4046                      "<td colspan=\"4\">$extra</td>\n" .
4047                      "</tr>\n";
4048        }
4049        print "</table>\n";
4050}
4051
4052sub git_history_body {
4053        # Warning: assumes constant type (blob or tree) during history
4054        my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4055
4056        $from = 0 unless defined $from;
4057        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4058
4059        print "<table class=\"history\">\n";
4060        my $alternate = 1;
4061        for (my $i = $from; $i <= $to; $i++) {
4062                my %co = %{$commitlist->[$i]};
4063                if (!%co) {
4064                        next;
4065                }
4066                my $commit = $co{'id'};
4067
4068                my $ref = format_ref_marker($refs, $commit);
4069
4070                if ($alternate) {
4071                        print "<tr class=\"dark\">\n";
4072                } else {
4073                        print "<tr class=\"light\">\n";
4074                }
4075                $alternate ^= 1;
4076        # shortlog uses      chop_str($co{'author_name'}, 10)
4077                my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4078                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4079                      "<td><i>" . $author . "</i></td>\n" .
4080                      "<td>";
4081                # originally git_history used chop_str($co{'title'}, 50)
4082                print format_subject_html($co{'title'}, $co{'title_short'},
4083                                          href(action=>"commit", hash=>$commit), $ref);
4084                print "</td>\n" .
4085                      "<td class=\"link\">" .
4086                      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4087                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4088
4089                if ($ftype eq 'blob') {
4090                        my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4091                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4092                        if (defined $blob_current && defined $blob_parent &&
4093                                        $blob_current ne $blob_parent) {
4094                                print " | " .
4095                                        $cgi->a({-href => href(action=>"blobdiff",
4096                                                               hash=>$blob_current, hash_parent=>$blob_parent,
4097                                                               hash_base=>$hash_base, hash_parent_base=>$commit,
4098                                                               file_name=>$file_name)},
4099                                                "diff to current");
4100                        }
4101                }
4102                print "</td>\n" .
4103                      "</tr>\n";
4104        }
4105        if (defined $extra) {
4106                print "<tr>\n" .
4107                      "<td colspan=\"4\">$extra</td>\n" .
4108                      "</tr>\n";
4109        }
4110        print "</table>\n";
4111}
4112
4113sub git_tags_body {
4114        # uses global variable $project
4115        my ($taglist, $from, $to, $extra) = @_;
4116        $from = 0 unless defined $from;
4117        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4118
4119        print "<table class=\"tags\">\n";
4120        my $alternate = 1;
4121        for (my $i = $from; $i <= $to; $i++) {
4122                my $entry = $taglist->[$i];
4123                my %tag = %$entry;
4124                my $comment = $tag{'subject'};
4125                my $comment_short;
4126                if (defined $comment) {
4127                        $comment_short = chop_str($comment, 30, 5);
4128                }
4129                if ($alternate) {
4130                        print "<tr class=\"dark\">\n";
4131                } else {
4132                        print "<tr class=\"light\">\n";
4133                }
4134                $alternate ^= 1;
4135                if (defined $tag{'age'}) {
4136                        print "<td><i>$tag{'age'}</i></td>\n";
4137                } else {
4138                        print "<td></td>\n";
4139                }
4140                print "<td>" .
4141                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4142                               -class => "list name"}, esc_html($tag{'name'})) .
4143                      "</td>\n" .
4144                      "<td>";
4145                if (defined $comment) {
4146                        print format_subject_html($comment, $comment_short,
4147                                                  href(action=>"tag", hash=>$tag{'id'}));
4148                }
4149                print "</td>\n" .
4150                      "<td class=\"selflink\">";
4151                if ($tag{'type'} eq "tag") {
4152                        print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4153                } else {
4154                        print "&nbsp;";
4155                }
4156                print "</td>\n" .
4157                      "<td class=\"link\">" . " | " .
4158                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4159                if ($tag{'reftype'} eq "commit") {
4160                        print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4161                              " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4162                } elsif ($tag{'reftype'} eq "blob") {
4163                        print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4164                }
4165                print "</td>\n" .
4166                      "</tr>";
4167        }
4168        if (defined $extra) {
4169                print "<tr>\n" .
4170                      "<td colspan=\"5\">$extra</td>\n" .
4171                      "</tr>\n";
4172        }
4173        print "</table>\n";
4174}
4175
4176sub git_heads_body {
4177        # uses global variable $project
4178        my ($headlist, $head, $from, $to, $extra) = @_;
4179        $from = 0 unless defined $from;
4180        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4181
4182        print "<table class=\"heads\">\n";
4183        my $alternate = 1;
4184        for (my $i = $from; $i <= $to; $i++) {
4185                my $entry = $headlist->[$i];
4186                my %ref = %$entry;
4187                my $curr = $ref{'id'} eq $head;
4188                if ($alternate) {
4189                        print "<tr class=\"dark\">\n";
4190                } else {
4191                        print "<tr class=\"light\">\n";
4192                }
4193                $alternate ^= 1;
4194                print "<td><i>$ref{'age'}</i></td>\n" .
4195                      ($curr ? "<td class=\"current_head\">" : "<td>") .
4196                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4197                               -class => "list name"},esc_html($ref{'name'})) .
4198                      "</td>\n" .
4199                      "<td class=\"link\">" .
4200                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4201                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4202                      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4203                      "</td>\n" .
4204                      "</tr>";
4205        }
4206        if (defined $extra) {
4207                print "<tr>\n" .
4208                      "<td colspan=\"3\">$extra</td>\n" .
4209                      "</tr>\n";
4210        }
4211        print "</table>\n";
4212}
4213
4214sub git_search_grep_body {
4215        my ($commitlist, $from, $to, $extra) = @_;
4216        $from = 0 unless defined $from;
4217        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4218
4219        print "<table class=\"commit_search\">\n";
4220        my $alternate = 1;
4221        for (my $i = $from; $i <= $to; $i++) {
4222                my %co = %{$commitlist->[$i]};
4223                if (!%co) {
4224                        next;
4225                }
4226                my $commit = $co{'id'};
4227                if ($alternate) {
4228                        print "<tr class=\"dark\">\n";
4229                } else {
4230                        print "<tr class=\"light\">\n";
4231                }
4232                $alternate ^= 1;
4233                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4234                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4235                      "<td><i>" . $author . "</i></td>\n" .
4236                      "<td>" .
4237                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4238                               -class => "list subject"},
4239                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
4240                my $comment = $co{'comment'};
4241                foreach my $line (@$comment) {
4242                        if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4243                                my ($lead, $match, $trail) = ($1, $2, $3);
4244                                $match = chop_str($match, 70, 5, 'center');
4245                                my $contextlen = int((80 - length($match))/2);
4246                                $contextlen = 30 if ($contextlen > 30);
4247                                $lead  = chop_str($lead,  $contextlen, 10, 'left');
4248                                $trail = chop_str($trail, $contextlen, 10, 'right');
4249
4250                                $lead  = esc_html($lead);
4251                                $match = esc_html($match);
4252                                $trail = esc_html($trail);
4253
4254                                print "$lead<span class=\"match\">$match</span>$trail<br />";
4255                        }
4256                }
4257                print "</td>\n" .
4258                      "<td class=\"link\">" .
4259                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4260                      " | " .
4261                      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4262                      " | " .
4263                      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4264                print "</td>\n" .
4265                      "</tr>\n";
4266        }
4267        if (defined $extra) {
4268                print "<tr>\n" .
4269                      "<td colspan=\"3\">$extra</td>\n" .
4270                      "</tr>\n";
4271        }
4272        print "</table>\n";
4273}
4274
4275## ======================================================================
4276## ======================================================================
4277## actions
4278
4279sub git_project_list {
4280        my $order = $input_params{'order'};
4281        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4282                die_error(400, "Unknown order parameter");
4283        }
4284
4285        my @list = git_get_projects_list();
4286        if (!@list) {
4287                die_error(404, "No projects found");
4288        }
4289
4290        git_header_html();
4291        if (-f $home_text) {
4292                print "<div class=\"index_include\">\n";
4293                open (my $fd, $home_text);
4294                print <$fd>;
4295                close $fd;
4296                print "</div>\n";
4297        }
4298        print $cgi->startform(-method => "get") .
4299              "<p class=\"projsearch\">Search:\n" .
4300              $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4301              "</p>" .
4302              $cgi->end_form() . "\n";
4303        git_project_list_body(\@list, $order);
4304        git_footer_html();
4305}
4306
4307sub git_forks {
4308        my $order = $input_params{'order'};
4309        if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4310                die_error(400, "Unknown order parameter");
4311        }
4312
4313        my @list = git_get_projects_list($project);
4314        if (!@list) {
4315                die_error(404, "No forks found");
4316        }
4317
4318        git_header_html();
4319        git_print_page_nav('','');
4320        git_print_header_div('summary', "$project forks");
4321        git_project_list_body(\@list, $order);
4322        git_footer_html();
4323}
4324
4325sub git_project_index {
4326        my @projects = git_get_projects_list($project);
4327
4328        print $cgi->header(
4329                -type => 'text/plain',
4330                -charset => 'utf-8',
4331                -content_disposition => 'inline; filename="index.aux"');
4332
4333        foreach my $pr (@projects) {
4334                if (!exists $pr->{'owner'}) {
4335                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4336                }
4337
4338                my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4339                # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4340                $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4341                $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4342                $path  =~ s/ /\+/g;
4343                $owner =~ s/ /\+/g;
4344
4345                print "$path $owner\n";
4346        }
4347}
4348
4349sub git_summary {
4350        my $descr = git_get_project_description($project) || "none";
4351        my %co = parse_commit("HEAD");
4352        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4353        my $head = $co{'id'};
4354
4355        my $owner = git_get_project_owner($project);
4356
4357        my $refs = git_get_references();
4358        # These get_*_list functions return one more to allow us to see if
4359        # there are more ...
4360        my @taglist  = git_get_tags_list(16);
4361        my @headlist = git_get_heads_list(16);
4362        my @forklist;
4363        my ($check_forks) = gitweb_check_feature('forks');
4364
4365        if ($check_forks) {
4366                @forklist = git_get_projects_list($project);
4367        }
4368
4369        git_header_html();
4370        git_print_page_nav('summary','', $head);
4371
4372        print "<div class=\"title\">&nbsp;</div>\n";
4373        print "<table class=\"projects_list\">\n" .
4374              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4375              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4376        if (defined $cd{'rfc2822'}) {
4377                print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4378        }
4379
4380        # use per project git URL list in $projectroot/$project/cloneurl
4381        # or make project git URL from git base URL and project name
4382        my $url_tag = "URL";
4383        my @url_list = git_get_project_url_list($project);
4384        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4385        foreach my $git_url (@url_list) {
4386                next unless $git_url;
4387                print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4388                $url_tag = "";
4389        }
4390
4391        # Tag cloud
4392        my $show_ctags = (gitweb_check_feature('ctags'))[0];
4393        if ($show_ctags) {
4394                my $ctags = git_get_project_ctags($project);
4395                my $cloud = git_populate_project_tagcloud($ctags);
4396                print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4397                print "</td>\n<td>" unless %$ctags;
4398                print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4399                print "</td>\n<td>" if %$ctags;
4400                print git_show_project_tagcloud($cloud, 48);
4401                print "</td></tr>";
4402        }
4403
4404        print "</table>\n";
4405
4406        if (-s "$projectroot/$project/README.html") {
4407                if (open my $fd, "$projectroot/$project/README.html") {
4408                        print "<div class=\"title\">readme</div>\n" .
4409                              "<div class=\"readme\">\n";
4410                        print $_ while (<$fd>);
4411                        print "\n</div>\n"; # class="readme"
4412                        close $fd;
4413                }
4414        }
4415
4416        # we need to request one more than 16 (0..15) to check if
4417        # those 16 are all
4418        my @commitlist = $head ? parse_commits($head, 17) : ();
4419        if (@commitlist) {
4420                git_print_header_div('shortlog');
4421                git_shortlog_body(\@commitlist, 0, 15, $refs,
4422                                  $#commitlist <=  15 ? undef :
4423                                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
4424        }
4425
4426        if (@taglist) {
4427                git_print_header_div('tags');
4428                git_tags_body(\@taglist, 0, 15,
4429                              $#taglist <=  15 ? undef :
4430                              $cgi->a({-href => href(action=>"tags")}, "..."));
4431        }
4432
4433        if (@headlist) {
4434                git_print_header_div('heads');
4435                git_heads_body(\@headlist, $head, 0, 15,
4436                               $#headlist <= 15 ? undef :
4437                               $cgi->a({-href => href(action=>"heads")}, "..."));
4438        }
4439
4440        if (@forklist) {
4441                git_print_header_div('forks');
4442                git_project_list_body(\@forklist, 'age', 0, 15,
4443                                      $#forklist <= 15 ? undef :
4444                                      $cgi->a({-href => href(action=>"forks")}, "..."),
4445                                      'no_header');
4446        }
4447
4448        git_footer_html();
4449}
4450
4451sub git_tag {
4452        my $head = git_get_head_hash($project);
4453        git_header_html();
4454        git_print_page_nav('','', $head,undef,$head);
4455        my %tag = parse_tag($hash);
4456
4457        if (! %tag) {
4458                die_error(404, "Unknown tag object");
4459        }
4460
4461        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4462        print "<div class=\"title_text\">\n" .
4463              "<table class=\"object_header\">\n" .
4464              "<tr>\n" .
4465              "<td>object</td>\n" .
4466              "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4467                               $tag{'object'}) . "</td>\n" .
4468              "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4469                                              $tag{'type'}) . "</td>\n" .
4470              "</tr>\n";
4471        if (defined($tag{'author'})) {
4472                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4473                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4474                print "<tr><td></td><td>" . $ad{'rfc2822'} .
4475                        sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4476                        "</td></tr>\n";
4477        }
4478        print "</table>\n\n" .
4479              "</div>\n";
4480        print "<div class=\"page_body\">";
4481        my $comment = $tag{'comment'};
4482        foreach my $line (@$comment) {
4483                chomp $line;
4484                print esc_html($line, -nbsp=>1) . "<br/>\n";
4485        }
4486        print "</div>\n";
4487        git_footer_html();
4488}
4489
4490sub git_blame {
4491        my $fd;
4492        my $ftype;
4493
4494        gitweb_check_feature('blame')
4495            or die_error(403, "Blame view not allowed");
4496
4497        die_error(400, "No file name given") unless $file_name;
4498        $hash_base ||= git_get_head_hash($project);
4499        die_error(404, "Couldn't find base commit") unless ($hash_base);
4500        my %co = parse_commit($hash_base)
4501                or die_error(404, "Commit not found");
4502        if (!defined $hash) {
4503                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4504                        or die_error(404, "Error looking up file");
4505        }
4506        $ftype = git_get_type($hash);
4507        if ($ftype !~ "blob") {
4508                die_error(400, "Object is not a blob");
4509        }
4510        open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4511              $file_name, $hash_base)
4512                or die_error(500, "Open git-blame failed");
4513        git_header_html();
4514        my $formats_nav =
4515                $cgi->a({-href => href(action=>"blob", -replay=>1)},
4516                        "blob") .
4517                " | " .
4518                $cgi->a({-href => href(action=>"history", -replay=>1)},
4519                        "history") .
4520                " | " .
4521                $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4522                        "HEAD");
4523        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4524        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4525        git_print_page_path($file_name, $ftype, $hash_base);
4526        my @rev_color = (qw(light2 dark2));
4527        my $num_colors = scalar(@rev_color);
4528        my $current_color = 0;
4529        my $last_rev;
4530        print <<HTML;
4531<div class="page_body">
4532<table class="blame">
4533<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4534HTML
4535        my %metainfo = ();
4536        while (1) {
4537                $_ = <$fd>;
4538                last unless defined $_;
4539                my ($full_rev, $orig_lineno, $lineno, $group_size) =
4540                    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4541                if (!exists $metainfo{$full_rev}) {
4542                        $metainfo{$full_rev} = {};
4543                }
4544                my $meta = $metainfo{$full_rev};
4545                while (<$fd>) {
4546                        last if (s/^\t//);
4547                        if (/^(\S+) (.*)$/) {
4548                                $meta->{$1} = $2;
4549                        }
4550                }
4551                my $data = $_;
4552                chomp $data;
4553                my $rev = substr($full_rev, 0, 8);
4554                my $author = $meta->{'author'};
4555                my %date = parse_date($meta->{'author-time'},
4556                                      $meta->{'author-tz'});
4557                my $date = $date{'iso-tz'};
4558                if ($group_size) {
4559                        $current_color = ++$current_color % $num_colors;
4560                }
4561                print "<tr class=\"$rev_color[$current_color]\">\n";
4562                if ($group_size) {
4563                        print "<td class=\"sha1\"";
4564                        print " title=\"". esc_html($author) . ", $date\"";
4565                        print " rowspan=\"$group_size\"" if ($group_size > 1);
4566                        print ">";
4567                        print $cgi->a({-href => href(action=>"commit",
4568                                                     hash=>$full_rev,
4569                                                     file_name=>$file_name)},
4570                                      esc_html($rev));
4571                        print "</td>\n";
4572                }
4573                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4574                        or die_error(500, "Open git-rev-parse failed");
4575                my $parent_commit = <$dd>;
4576                close $dd;
4577                chomp($parent_commit);
4578                my $blamed = href(action => 'blame',
4579                                  file_name => $meta->{'filename'},
4580                                  hash_base => $parent_commit);
4581                print "<td class=\"linenr\">";
4582                print $cgi->a({ -href => "$blamed#l$orig_lineno",
4583                                -id => "l$lineno",
4584                                -class => "linenr" },
4585                              esc_html($lineno));
4586                print "</td>";
4587                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4588                print "</tr>\n";
4589        }
4590        print "</table>\n";
4591        print "</div>";
4592        close $fd
4593                or print "Reading blob failed\n";
4594        git_footer_html();
4595}
4596
4597sub git_tags {
4598        my $head = git_get_head_hash($project);
4599        git_header_html();
4600        git_print_page_nav('','', $head,undef,$head);
4601        git_print_header_div('summary', $project);
4602
4603        my @tagslist = git_get_tags_list();
4604        if (@tagslist) {
4605                git_tags_body(\@tagslist);
4606        }
4607        git_footer_html();
4608}
4609
4610sub git_heads {
4611        my $head = git_get_head_hash($project);
4612        git_header_html();
4613        git_print_page_nav('','', $head,undef,$head);
4614        git_print_header_div('summary', $project);
4615
4616        my @headslist = git_get_heads_list();
4617        if (@headslist) {
4618                git_heads_body(\@headslist, $head);
4619        }
4620        git_footer_html();
4621}
4622
4623sub git_blob_plain {
4624        my $type = shift;
4625        my $expires;
4626
4627        if (!defined $hash) {
4628                if (defined $file_name) {
4629                        my $base = $hash_base || git_get_head_hash($project);
4630                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4631                                or die_error(404, "Cannot find file");
4632                } else {
4633                        die_error(400, "No file name defined");
4634                }
4635        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4636                # blobs defined by non-textual hash id's can be cached
4637                $expires = "+1d";
4638        }
4639
4640        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4641                or die_error(500, "Open git-cat-file blob '$hash' failed");
4642
4643        # content-type (can include charset)
4644        $type = blob_contenttype($fd, $file_name, $type);
4645
4646        # "save as" filename, even when no $file_name is given
4647        my $save_as = "$hash";
4648        if (defined $file_name) {
4649                $save_as = $file_name;
4650        } elsif ($type =~ m/^text\//) {
4651                $save_as .= '.txt';
4652        }
4653
4654        print $cgi->header(
4655                -type => $type,
4656                -expires => $expires,
4657                -content_disposition => 'inline; filename="' . $save_as . '"');
4658        undef $/;
4659        binmode STDOUT, ':raw';
4660        print <$fd>;
4661        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4662        $/ = "\n";
4663        close $fd;
4664}
4665
4666sub git_blob {
4667        my $expires;
4668
4669        if (!defined $hash) {
4670                if (defined $file_name) {
4671                        my $base = $hash_base || git_get_head_hash($project);
4672                        $hash = git_get_hash_by_path($base, $file_name, "blob")
4673                                or die_error(404, "Cannot find file");
4674                } else {
4675                        die_error(400, "No file name defined");
4676                }
4677        } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4678                # blobs defined by non-textual hash id's can be cached
4679                $expires = "+1d";
4680        }
4681
4682        my ($have_blame) = gitweb_check_feature('blame');
4683        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4684                or die_error(500, "Couldn't cat $file_name, $hash");
4685        my $mimetype = blob_mimetype($fd, $file_name);
4686        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4687                close $fd;
4688                return git_blob_plain($mimetype);
4689        }
4690        # we can have blame only for text/* mimetype
4691        $have_blame &&= ($mimetype =~ m!^text/!);
4692
4693        git_header_html(undef, $expires);
4694        my $formats_nav = '';
4695        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4696                if (defined $file_name) {
4697                        if ($have_blame) {
4698                                $formats_nav .=
4699                                        $cgi->a({-href => href(action=>"blame", -replay=>1)},
4700                                                "blame") .
4701                                        " | ";
4702                        }
4703                        $formats_nav .=
4704                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4705                                        "history") .
4706                                " | " .
4707                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4708                                        "raw") .
4709                                " | " .
4710                                $cgi->a({-href => href(action=>"blob",
4711                                                       hash_base=>"HEAD", file_name=>$file_name)},
4712                                        "HEAD");
4713                } else {
4714                        $formats_nav .=
4715                                $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4716                                        "raw");
4717                }
4718                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4719                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4720        } else {
4721                print "<div class=\"page_nav\">\n" .
4722                      "<br/><br/></div>\n" .
4723                      "<div class=\"title\">$hash</div>\n";
4724        }
4725        git_print_page_path($file_name, "blob", $hash_base);
4726        print "<div class=\"page_body\">\n";
4727        if ($mimetype =~ m!^image/!) {
4728                print qq!<img type="$mimetype"!;
4729                if ($file_name) {
4730                        print qq! alt="$file_name" title="$file_name"!;
4731                }
4732                print qq! src="! .
4733                      href(action=>"blob_plain", hash=>$hash,
4734                           hash_base=>$hash_base, file_name=>$file_name) .
4735                      qq!" />\n!;
4736        } else {
4737                my $nr;
4738                while (my $line = <$fd>) {
4739                        chomp $line;
4740                        $nr++;
4741                        $line = untabify($line);
4742                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4743                               $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4744                }
4745        }
4746        close $fd
4747                or print "Reading blob failed.\n";
4748        print "</div>";
4749        git_footer_html();
4750}
4751
4752sub git_tree {
4753        if (!defined $hash_base) {
4754                $hash_base = "HEAD";
4755        }
4756        if (!defined $hash) {
4757                if (defined $file_name) {
4758                        $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4759                } else {
4760                        $hash = $hash_base;
4761                }
4762        }
4763        die_error(404, "No such tree") unless defined($hash);
4764        $/ = "\0";
4765        open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4766                or die_error(500, "Open git-ls-tree failed");
4767        my @entries = map { chomp; $_ } <$fd>;
4768        close $fd or die_error(404, "Reading tree failed");
4769        $/ = "\n";
4770
4771        my $refs = git_get_references();
4772        my $ref = format_ref_marker($refs, $hash_base);
4773        git_header_html();
4774        my $basedir = '';
4775        my ($have_blame) = gitweb_check_feature('blame');
4776        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4777                my @views_nav = ();
4778                if (defined $file_name) {
4779                        push @views_nav,
4780                                $cgi->a({-href => href(action=>"history", -replay=>1)},
4781                                        "history"),
4782                                $cgi->a({-href => href(action=>"tree",
4783                                                       hash_base=>"HEAD", file_name=>$file_name)},
4784                                        "HEAD"),
4785                }
4786                my $snapshot_links = format_snapshot_links($hash);
4787                if (defined $snapshot_links) {
4788                        # FIXME: Should be available when we have no hash base as well.
4789                        push @views_nav, $snapshot_links;
4790                }
4791                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4792                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4793        } else {
4794                undef $hash_base;
4795                print "<div class=\"page_nav\">\n";
4796                print "<br/><br/></div>\n";
4797                print "<div class=\"title\">$hash</div>\n";
4798        }
4799        if (defined $file_name) {
4800                $basedir = $file_name;
4801                if ($basedir ne '' && substr($basedir, -1) ne '/') {
4802                        $basedir .= '/';
4803                }
4804                git_print_page_path($file_name, 'tree', $hash_base);
4805        }
4806        print "<div class=\"page_body\">\n";
4807        print "<table class=\"tree\">\n";
4808        my $alternate = 1;
4809        # '..' (top directory) link if possible
4810        if (defined $hash_base &&
4811            defined $file_name && $file_name =~ m![^/]+$!) {
4812                if ($alternate) {
4813                        print "<tr class=\"dark\">\n";
4814                } else {
4815                        print "<tr class=\"light\">\n";
4816                }
4817                $alternate ^= 1;
4818
4819                my $up = $file_name;
4820                $up =~ s!/?[^/]+$!!;
4821                undef $up unless $up;
4822                # based on git_print_tree_entry
4823                print '<td class="mode">' . mode_str('040000') . "</td>\n";
4824                print '<td class="list">';
4825                print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4826                                             file_name=>$up)},
4827                              "..");
4828                print "</td>\n";
4829                print "<td class=\"link\"></td>\n";
4830
4831                print "</tr>\n";
4832        }
4833        foreach my $line (@entries) {
4834                my %t = parse_ls_tree_line($line, -z => 1);
4835
4836                if ($alternate) {
4837                        print "<tr class=\"dark\">\n";
4838                } else {
4839                        print "<tr class=\"light\">\n";
4840                }
4841                $alternate ^= 1;
4842
4843                git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4844
4845                print "</tr>\n";
4846        }
4847        print "</table>\n" .
4848              "</div>";
4849        git_footer_html();
4850}
4851
4852sub git_snapshot {
4853        my @supported_fmts = gitweb_check_feature('snapshot');
4854        @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4855
4856        my $format = $input_params{'snapshot_format'};
4857        if (!@supported_fmts) {
4858                die_error(403, "Snapshots not allowed");
4859        }
4860        # default to first supported snapshot format
4861        $format ||= $supported_fmts[0];
4862        if ($format !~ m/^[a-z0-9]+$/) {
4863                die_error(400, "Invalid snapshot format parameter");
4864        } elsif (!exists($known_snapshot_formats{$format})) {
4865                die_error(400, "Unknown snapshot format");
4866        } elsif (!grep($_ eq $format, @supported_fmts)) {
4867                die_error(403, "Unsupported snapshot format");
4868        }
4869
4870        if (!defined $hash) {
4871                $hash = git_get_head_hash($project);
4872        }
4873
4874        my $name = $project;
4875        $name =~ s,([^/])/*\.git$,$1,;
4876        $name = basename($name);
4877        my $filename = to_utf8($name);
4878        $name =~ s/\047/\047\\\047\047/g;
4879        my $cmd;
4880        $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4881        $cmd = quote_command(
4882                git_cmd(), 'archive',
4883                "--format=$known_snapshot_formats{$format}{'format'}",
4884                "--prefix=$name/", $hash);
4885        if (exists $known_snapshot_formats{$format}{'compressor'}) {
4886                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4887        }
4888
4889        print $cgi->header(
4890                -type => $known_snapshot_formats{$format}{'type'},
4891                -content_disposition => 'inline; filename="' . "$filename" . '"',
4892                -status => '200 OK');
4893
4894        open my $fd, "-|", $cmd
4895                or die_error(500, "Execute git-archive failed");
4896        binmode STDOUT, ':raw';
4897        print <$fd>;
4898        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4899        close $fd;
4900}
4901
4902sub git_log {
4903        my $head = git_get_head_hash($project);
4904        if (!defined $hash) {
4905                $hash = $head;
4906        }
4907        if (!defined $page) {
4908                $page = 0;
4909        }
4910        my $refs = git_get_references();
4911
4912        my @commitlist = parse_commits($hash, 101, (100 * $page));
4913
4914        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4915
4916        git_header_html();
4917        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4918
4919        if (!@commitlist) {
4920                my %co = parse_commit($hash);
4921
4922                git_print_header_div('summary', $project);
4923                print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4924        }
4925        my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4926        for (my $i = 0; $i <= $to; $i++) {
4927                my %co = %{$commitlist[$i]};
4928                next if !%co;
4929                my $commit = $co{'id'};
4930                my $ref = format_ref_marker($refs, $commit);
4931                my %ad = parse_date($co{'author_epoch'});
4932                git_print_header_div('commit',
4933                               "<span class=\"age\">$co{'age_string'}</span>" .
4934                               esc_html($co{'title'}) . $ref,
4935                               $commit);
4936                print "<div class=\"title_text\">\n" .
4937                      "<div class=\"log_link\">\n" .
4938                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4939                      " | " .
4940                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4941                      " | " .
4942                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4943                      "<br/>\n" .
4944                      "</div>\n" .
4945                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4946                      "</div>\n";
4947
4948                print "<div class=\"log_body\">\n";
4949                git_print_log($co{'comment'}, -final_empty_line=> 1);
4950                print "</div>\n";
4951        }
4952        if ($#commitlist >= 100) {
4953                print "<div class=\"page_nav\">\n";
4954                print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4955                               -accesskey => "n", -title => "Alt-n"}, "next");
4956                print "</div>\n";
4957        }
4958        git_footer_html();
4959}
4960
4961sub git_commit {
4962        $hash ||= $hash_base || "HEAD";
4963        my %co = parse_commit($hash)
4964            or die_error(404, "Unknown commit object");
4965        my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4966        my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4967
4968        my $parent  = $co{'parent'};
4969        my $parents = $co{'parents'}; # listref
4970
4971        # we need to prepare $formats_nav before any parameter munging
4972        my $formats_nav;
4973        if (!defined $parent) {
4974                # --root commitdiff
4975                $formats_nav .= '(initial)';
4976        } elsif (@$parents == 1) {
4977                # single parent commit
4978                $formats_nav .=
4979                        '(parent: ' .
4980                        $cgi->a({-href => href(action=>"commit",
4981                                               hash=>$parent)},
4982                                esc_html(substr($parent, 0, 7))) .
4983                        ')';
4984        } else {
4985                # merge commit
4986                $formats_nav .=
4987                        '(merge: ' .
4988                        join(' ', map {
4989                                $cgi->a({-href => href(action=>"commit",
4990                                                       hash=>$_)},
4991                                        esc_html(substr($_, 0, 7)));
4992                        } @$parents ) .
4993                        ')';
4994        }
4995
4996        if (!defined $parent) {
4997                $parent = "--root";
4998        }
4999        my @difftree;
5000        open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5001                @diff_opts,
5002                (@$parents <= 1 ? $parent : '-c'),
5003                $hash, "--"
5004                or die_error(500, "Open git-diff-tree failed");
5005        @difftree = map { chomp; $_ } <$fd>;
5006        close $fd or die_error(404, "Reading git-diff-tree failed");
5007
5008        # non-textual hash id's can be cached
5009        my $expires;
5010        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5011                $expires = "+1d";
5012        }
5013        my $refs = git_get_references();
5014        my $ref = format_ref_marker($refs, $co{'id'});
5015
5016        git_header_html(undef, $expires);
5017        git_print_page_nav('commit', '',
5018                           $hash, $co{'tree'}, $hash,
5019                           $formats_nav);
5020
5021        if (defined $co{'parent'}) {
5022                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5023        } else {
5024                git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5025        }
5026        print "<div class=\"title_text\">\n" .
5027              "<table class=\"object_header\">\n";
5028        print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5029              "<tr>" .
5030              "<td></td><td> $ad{'rfc2822'}";
5031        if ($ad{'hour_local'} < 6) {
5032                printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5033                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5034        } else {
5035                printf(" (%02d:%02d %s)",
5036                       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5037        }
5038        print "</td>" .
5039              "</tr>\n";
5040        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5041        print "<tr><td></td><td> $cd{'rfc2822'}" .
5042              sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5043              "</td></tr>\n";
5044        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5045        print "<tr>" .
5046              "<td>tree</td>" .
5047              "<td class=\"sha1\">" .
5048              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5049                       class => "list"}, $co{'tree'}) .
5050              "</td>" .
5051              "<td class=\"link\">" .
5052              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5053                      "tree");
5054        my $snapshot_links = format_snapshot_links($hash);
5055        if (defined $snapshot_links) {
5056                print " | " . $snapshot_links;
5057        }
5058        print "</td>" .
5059              "</tr>\n";
5060
5061        foreach my $par (@$parents) {
5062                print "<tr>" .
5063                      "<td>parent</td>" .
5064                      "<td class=\"sha1\">" .
5065                      $cgi->a({-href => href(action=>"commit", hash=>$par),
5066                               class => "list"}, $par) .
5067                      "</td>" .
5068                      "<td class=\"link\">" .
5069                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5070                      " | " .
5071                      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5072                      "</td>" .
5073                      "</tr>\n";
5074        }
5075        print "</table>".
5076              "</div>\n";
5077
5078        print "<div class=\"page_body\">\n";
5079        git_print_log($co{'comment'});
5080        print "</div>\n";
5081
5082        git_difftree_body(\@difftree, $hash, @$parents);
5083
5084        git_footer_html();
5085}
5086
5087sub git_object {
5088        # object is defined by:
5089        # - hash or hash_base alone
5090        # - hash_base and file_name
5091        my $type;
5092
5093        # - hash or hash_base alone
5094        if ($hash || ($hash_base && !defined $file_name)) {
5095                my $object_id = $hash || $hash_base;
5096
5097                open my $fd, "-|", quote_command(
5098                        git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5099                        or die_error(404, "Object does not exist");
5100                $type = <$fd>;
5101                chomp $type;
5102                close $fd
5103                        or die_error(404, "Object does not exist");
5104
5105        # - hash_base and file_name
5106        } elsif ($hash_base && defined $file_name) {
5107                $file_name =~ s,/+$,,;
5108
5109                system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5110                        or die_error(404, "Base object does not exist");
5111
5112                # here errors should not hapen
5113                open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5114                        or die_error(500, "Open git-ls-tree failed");
5115                my $line = <$fd>;
5116                close $fd;
5117
5118                #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5119                unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5120                        die_error(404, "File or directory for given base does not exist");
5121                }
5122                $type = $2;
5123                $hash = $3;
5124        } else {
5125                die_error(400, "Not enough information to find object");
5126        }
5127
5128        print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5129                                          hash=>$hash, hash_base=>$hash_base,
5130                                          file_name=>$file_name),
5131                             -status => '302 Found');
5132}
5133
5134sub git_blobdiff {
5135        my $format = shift || 'html';
5136
5137        my $fd;
5138        my @difftree;
5139        my %diffinfo;
5140        my $expires;
5141
5142        # preparing $fd and %diffinfo for git_patchset_body
5143        # new style URI
5144        if (defined $hash_base && defined $hash_parent_base) {
5145                if (defined $file_name) {
5146                        # read raw output
5147                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5148                                $hash_parent_base, $hash_base,
5149                                "--", (defined $file_parent ? $file_parent : ()), $file_name
5150                                or die_error(500, "Open git-diff-tree failed");
5151                        @difftree = map { chomp; $_ } <$fd>;
5152                        close $fd
5153                                or die_error(404, "Reading git-diff-tree failed");
5154                        @difftree
5155                                or die_error(404, "Blob diff not found");
5156
5157                } elsif (defined $hash &&
5158                         $hash =~ /[0-9a-fA-F]{40}/) {
5159                        # try to find filename from $hash
5160
5161                        # read filtered raw output
5162                        open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5163                                $hash_parent_base, $hash_base, "--"
5164                                or die_error(500, "Open git-diff-tree failed");
5165                        @difftree =
5166                                # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5167                                # $hash == to_id
5168                                grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5169                                map { chomp; $_ } <$fd>;
5170                        close $fd
5171                                or die_error(404, "Reading git-diff-tree failed");
5172                        @difftree
5173                                or die_error(404, "Blob diff not found");
5174
5175                } else {
5176                        die_error(400, "Missing one of the blob diff parameters");
5177                }
5178
5179                if (@difftree > 1) {
5180                        die_error(400, "Ambiguous blob diff specification");
5181                }
5182
5183                %diffinfo = parse_difftree_raw_line($difftree[0]);
5184                $file_parent ||= $diffinfo{'from_file'} || $file_name;
5185                $file_name   ||= $diffinfo{'to_file'};
5186
5187                $hash_parent ||= $diffinfo{'from_id'};
5188                $hash        ||= $diffinfo{'to_id'};
5189
5190                # non-textual hash id's can be cached
5191                if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5192                    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5193                        $expires = '+1d';
5194                }
5195
5196                # open patch output
5197                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5198                        '-p', ($format eq 'html' ? "--full-index" : ()),
5199                        $hash_parent_base, $hash_base,
5200                        "--", (defined $file_parent ? $file_parent : ()), $file_name
5201                        or die_error(500, "Open git-diff-tree failed");
5202        }
5203
5204        # old/legacy style URI
5205        if (!%diffinfo && # if new style URI failed
5206            defined $hash && defined $hash_parent) {
5207                # fake git-diff-tree raw output
5208                $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5209                $diffinfo{'from_id'} = $hash_parent;
5210                $diffinfo{'to_id'}   = $hash;
5211                if (defined $file_name) {
5212                        if (defined $file_parent) {
5213                                $diffinfo{'status'} = '2';
5214                                $diffinfo{'from_file'} = $file_parent;
5215                                $diffinfo{'to_file'}   = $file_name;
5216                        } else { # assume not renamed
5217                                $diffinfo{'status'} = '1';
5218                                $diffinfo{'from_file'} = $file_name;
5219                                $diffinfo{'to_file'}   = $file_name;
5220                        }
5221                } else { # no filename given
5222                        $diffinfo{'status'} = '2';
5223                        $diffinfo{'from_file'} = $hash_parent;
5224                        $diffinfo{'to_file'}   = $hash;
5225                }
5226
5227                # non-textual hash id's can be cached
5228                if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5229                    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5230                        $expires = '+1d';
5231                }
5232
5233                # open patch output
5234                open $fd, "-|", git_cmd(), "diff", @diff_opts,
5235                        '-p', ($format eq 'html' ? "--full-index" : ()),
5236                        $hash_parent, $hash, "--"
5237                        or die_error(500, "Open git-diff failed");
5238        } else  {
5239                die_error(400, "Missing one of the blob diff parameters")
5240                        unless %diffinfo;
5241        }
5242
5243        # header
5244        if ($format eq 'html') {
5245                my $formats_nav =
5246                        $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5247                                "raw");
5248                git_header_html(undef, $expires);
5249                if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5250                        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5251                        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5252                } else {
5253                        print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5254                        print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5255                }
5256                if (defined $file_name) {
5257                        git_print_page_path($file_name, "blob", $hash_base);
5258                } else {
5259                        print "<div class=\"page_path\"></div>\n";
5260                }
5261
5262        } elsif ($format eq 'plain') {
5263                print $cgi->header(
5264                        -type => 'text/plain',
5265                        -charset => 'utf-8',
5266                        -expires => $expires,
5267                        -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5268
5269                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5270
5271        } else {
5272                die_error(400, "Unknown blobdiff format");
5273        }
5274
5275        # patch
5276        if ($format eq 'html') {
5277                print "<div class=\"page_body\">\n";
5278
5279                git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5280                close $fd;
5281
5282                print "</div>\n"; # class="page_body"
5283                git_footer_html();
5284
5285        } else {
5286                while (my $line = <$fd>) {
5287                        $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5288                        $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5289
5290                        print $line;
5291
5292                        last if $line =~ m!^\+\+\+!;
5293                }
5294                local $/ = undef;
5295                print <$fd>;
5296                close $fd;
5297        }
5298}
5299
5300sub git_blobdiff_plain {
5301        git_blobdiff('plain');
5302}
5303
5304sub git_commitdiff {
5305        my $format = shift || 'html';
5306        $hash ||= $hash_base || "HEAD";
5307        my %co = parse_commit($hash)
5308            or die_error(404, "Unknown commit object");
5309
5310        # choose format for commitdiff for merge
5311        if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5312                $hash_parent = '--cc';
5313        }
5314        # we need to prepare $formats_nav before almost any parameter munging
5315        my $formats_nav;
5316        if ($format eq 'html') {
5317                $formats_nav =
5318                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5319                                "raw");
5320
5321                if (defined $hash_parent &&
5322                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
5323                        # commitdiff with two commits given
5324                        my $hash_parent_short = $hash_parent;
5325                        if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5326                                $hash_parent_short = substr($hash_parent, 0, 7);
5327                        }
5328                        $formats_nav .=
5329                                ' (from';
5330                        for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5331                                if ($co{'parents'}[$i] eq $hash_parent) {
5332                                        $formats_nav .= ' parent ' . ($i+1);
5333                                        last;
5334                                }
5335                        }
5336                        $formats_nav .= ': ' .
5337                                $cgi->a({-href => href(action=>"commitdiff",
5338                                                       hash=>$hash_parent)},
5339                                        esc_html($hash_parent_short)) .
5340                                ')';
5341                } elsif (!$co{'parent'}) {
5342                        # --root commitdiff
5343                        $formats_nav .= ' (initial)';
5344                } elsif (scalar @{$co{'parents'}} == 1) {
5345                        # single parent commit
5346                        $formats_nav .=
5347                                ' (parent: ' .
5348                                $cgi->a({-href => href(action=>"commitdiff",
5349                                                       hash=>$co{'parent'})},
5350                                        esc_html(substr($co{'parent'}, 0, 7))) .
5351                                ')';
5352                } else {
5353                        # merge commit
5354                        if ($hash_parent eq '--cc') {
5355                                $formats_nav .= ' | ' .
5356                                        $cgi->a({-href => href(action=>"commitdiff",
5357                                                               hash=>$hash, hash_parent=>'-c')},
5358                                                'combined');
5359                        } else { # $hash_parent eq '-c'
5360                                $formats_nav .= ' | ' .
5361                                        $cgi->a({-href => href(action=>"commitdiff",
5362                                                               hash=>$hash, hash_parent=>'--cc')},
5363                                                'compact');
5364                        }
5365                        $formats_nav .=
5366                                ' (merge: ' .
5367                                join(' ', map {
5368                                        $cgi->a({-href => href(action=>"commitdiff",
5369                                                               hash=>$_)},
5370                                                esc_html(substr($_, 0, 7)));
5371                                } @{$co{'parents'}} ) .
5372                                ')';
5373                }
5374        }
5375
5376        my $hash_parent_param = $hash_parent;
5377        if (!defined $hash_parent_param) {
5378                # --cc for multiple parents, --root for parentless
5379                $hash_parent_param =
5380                        @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5381        }
5382
5383        # read commitdiff
5384        my $fd;
5385        my @difftree;
5386        if ($format eq 'html') {
5387                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5388                        "--no-commit-id", "--patch-with-raw", "--full-index",
5389                        $hash_parent_param, $hash, "--"
5390                        or die_error(500, "Open git-diff-tree failed");
5391
5392                while (my $line = <$fd>) {
5393                        chomp $line;
5394                        # empty line ends raw part of diff-tree output
5395                        last unless $line;
5396                        push @difftree, scalar parse_difftree_raw_line($line);
5397                }
5398
5399        } elsif ($format eq 'plain') {
5400                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5401                        '-p', $hash_parent_param, $hash, "--"
5402                        or die_error(500, "Open git-diff-tree failed");
5403
5404        } else {
5405                die_error(400, "Unknown commitdiff format");
5406        }
5407
5408        # non-textual hash id's can be cached
5409        my $expires;
5410        if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5411                $expires = "+1d";
5412        }
5413
5414        # write commit message
5415        if ($format eq 'html') {
5416                my $refs = git_get_references();
5417                my $ref = format_ref_marker($refs, $co{'id'});
5418
5419                git_header_html(undef, $expires);
5420                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5421                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5422                git_print_authorship(\%co);
5423                print "<div class=\"page_body\">\n";
5424                if (@{$co{'comment'}} > 1) {
5425                        print "<div class=\"log\">\n";
5426                        git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5427                        print "</div>\n"; # class="log"
5428                }
5429
5430        } elsif ($format eq 'plain') {
5431                my $refs = git_get_references("tags");
5432                my $tagname = git_get_rev_name_tags($hash);
5433                my $filename = basename($project) . "-$hash.patch";
5434
5435                print $cgi->header(
5436                        -type => 'text/plain',
5437                        -charset => 'utf-8',
5438                        -expires => $expires,
5439                        -content_disposition => 'inline; filename="' . "$filename" . '"');
5440                my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5441                print "From: " . to_utf8($co{'author'}) . "\n";
5442                print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5443                print "Subject: " . to_utf8($co{'title'}) . "\n";
5444
5445                print "X-Git-Tag: $tagname\n" if $tagname;
5446                print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5447
5448                foreach my $line (@{$co{'comment'}}) {
5449                        print to_utf8($line) . "\n";
5450                }
5451                print "---\n\n";
5452        }
5453
5454        # write patch
5455        if ($format eq 'html') {
5456                my $use_parents = !defined $hash_parent ||
5457                        $hash_parent eq '-c' || $hash_parent eq '--cc';
5458                git_difftree_body(\@difftree, $hash,
5459                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5460                print "<br/>\n";
5461
5462                git_patchset_body($fd, \@difftree, $hash,
5463                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
5464                close $fd;
5465                print "</div>\n"; # class="page_body"
5466                git_footer_html();
5467
5468        } elsif ($format eq 'plain') {
5469                local $/ = undef;
5470                print <$fd>;
5471                close $fd
5472                        or print "Reading git-diff-tree failed\n";
5473        }
5474}
5475
5476sub git_commitdiff_plain {
5477        git_commitdiff('plain');
5478}
5479
5480sub git_history {
5481        if (!defined $hash_base) {
5482                $hash_base = git_get_head_hash($project);
5483        }
5484        if (!defined $page) {
5485                $page = 0;
5486        }
5487        my $ftype;
5488        my %co = parse_commit($hash_base)
5489            or die_error(404, "Unknown commit object");
5490
5491        my $refs = git_get_references();
5492        my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5493
5494        my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5495                                       $file_name, "--full-history")
5496            or die_error(404, "No such file or directory on given branch");
5497
5498        if (!defined $hash && defined $file_name) {
5499                # some commits could have deleted file in question,
5500                # and not have it in tree, but one of them has to have it
5501                for (my $i = 0; $i <= @commitlist; $i++) {
5502                        $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5503                        last if defined $hash;
5504                }
5505        }
5506        if (defined $hash) {
5507                $ftype = git_get_type($hash);
5508        }
5509        if (!defined $ftype) {
5510                die_error(500, "Unknown type of object");
5511        }
5512
5513        my $paging_nav = '';
5514        if ($page > 0) {
5515                $paging_nav .=
5516                        $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5517                                               file_name=>$file_name)},
5518                                "first");
5519                $paging_nav .= " &sdot; " .
5520                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
5521                                 -accesskey => "p", -title => "Alt-p"}, "prev");
5522        } else {
5523                $paging_nav .= "first";
5524                $paging_nav .= " &sdot; prev";
5525        }
5526        my $next_link = '';
5527        if ($#commitlist >= 100) {
5528                $next_link =
5529                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5530                                 -accesskey => "n", -title => "Alt-n"}, "next");
5531                $paging_nav .= " &sdot; $next_link";
5532        } else {
5533                $paging_nav .= " &sdot; next";
5534        }
5535
5536        git_header_html();
5537        git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5538        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5539        git_print_page_path($file_name, $ftype, $hash_base);
5540
5541        git_history_body(\@commitlist, 0, 99,
5542                         $refs, $hash_base, $ftype, $next_link);
5543
5544        git_footer_html();
5545}
5546
5547sub git_search {
5548        gitweb_check_feature('search') or die_error(403, "Search is disabled");
5549        if (!defined $searchtext) {
5550                die_error(400, "Text field is empty");
5551        }
5552        if (!defined $hash) {
5553                $hash = git_get_head_hash($project);
5554        }
5555        my %co = parse_commit($hash);
5556        if (!%co) {
5557                die_error(404, "Unknown commit object");
5558        }
5559        if (!defined $page) {
5560                $page = 0;
5561        }
5562
5563        $searchtype ||= 'commit';
5564        if ($searchtype eq 'pickaxe') {
5565                # pickaxe may take all resources of your box and run for several minutes
5566                # with every query - so decide by yourself how public you make this feature
5567                gitweb_check_feature('pickaxe')
5568                    or die_error(403, "Pickaxe is disabled");
5569        }
5570        if ($searchtype eq 'grep') {
5571                gitweb_check_feature('grep')
5572                    or die_error(403, "Grep is disabled");
5573        }
5574
5575        git_header_html();
5576
5577        if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5578                my $greptype;
5579                if ($searchtype eq 'commit') {
5580                        $greptype = "--grep=";
5581                } elsif ($searchtype eq 'author') {
5582                        $greptype = "--author=";
5583                } elsif ($searchtype eq 'committer') {
5584                        $greptype = "--committer=";
5585                }
5586                $greptype .= $searchtext;
5587                my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5588                                               $greptype, '--regexp-ignore-case',
5589                                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5590
5591                my $paging_nav = '';
5592                if ($page > 0) {
5593                        $paging_nav .=
5594                                $cgi->a({-href => href(action=>"search", hash=>$hash,
5595                                                       searchtext=>$searchtext,
5596                                                       searchtype=>$searchtype)},
5597                                        "first");
5598                        $paging_nav .= " &sdot; " .
5599                                $cgi->a({-href => href(-replay=>1, page=>$page-1),
5600                                         -accesskey => "p", -title => "Alt-p"}, "prev");
5601                } else {
5602                        $paging_nav .= "first";
5603                        $paging_nav .= " &sdot; prev";
5604                }
5605                my $next_link = '';
5606                if ($#commitlist >= 100) {
5607                        $next_link =
5608                                $cgi->a({-href => href(-replay=>1, page=>$page+1),
5609                                         -accesskey => "n", -title => "Alt-n"}, "next");
5610                        $paging_nav .= " &sdot; $next_link";
5611                } else {
5612                        $paging_nav .= " &sdot; next";
5613                }
5614
5615                if ($#commitlist >= 100) {
5616                }
5617
5618                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5619                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5620                git_search_grep_body(\@commitlist, 0, 99, $next_link);
5621        }
5622
5623        if ($searchtype eq 'pickaxe') {
5624                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5625                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5626
5627                print "<table class=\"pickaxe search\">\n";
5628                my $alternate = 1;
5629                $/ = "\n";
5630                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5631                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5632                        ($search_use_regexp ? '--pickaxe-regex' : ());
5633                undef %co;
5634                my @files;
5635                while (my $line = <$fd>) {
5636                        chomp $line;
5637                        next unless $line;
5638
5639                        my %set = parse_difftree_raw_line($line);
5640                        if (defined $set{'commit'}) {
5641                                # finish previous commit
5642                                if (%co) {
5643                                        print "</td>\n" .
5644                                              "<td class=\"link\">" .
5645                                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5646                                              " | " .
5647                                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5648                                        print "</td>\n" .
5649                                              "</tr>\n";
5650                                }
5651
5652                                if ($alternate) {
5653                                        print "<tr class=\"dark\">\n";
5654                                } else {
5655                                        print "<tr class=\"light\">\n";
5656                                }
5657                                $alternate ^= 1;
5658                                %co = parse_commit($set{'commit'});
5659                                my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5660                                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5661                                      "<td><i>$author</i></td>\n" .
5662                                      "<td>" .
5663                                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5664                                              -class => "list subject"},
5665                                              chop_and_escape_str($co{'title'}, 50) . "<br/>");
5666                        } elsif (defined $set{'to_id'}) {
5667                                next if ($set{'to_id'} =~ m/^0{40}$/);
5668
5669                                print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5670                                                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5671                                              -class => "list"},
5672                                              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5673                                      "<br/>\n";
5674                        }
5675                }
5676                close $fd;
5677
5678                # finish last commit (warning: repetition!)
5679                if (%co) {
5680                        print "</td>\n" .
5681                              "<td class=\"link\">" .
5682                              $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5683                              " | " .
5684                              $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5685                        print "</td>\n" .
5686                              "</tr>\n";
5687                }
5688
5689                print "</table>\n";
5690        }
5691
5692        if ($searchtype eq 'grep') {
5693                git_print_page_nav('','', $hash,$co{'tree'},$hash);
5694                git_print_header_div('commit', esc_html($co{'title'}), $hash);
5695
5696                print "<table class=\"grep_search\">\n";
5697                my $alternate = 1;
5698                my $matches = 0;
5699                $/ = "\n";
5700                open my $fd, "-|", git_cmd(), 'grep', '-n',
5701                        $search_use_regexp ? ('-E', '-i') : '-F',
5702                        $searchtext, $co{'tree'};
5703                my $lastfile = '';
5704                while (my $line = <$fd>) {
5705                        chomp $line;
5706                        my ($file, $lno, $ltext, $binary);
5707                        last if ($matches++ > 1000);
5708                        if ($line =~ /^Binary file (.+) matches$/) {
5709                                $file = $1;
5710                                $binary = 1;
5711                        } else {
5712                                (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5713                        }
5714                        if ($file ne $lastfile) {
5715                                $lastfile and print "</td></tr>\n";
5716                                if ($alternate++) {
5717                                        print "<tr class=\"dark\">\n";
5718                                } else {
5719                                        print "<tr class=\"light\">\n";
5720                                }
5721                                print "<td class=\"list\">".
5722                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5723                                                               file_name=>"$file"),
5724                                                -class => "list"}, esc_path($file));
5725                                print "</td><td>\n";
5726                                $lastfile = $file;
5727                        }
5728                        if ($binary) {
5729                                print "<div class=\"binary\">Binary file</div>\n";
5730                        } else {
5731                                $ltext = untabify($ltext);
5732                                if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5733                                        $ltext = esc_html($1, -nbsp=>1);
5734                                        $ltext .= '<span class="match">';
5735                                        $ltext .= esc_html($2, -nbsp=>1);
5736                                        $ltext .= '</span>';
5737                                        $ltext .= esc_html($3, -nbsp=>1);
5738                                } else {
5739                                        $ltext = esc_html($ltext, -nbsp=>1);
5740                                }
5741                                print "<div class=\"pre\">" .
5742                                        $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5743                                                               file_name=>"$file").'#l'.$lno,
5744                                                -class => "linenr"}, sprintf('%4i', $lno))
5745                                        . ' ' .  $ltext . "</div>\n";
5746                        }
5747                }
5748                if ($lastfile) {
5749                        print "</td></tr>\n";
5750                        if ($matches > 1000) {
5751                                print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5752                        }
5753                } else {
5754                        print "<div class=\"diff nodifferences\">No matches found</div>\n";
5755                }
5756                close $fd;
5757
5758                print "</table>\n";
5759        }
5760        git_footer_html();
5761}
5762
5763sub git_search_help {
5764        git_header_html();
5765        git_print_page_nav('','', $hash,$hash,$hash);
5766        print <<EOT;
5767<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5768regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5769the pattern entered is recognized as the POSIX extended
5770<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5771insensitive).</p>
5772<dl>
5773<dt><b>commit</b></dt>
5774<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5775EOT
5776        my ($have_grep) = gitweb_check_feature('grep');
5777        if ($have_grep) {
5778                print <<EOT;
5779<dt><b>grep</b></dt>
5780<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5781    a different one) are searched for the given pattern. On large trees, this search can take
5782a while and put some strain on the server, so please use it with some consideration. Note that
5783due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5784case-sensitive.</dd>
5785EOT
5786        }
5787        print <<EOT;
5788<dt><b>author</b></dt>
5789<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5790<dt><b>committer</b></dt>
5791<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5792EOT
5793        my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5794        if ($have_pickaxe) {
5795                print <<EOT;
5796<dt><b>pickaxe</b></dt>
5797<dd>All commits that caused the string to appear or disappear from any file (changes that
5798added, removed or "modified" the string) will be listed. This search can take a while and
5799takes a lot of strain on the server, so please use it wisely. Note that since you may be
5800interested even in changes just changing the case as well, this search is case sensitive.</dd>
5801EOT
5802        }
5803        print "</dl>\n";
5804        git_footer_html();
5805}
5806
5807sub git_shortlog {
5808        my $head = git_get_head_hash($project);
5809        if (!defined $hash) {
5810                $hash = $head;
5811        }
5812        if (!defined $page) {
5813                $page = 0;
5814        }
5815        my $refs = git_get_references();
5816
5817        my $commit_hash = $hash;
5818        if (defined $hash_parent) {
5819                $commit_hash = "$hash_parent..$hash";
5820        }
5821        my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5822
5823        my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5824        my $next_link = '';
5825        if ($#commitlist >= 100) {
5826                $next_link =
5827                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
5828                                 -accesskey => "n", -title => "Alt-n"}, "next");
5829        }
5830
5831        git_header_html();
5832        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5833        git_print_header_div('summary', $project);
5834
5835        git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5836
5837        git_footer_html();
5838}
5839
5840## ......................................................................
5841## feeds (RSS, Atom; OPML)
5842
5843sub git_feed {
5844        my $format = shift || 'atom';
5845        my ($have_blame) = gitweb_check_feature('blame');
5846
5847        # Atom: http://www.atomenabled.org/developers/syndication/
5848        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5849        if ($format ne 'rss' && $format ne 'atom') {
5850                die_error(400, "Unknown web feed format");
5851        }
5852
5853        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5854        my $head = $hash || 'HEAD';
5855        my @commitlist = parse_commits($head, 150, 0, $file_name);
5856
5857        my %latest_commit;
5858        my %latest_date;
5859        my $content_type = "application/$format+xml";
5860        if (defined $cgi->http('HTTP_ACCEPT') &&
5861                 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5862                # browser (feed reader) prefers text/xml
5863                $content_type = 'text/xml';
5864        }
5865        if (defined($commitlist[0])) {
5866                %latest_commit = %{$commitlist[0]};
5867                %latest_date   = parse_date($latest_commit{'author_epoch'});
5868                print $cgi->header(
5869                        -type => $content_type,
5870                        -charset => 'utf-8',
5871                        -last_modified => $latest_date{'rfc2822'});
5872        } else {
5873                print $cgi->header(
5874                        -type => $content_type,
5875                        -charset => 'utf-8');
5876        }
5877
5878        # Optimization: skip generating the body if client asks only
5879        # for Last-Modified date.
5880        return if ($cgi->request_method() eq 'HEAD');
5881
5882        # header variables
5883        my $title = "$site_name - $project/$action";
5884        my $feed_type = 'log';
5885        if (defined $hash) {
5886                $title .= " - '$hash'";
5887                $feed_type = 'branch log';
5888                if (defined $file_name) {
5889                        $title .= " :: $file_name";
5890                        $feed_type = 'history';
5891                }
5892        } elsif (defined $file_name) {
5893                $title .= " - $file_name";
5894                $feed_type = 'history';
5895        }
5896        $title .= " $feed_type";
5897        my $descr = git_get_project_description($project);
5898        if (defined $descr) {
5899                $descr = esc_html($descr);
5900        } else {
5901                $descr = "$project " .
5902                         ($format eq 'rss' ? 'RSS' : 'Atom') .
5903                         " feed";
5904        }
5905        my $owner = git_get_project_owner($project);
5906        $owner = esc_html($owner);
5907
5908        #header
5909        my $alt_url;
5910        if (defined $file_name) {
5911                $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5912        } elsif (defined $hash) {
5913                $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5914        } else {
5915                $alt_url = href(-full=>1, action=>"summary");
5916        }
5917        print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5918        if ($format eq 'rss') {
5919                print <<XML;
5920<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5921<channel>
5922XML
5923                print "<title>$title</title>\n" .
5924                      "<link>$alt_url</link>\n" .
5925                      "<description>$descr</description>\n" .
5926                      "<language>en</language>\n";
5927        } elsif ($format eq 'atom') {
5928                print <<XML;
5929<feed xmlns="http://www.w3.org/2005/Atom">
5930XML
5931                print "<title>$title</title>\n" .
5932                      "<subtitle>$descr</subtitle>\n" .
5933                      '<link rel="alternate" type="text/html" href="' .
5934                      $alt_url . '" />' . "\n" .
5935                      '<link rel="self" type="' . $content_type . '" href="' .
5936                      $cgi->self_url() . '" />' . "\n" .
5937                      "<id>" . href(-full=>1) . "</id>\n" .
5938                      # use project owner for feed author
5939                      "<author><name>$owner</name></author>\n";
5940                if (defined $favicon) {
5941                        print "<icon>" . esc_url($favicon) . "</icon>\n";
5942                }
5943                if (defined $logo_url) {
5944                        # not twice as wide as tall: 72 x 27 pixels
5945                        print "<logo>" . esc_url($logo) . "</logo>\n";
5946                }
5947                if (! %latest_date) {
5948                        # dummy date to keep the feed valid until commits trickle in:
5949                        print "<updated>1970-01-01T00:00:00Z</updated>\n";
5950                } else {
5951                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
5952                }
5953        }
5954
5955        # contents
5956        for (my $i = 0; $i <= $#commitlist; $i++) {
5957                my %co = %{$commitlist[$i]};
5958                my $commit = $co{'id'};
5959                # we read 150, we always show 30 and the ones more recent than 48 hours
5960                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5961                        last;
5962                }
5963                my %cd = parse_date($co{'author_epoch'});
5964
5965                # get list of changed files
5966                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5967                        $co{'parent'} || "--root",
5968                        $co{'id'}, "--", (defined $file_name ? $file_name : ())
5969                        or next;
5970                my @difftree = map { chomp; $_ } <$fd>;
5971                close $fd
5972                        or next;
5973
5974                # print element (entry, item)
5975                my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5976                if ($format eq 'rss') {
5977                        print "<item>\n" .
5978                              "<title>" . esc_html($co{'title'}) . "</title>\n" .
5979                              "<author>" . esc_html($co{'author'}) . "</author>\n" .
5980                              "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5981                              "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5982                              "<link>$co_url</link>\n" .
5983                              "<description>" . esc_html($co{'title'}) . "</description>\n" .
5984                              "<content:encoded>" .
5985                              "<![CDATA[\n";
5986                } elsif ($format eq 'atom') {
5987                        print "<entry>\n" .
5988                              "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5989                              "<updated>$cd{'iso-8601'}</updated>\n" .
5990                              "<author>\n" .
5991                              "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5992                        if ($co{'author_email'}) {
5993                                print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5994                        }
5995                        print "</author>\n" .
5996                              # use committer for contributor
5997                              "<contributor>\n" .
5998                              "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5999                        if ($co{'committer_email'}) {
6000                                print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6001                        }
6002                        print "</contributor>\n" .
6003                              "<published>$cd{'iso-8601'}</published>\n" .
6004                              "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6005                              "<id>$co_url</id>\n" .
6006                              "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6007                              "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6008                }
6009                my $comment = $co{'comment'};
6010                print "<pre>\n";
6011                foreach my $line (@$comment) {
6012                        $line = esc_html($line);
6013                        print "$line\n";
6014                }
6015                print "</pre><ul>\n";
6016                foreach my $difftree_line (@difftree) {
6017                        my %difftree = parse_difftree_raw_line($difftree_line);
6018                        next if !$difftree{'from_id'};
6019
6020                        my $file = $difftree{'file'} || $difftree{'to_file'};
6021
6022                        print "<li>" .
6023                              "[" .
6024                              $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6025                                                     hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6026                                                     hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6027                                                     file_name=>$file, file_parent=>$difftree{'from_file'}),
6028                                      -title => "diff"}, 'D');
6029                        if ($have_blame) {
6030                                print $cgi->a({-href => href(-full=>1, action=>"blame",
6031                                                             file_name=>$file, hash_base=>$commit),
6032                                              -title => "blame"}, 'B');
6033                        }
6034                        # if this is not a feed of a file history
6035                        if (!defined $file_name || $file_name ne $file) {
6036                                print $cgi->a({-href => href(-full=>1, action=>"history",
6037                                                             file_name=>$file, hash=>$commit),
6038                                              -title => "history"}, 'H');
6039                        }
6040                        $file = esc_path($file);
6041                        print "] ".
6042                              "$file</li>\n";
6043                }
6044                if ($format eq 'rss') {
6045                        print "</ul>]]>\n" .
6046                              "</content:encoded>\n" .
6047                              "</item>\n";
6048                } elsif ($format eq 'atom') {
6049                        print "</ul>\n</div>\n" .
6050                              "</content>\n" .
6051                              "</entry>\n";
6052                }
6053        }
6054
6055        # end of feed
6056        if ($format eq 'rss') {
6057                print "</channel>\n</rss>\n";
6058        }       elsif ($format eq 'atom') {
6059                print "</feed>\n";
6060        }
6061}
6062
6063sub git_rss {
6064        git_feed('rss');
6065}
6066
6067sub git_atom {
6068        git_feed('atom');
6069}
6070
6071sub git_opml {
6072        my @list = git_get_projects_list();
6073
6074        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6075        print <<XML;
6076<?xml version="1.0" encoding="utf-8"?>
6077<opml version="1.0">
6078<head>
6079  <title>$site_name OPML Export</title>
6080</head>
6081<body>
6082<outline text="git RSS feeds">
6083XML
6084
6085        foreach my $pr (@list) {
6086                my %proj = %$pr;
6087                my $head = git_get_head_hash($proj{'path'});
6088                if (!defined $head) {
6089                        next;
6090                }
6091                $git_dir = "$projectroot/$proj{'path'}";
6092                my %co = parse_commit($head);
6093                if (!%co) {
6094                        next;
6095                }
6096
6097                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6098                my $rss  = "$my_url?p=$proj{'path'};a=rss";
6099                my $html = "$my_url?p=$proj{'path'};a=summary";
6100                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6101        }
6102        print <<XML;
6103</outline>
6104</body>
6105</opml>
6106XML
6107}