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