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