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