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