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