gitweb: Syntax highlighting support
[gitweb.git] / gitweb / gitweb.perl
index 7e477af9567cff322d18fc9a1fbfe507732418dc..de18ebf30195f43082f80e2f76dc20c0b8c061dd 100755 (executable)
@@ -221,6 +221,42 @@ BEGIN
        'double'  => 32
 );
 
+# Used to set the maximum load that we will still respond to gitweb queries.
+# If server load exceed this value then return "503 server busy" error.
+# If gitweb cannot determined server load, it is taken to be 0.
+# Leave it undefined (or set to 'undef') to turn off load checking.
+our $maxload = 300;
+
+# syntax highlighting
+our %highlight_type = (
+       # match by basename
+       'SConstruct' => 'py',
+       'Program' => 'py',
+       'Library' => 'py',
+       'Makefile' => 'make',
+       # match by extension
+       '\.py$' => 'py', # Python
+       '\.c$' => 'c',
+       '\.h$' => 'c',
+       '\.cpp$' => 'cpp',
+       '\.cxx$' => 'cpp',
+       '\.rb$' => 'ruby',
+       '\.java$' => 'java',
+       '\.css$' => 'css',
+       '\.php3?$' => 'php',
+       '\.sh$' => 'sh', # Bash / shell script
+       '\.pl$' => 'pl', # Perl
+       '\.js$' => 'js', # JavaScript
+       '\.tex$' => 'tex', # TeX and LaTeX
+       '\.bib$' => 'bib', # BibTeX
+       '\.x?html$' => 'xml',
+       '\.xml$' => 'xml',
+       '\.awk$' => 'awk',
+       '\.bat$' => 'bat', # DOS Batch script
+       '\.ini$' => 'ini',
+       '\.spec$' => 'spec', # RPM Spec
+);
+
 # You define site-wide feature defaults here; override them with
 # $GITWEB_CONFIG as necessary.
 our %feature = (
@@ -439,6 +475,19 @@ BEGIN
        'javascript-actions' => {
                'override' => 0,
                'default' => [0]},
+
+       # Syntax highlighting support. This is based on Daniel Svensson's
+       # and Sham Chukoury's work in gitweb-xmms2.git.
+       # It requires the 'highlight' program, and therefore is disabled
+       # by default.
+
+       # To enable system wide have in $GITWEB_CONFIG
+       # $feature{'highlight'}{'default'} = [1];
+
+       'highlight' => {
+               'sub' => sub { feature_bool('highlight', @_) },
+               'override' => 0,
+               'default' => [0]},
 );
 
 sub gitweb_get_feature {
@@ -448,7 +497,11 @@ sub gitweb_get_feature {
                $feature{$name}{'sub'},
                $feature{$name}{'override'},
                @{$feature{$name}{'default'}});
-       if (!$override) { return @defaults; }
+       # project specific override is possible only if we have project
+       our $git_dir; # global variable, declared later
+       if (!$override || !defined $git_dir) {
+               return @defaults;
+       }
        if (!defined $sub) {
                warn "feature $name is not overridable";
                return @defaults;
@@ -544,11 +597,36 @@ sub filter_snapshot_fmts {
 }
 
 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
+our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
+# die if there are errors parsing config file
 if (-e $GITWEB_CONFIG) {
        do $GITWEB_CONFIG;
-} else {
-       our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
-       do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
+       die $@ if $@;
+} elsif (-e $GITWEB_CONFIG_SYSTEM) {
+       do $GITWEB_CONFIG_SYSTEM;
+       die $@ if $@;
+}
+
+# Get loadavg of system, to compare against $maxload.
+# Currently it requires '/proc/loadavg' present to get loadavg;
+# if it is not present it returns 0, which means no load checking.
+sub get_loadavg {
+       if( -e '/proc/loadavg' ){
+               open my $fd, '<', '/proc/loadavg'
+                       or return 0;
+               my @load = split(/\s+/, scalar <$fd>);
+               close $fd;
+
+               # The first three columns measure CPU and IO utilization of the last one,
+               # five, and 10 minute periods.  The fourth column shows the number of
+               # currently running processes and the total number of processes in the m/n
+               # format.  The last column displays the last process ID used.
+               return $load[0] || 0;
+       }
+       # additional checks for load average should go here for things that don't export
+       # /proc/loadavg
+
+       return 0;
 }
 
 # version of the core git binary
@@ -557,6 +635,10 @@ sub filter_snapshot_fmts {
 
 $projects_list ||= $projectroot;
 
+if (defined $maxload && get_loadavg() > $maxload) {
+       die_error(503, "The load average on the server is too high");
+}
+
 # ======================================================================
 # input validation and dispatch
 
@@ -1111,6 +1193,7 @@ sub validate_refname {
 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
 sub to_utf8 {
        my $str = shift;
+       return undef unless defined $str;
        if (utf8::valid($str)) {
                utf8::decode($str);
                return $str;
@@ -1123,6 +1206,7 @@ sub to_utf8 {
 # correct, but quoted slashes look too horrible in bookmarks
 sub esc_param {
        my $str = shift;
+       return undef unless defined $str;
        $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
        $str =~ s/ /\+/g;
        return $str;
@@ -1131,6 +1215,7 @@ sub esc_param {
 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
 sub esc_url {
        my $str = shift;
+       return undef unless defined $str;
        $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
        $str =~ s/\+/%2B/g;
        $str =~ s/ /\+/g;
@@ -1142,6 +1227,8 @@ sub esc_html {
        my $str = shift;
        my %opts = @_;
 
+       return undef unless defined $str;
+
        $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
@@ -1156,6 +1243,8 @@ sub esc_path {
        my $str = shift;
        my %opts = @_;
 
+       return undef unless defined $str;
+
        $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
@@ -1298,7 +1387,6 @@ sub chop_str {
                $str =~ m/^(.*?)($begre)$/;
                my ($lead, $body) = ($1, $2);
                if (length($lead) > 4) {
-                       $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
                        $lead = " ...";
                }
                return "$lead$body";
@@ -1309,8 +1397,6 @@ sub chop_str {
                $str =~ m/^(.*?)($begre)$/;
                my ($mid, $right) = ($1, $2);
                if (length($mid) > 5) {
-                       $left  =~ s/&[^;]*$//;
-                       $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
                        $mid = " ... ";
                }
                return "$left$mid$right";
@@ -1320,7 +1406,6 @@ sub chop_str {
                my $body = $1;
                my $tail = $2;
                if (length($tail) > 4) {
-                       $body =~ s/&[^;]*$//;
                        $tail = "... ";
                }
                return "$body$tail";
@@ -2174,6 +2259,8 @@ sub config_to_multi {
 sub git_get_project_config {
        my ($key, $type) = @_;
 
+       return unless defined $git_dir;
+
        # key sanity check
        return unless ($key);
        $key =~ s/^gitweb\.//;
@@ -3222,7 +3309,7 @@ sub git_header_html {
        print "</head>\n" .
              "<body>\n";
 
-       if (-f $site_header) {
+       if (defined $site_header && -f $site_header) {
                insert_file($site_header);
        }
 
@@ -3323,12 +3410,13 @@ sub git_footer_html {
                print "</div>\n"; # class="page_footer"
        }
 
-       if (-f $site_footer) {
+       if (defined $site_footer && -f $site_footer) {
                insert_file($site_footer);
        }
 
        print qq!<script type="text/javascript" src="$javascript"></script>\n!;
-       if ($action eq 'blame_incremental') {
+       if (defined $action &&
+           $action eq 'blame_incremental') {
                print qq!<script type="text/javascript">\n!.
                      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
                      qq!           "!. href() .qq!");\n!.
@@ -3343,7 +3431,7 @@ sub git_footer_html {
              "</html>";
 }
 
-# die_error(<http_status_code>, <error_message>)
+# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
 # Example: die_error(404, 'Hash not found')
 # By convention, use the following status codes (as defined in RFC 2616):
 # 400: Invalid or missing CGI parameters, or
@@ -3354,22 +3442,33 @@ sub git_footer_html {
 # 500: The server isn't configured properly, or
 #      an internal error occurred (e.g. failed assertions caused by bugs), or
 #      an unknown error occurred (e.g. the git binary died unexpectedly).
+# 503: The server is currently unavailable (because it is overloaded,
+#      or down for maintenance).  Generally, this is a temporary state.
 sub die_error {
        my $status = shift || 500;
-       my $error = shift || "Internal server error";
-
-       my %http_responses = (400 => '400 Bad Request',
-                             403 => '403 Forbidden',
-                             404 => '404 Not Found',
-                             500 => '500 Internal Server Error');
+       my $error = esc_html(shift) || "Internal Server Error";
+       my $extra = shift;
+
+       my %http_responses = (
+               400 => '400 Bad Request',
+               403 => '403 Forbidden',
+               404 => '404 Not Found',
+               500 => '500 Internal Server Error',
+               503 => '503 Service Unavailable',
+       );
        git_header_html($http_responses{$status});
        print <<EOF;
 <div class="page_body">
 <br /><br />
 $status - $error
 <br />
-</div>
 EOF
+       if (defined $extra) {
+               print "<hr />\n" .
+                     "$extra\n";
+       }
+       print "</div>\n";
+
        git_footer_html();
        exit;
 }
@@ -3471,14 +3570,21 @@ sub git_print_header_div {
 }
 
 sub print_local_time {
+       print format_local_time(@_);
+}
+
+sub format_local_time {
+       my $localtime = '';
        my %date = @_;
        if ($date{'hour_local'} < 6) {
-               printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
+               $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
        } else {
-               printf(" (%02d:%02d %s)",
+               $localtime .= sprintf(" (%02d:%02d %s)",
                        $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
        }
+
+       return $localtime;
 }
 
 # Outputs the author name and date in long form
@@ -4302,17 +4408,24 @@ sub fill_project_list_info {
 # print 'sort by' <th> element, generating 'sort by $name' replay link
 # if that order is not selected
 sub print_sort_th {
+       print format_sort_th(@_);
+}
+
+sub format_sort_th {
        my ($name, $order, $header) = @_;
+       my $sort_th = "";
        $header ||= ucfirst($name);
 
        if ($order eq $name) {
-               print "<th>$header</th>\n";
+               $sort_th .= "<th>$header</th>\n";
        } else {
-               print "<th>" .
-                     $cgi->a({-href => href(-replay=>1, order=>$name),
-                              -class => "header"}, $header) .
-                     "</th>\n";
+               $sort_th .= "<th>" .
+                           $cgi->a({-href => href(-replay=>1, order=>$name),
+                                    -class => "header"}, $header) .
+                           "</th>\n";
        }
+
+       return $sort_th;
 }
 
 sub git_project_list_body {
@@ -4743,7 +4856,7 @@ sub git_project_list {
        }
 
        git_header_html();
-       if (-f $home_text) {
+       if (defined $home_text && -f $home_text) {
                print "<div class=\"index_include\">\n";
                insert_file($home_text);
                print "</div>\n";
@@ -5276,6 +5389,7 @@ sub git_blob {
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(500, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
+       # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
                close $fd;
                return git_blob_plain($mimetype);
@@ -5283,6 +5397,25 @@ sub git_blob {
        # we can have blame only for text/* mimetype
        $have_blame &&= ($mimetype =~ m!^text/!);
 
+       my $have_highlight = gitweb_check_feature('highlight');
+       my $syntax;
+       if ($have_highlight && defined($file_name)) {
+               my $basename = basename($file_name, '.in');
+               foreach my $regexp (keys %highlight_type) {
+                       if ($basename =~ /$regexp/) {
+                               $syntax = $highlight_type{$regexp};
+                               last;
+                       }
+               }
+
+               if ($syntax) {
+                       close $fd;
+                       open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
+                                 "highlight --xhtml --fragment -t 8 --syntax $syntax |"
+                               or die_error(500, "Couldn't open file or run syntax highlighter");
+               }
+       }
+
        git_header_html(undef, $expires);
        my $formats_nav = '';
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
@@ -5334,7 +5467,7 @@ sub git_blob {
                        $line = untabify($line);
                        printf "<div class=\"pre\"><a id=\"l%i\" href=\"" . href(-replay => 1)
                                . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
-                              $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+                              $nr, $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
                }
        }
        close $fd