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