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