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