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