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