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