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