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