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