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