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