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