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