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