general improvements
authorAndrew Lorimer <andrew@lorimer.id.au>
Tue, 21 Jul 2020 08:50:49 +0000 (18:50 +1000)
committerAndrew Lorimer <andrew@lorimer.id.au>
Tue, 21 Jul 2020 08:50:49 +0000 (18:50 +1000)
gitweb/gitweb.perl
index 49ed13b9dea06618cdc87481f55613c3cd7f6191..6ddde75deec8b7f974495d7b5fde4fcde78720a6 100755 (executable)
 use warnings;
 # handle ACL in file access tests
 use filetest 'access';
+use HTML::Entities;
+use Pandoc;
 use CGI qw(:standard :escapeHTML -nosticky);
 use CGI::Util qw(unescape);
-#use CGI::Carp qw(fatalsToBrowser set_message);
-use CGI::Carp qw(set_message);
+#use CGI::Carp qw(fatalsToBrowser set_message);  # debugging only
+use CGI::Carp qw(set_message);                 # production
 use Encode;
 use Fcntl ':mode';
 use File::Find qw();
@@ -85,6 +87,9 @@ sub evaluate_uri {
 # this can just be "git" if your webserver has a sensible PATH
 our $GIT = "++GIT_BINDIR++/git";
 
+# executable path to Pandoc executable for rendering readme files etc
+our $PANDOC = "pandoc";
+
 # absolute fs-path which will be prepended to the project path
 #our $projectroot = "/pub/scm";
 our $projectroot = "++GITWEB_PROJECTROOT++";
@@ -608,6 +613,37 @@ sub evaluate_uri {
                'sub' => \&feature_extra_branch_refs,
                'override' => 0,
                'default' => []},
+
+        # Enable or disable RSS feed
+        # Added by Andrew Lorimer, October 2019
+        
+        'rss' => {
+                'sub' => \&feature_bool,
+                'override' => 0,
+                'default' => [1]},
+
+        # Enable or disable Atom feed
+        # Added by Andrew Lorimer, October 2019
+        
+        'atom' => {
+                'sub' => \&feature_bool,
+                'override' => 0,
+                'default' => [1]},
+
+
+        # Enable or disable the shortlog view
+        # Added by Andrew Lorimer, November 2019
+        'shortlog' => {
+                'sub' => \&feature_bool,
+                'override' => 0,
+                'default' => [1]},
+
+        # Show author name for every commit in main commit list
+        # Added by Andrew Lorimer, January 2020
+        'summary_omit_author' => {
+                'sub' => \&feature_bool,
+                'override' => 0,
+                'default' => [1]},
 );
 
 sub gitweb_get_feature {
@@ -1488,7 +1524,7 @@ sub href {
                        }
 
                        $href .= esc_path_info($params{'hash_base'});
-                       if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
+                        if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
                                $href .= ":/".esc_path_info($params{'file_name'});
                                delete $params{'file_name'};
                        }
@@ -2110,7 +2146,7 @@ sub format_log_line_html {
        # Potentially abbreviated OID.
        my $regex = oid_nlen_regex("7,64");
 
-       $line = esc_html($line, -nbsp=>1);
+       $line = esc_html($line);
        $line =~ s{
         \b
         (
@@ -2309,6 +2345,7 @@ sub format_search_author {
 # the author name is chopped and escaped according to the other
 # optional parameters (see chop_str).
 sub format_author_html {
+    if (not gitweb_check_feature('summary_omit_author')) {
        my $tag = shift;
        my $co = shift;
        my $author = chop_and_escape_str($co->{'author_name'}, @_);
@@ -2317,6 +2354,9 @@ sub format_author_html {
                       git_get_avatar($co->{'author_email'}, -pad_after => 1) .
                       $author) .
               "</$tag>";
+    } else {
+      return "";
+    }
 }
 
 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
@@ -3586,7 +3626,6 @@ sub parse_commit_text {
        foreach my $title (@commit_lines) {
                $title =~ s/^    //;
                if ($title ne "") {
-                        #$co{'title'} = chop_str($title, 80, 5);
                         $co{'title'} = $title;
                        # remove leading stuff of merges to make the interesting part visible
                        if (length($title) > 50) {
@@ -3605,8 +3644,7 @@ sub parse_commit_text {
                                        $title =~ s/\/pub\/scm//;
                                }
                        }
-                        #$co{'title_short'} = chop_str($title, 50, 5);
-                        $co{'title_short'} = $title;
+                        $co{'title_short'} = chop_str($title, 50, 5);
                        last;
                }
        }
@@ -3615,7 +3653,8 @@ sub parse_commit_text {
        }
        # remove added spaces
        foreach my $line (@commit_lines) {
-               $line =~ s/^    //;
+               $line =~ s/\s\s+//;
+                $line =~ s/\n+$//;
        }
        $co{'comment'} = \@commit_lines;
 
@@ -3937,6 +3976,13 @@ sub insert_file {
        close $fd;
 }
 
+# Check if a file should be displayed
+sub check_tree_filter {
+  my $filename = shift;
+  my @treefilter_pattern = gitweb_get_feature('tree_filter');
+  return $filename =~ /$treefilter_pattern[0]/;
+}
+
 ## ......................................................................
 ## mimetype related functions
 
@@ -4097,33 +4143,10 @@ sub print_feed_meta {
                        $href_params{'-title'} = 'log';
                }
 
-               foreach my $format (qw(RSS Atom)) {
-                       my $type = lc($format);
-                       my %link_attr = (
-                               '-rel' => 'alternate',
-                               '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
-                               '-type' => "application/$type+xml"
-                       );
-
-                       $href_params{'extra_options'} = undef;
-                       $href_params{'action'} = $type;
-                       $link_attr{'-href'} = href(%href_params);
-                       print "<link ".
-                             "rel=\"$link_attr{'-rel'}\" ".
-                             "title=\"$link_attr{'-title'}\" ".
-                             "href=\"$link_attr{'-href'}\" ".
-                             "type=\"$link_attr{'-type'}\" ".
-                             "/>\n";
-
-                       $href_params{'extra_options'} = '--no-merges';
-                       $link_attr{'-href'} = href(%href_params);
-                       $link_attr{'-title'} .= ' (no merges)';
-                       print "<link ".
-                             "rel=\"$link_attr{'-rel'}\" ".
-                             "title=\"$link_attr{'-title'}\" ".
-                             "href=\"$link_attr{'-href'}\" ".
-                             "type=\"$link_attr{'-type'}\" ".
-                             "/>\n";
+               foreach my $format (qw("RSS Atom")) {
+                  if (gitweb_check_feature($format)) {
+                    print_feed_links($format, %href_params);
+                  }
                }
 
        } else {
@@ -4136,6 +4159,39 @@ sub print_feed_meta {
        }
 }
 
+sub print_feed_links {
+
+  my ($format, %href_params) = @_;
+
+  my $type = lc($format);
+  my %link_attr = (
+    '-rel' => 'alternate',
+    '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
+    '-type' => "application/$type+xml"
+  );
+
+  $href_params{'extra_options'} = undef;
+  $href_params{'action'} = $type;
+  $link_attr{'-href'} = href(%href_params);
+  print "<link ".
+  "rel=\"$link_attr{'-rel'}\" ".
+  "title=\"$link_attr{'-title'}\" ".
+  "href=\"$link_attr{'-href'}\" ".
+  "type=\"$link_attr{'-type'}\" ".
+  "/>\n";
+
+  $href_params{'extra_options'} = '--no-merges';
+  $link_attr{'-href'} = href(%href_params);
+  $link_attr{'-title'} .= ' (no merges)';
+  print "<link ".
+  "rel=\"$link_attr{'-rel'}\" ".
+  "title=\"$link_attr{'-title'}\" ".
+  "href=\"$link_attr{'-href'}\" ".
+  "type=\"$link_attr{'-type'}\" ".
+  "/>\n";
+
+}
+
 sub print_header_links {
        my $status = shift;
 
@@ -4193,8 +4249,11 @@ sub print_nav_breadcrumbs {
                my $projectbasename = pop @dirname;
                print_nav_breadcrumbs_path(@dirname);
                print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
-               if (defined $action) {
+               if (defined $action && $action ne "summary") {
                         $action =~ s/_/ /;
+                        if ($action eq "blob") {
+                          $action = $file_name;
+                        }
                        my $action_print = $action ;
                        if (defined $opts{-action_extra}) {
                                $action_print = $cgi->a({-href => href(action=>$action)},
@@ -4335,11 +4394,17 @@ sub git_footer_html {
                }
                $href_params{'-title'} ||= 'log';
 
-               foreach my $format (qw(RSS Atom)) {
-                       $href_params{'action'} = lc($format);
+                if (gitweb_check_feature("rss")) {
+                       $href_params{'action'} = "rss";
+                       print $cgi->a({-href => href(%href_params),
+                                     -title => "$href_params{'-title'} RSS feed",
+                                     -class => $feed_class}, "RSS")."\n";
+               }
+                if (gitweb_check_feature("atom")) {
+                       $href_params{'action'} = "atom";
                        print $cgi->a({-href => href(%href_params),
-                                     -title => "$href_params{'-title'} $format feed",
-                                     -class => $feed_class}, $format)."\n";
+                                     -title => "$href_params{'-title'} Atom feed",
+                                     -class => $feed_class}, "Atom")."\n";
                }
 
        } else {
@@ -4453,7 +4518,12 @@ sub git_print_page_nav {
        my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
        $extra = '' if !defined $extra; # pager or formats
 
-       my @navs = qw(summary shortlog log commit diff tree);
+        # Check if shortlog should be added to breadcrumb
+        # Added by Andrew Lorimer, November 2019
+        my @navs = qw(summary log commit diff tree);
+        if (gitweb_check_feature("shortlog")) {
+          @navs = qw(summary shortlog log commit diff tree);
+        }
        if ($suppress) {
                @navs = grep { $_ ne $suppress } @navs;
        }
@@ -4636,11 +4706,13 @@ sub git_print_authorship {
        my $author = $co->{'author_name'};
 
        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
-       print "<$tag class=\"author_date\">" .
-             format_search_author($author, "author", esc_html($author)) .
-             " [".format_timestamp_html(\%ad)."]".
-             git_get_avatar($co->{'author_email'}, -pad_before => 1) .
-             "</$tag>\n";
+       print("<$tag class=\"author\">");
+        print(git_get_avatar($co->{'author_email'}, -pad_before => 1));
+       print(format_search_author($author, "author", esc_html($author)));
+        print("</$tag>\n");
+        print("<$tag class=\"age\">");
+       print(format_timestamp_html(\%ad));
+        print("</$tag>\n");
 }
 
 # Outputs table rows containing the full author or committer information,
@@ -4761,6 +4833,8 @@ sub git_path_header {
         return $output;
 }
 
+
+# Print the log output for a single commit
 sub git_print_log {
        my $log = shift;
        my %opts = @_;
@@ -6057,7 +6131,9 @@ sub git_log_body {
        # uses global variable $project
        my ($commitlist, $from, $to, $refs, $extra) = @_;
 
-       $from = 0 unless defined $from;
+        if (not defined $from) {
+          my $from = 0;
+        }
        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
        for (my $i = 0; $i <= $to; $i++) {
@@ -6065,23 +6141,28 @@ sub git_log_body {
                next if !%co;
                my $commit = $co{'id'};
                my $ref = format_ref_marker($refs, $commit);
-               git_print_header_div('commit',
-                              "<span class=\"age\">$co{'age_string'}</span>" .
-                              esc_html($co{'title'}), $ref,
-                              $commit);
-               print "<div class=\"title_text\">\n" .
-                     "<div class=\"log_link\">\n" .
+
+                print("<div class=\"log_commit\">\n");
+               print "<div class=\"log_link\">\n" .
                      $cgi->a({-href => href(action=>"diff", hash=>$commit)}, "diff") .
                      " | " .
                      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
                      "<br/>\n" .
                      "</div>\n";
-                     git_print_authorship(\%co, -tag => 'span');
-                     print "<br/>\n</div>\n";
-
-               print "<div class=\"log_body\">\n";
-               git_print_log($co{'comment'}, -final_empty_line=> 1);
-               print "</div>\n";
+                print("<span class=\"commit_title\">");
+               print format_subject_html($co{'title'}, $co{'title_short'},
+                  href(action=>"commit", hash=>$commit), $ref);
+                print("</span>");
+                git_print_authorship(\%co, -tag => 'span');
+                print("<p class=\"commit_message\">");
+                git_print_log($co{'comment'}, -final_empty_line=>1);
+                print("</p>");
+                      #      print "<br/>\n</div>\n";
+
+                #print "<div class=\"log_body\">\n";
+                #git_print_log($co{'comment'}, -final_empty_line=> 1);
+                #print "</div>\n";
+                print("</div>");
        }
        if ($extra) {
                print "<nav>\n";
@@ -6094,21 +6175,18 @@ sub git_shortlog_body {
        # uses global variable $project
        my ($commitlist, $from, $to, $refs, $extra) = @_;
 
-       $from = 0 unless defined $from;
+        if (not defined $from) {
+          my $from = 0;
+        }
        $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
+        print "<h3>Commits</h3>";
        print "<table class=\"shortlog\">\n";
-       my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
                my %co = %{$commitlist->[$i]};
                my $commit = $co{'id'};
                my $ref = format_ref_marker($refs, $commit);
-               if ($alternate) {
-                       print "<tr class=\"dark\">\n";
-               } else {
-                       print "<tr class=\"light\">\n";
-               }
-               $alternate ^= 1;
+                print("<tr>");
                print "<td class=\"age\" title=\"$co{'age_string_age'}\">$co{'age_string_date'}</td>\n" .
                       "<td class=\"separator\">&#8226;</td>" .
                      format_author_html('td', \%co, 10) . "<td>";
@@ -6202,6 +6280,7 @@ sub git_tags_body {
        $from = 0 unless defined $from;
        $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
 
+        print "<h3>Tags</h3>";
        print "<table class=\"tags\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
@@ -6265,6 +6344,7 @@ sub git_heads_body {
        $from = 0 unless defined $from;
        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
 
+        print "<h3>Branches</h3>";
        print "<table class=\"heads\">\n";
        my $alternate = 1;
        for (my $i = $from; $i <= $to; $i++) {
@@ -6815,11 +6895,44 @@ sub git_summary {
 
        # If XSS prevention is on, we don't include README.html.
        # TODO: Allow a readme in some safe format.
-       if (!$prevent_xss && -s "$projectroot/$project/README.html") {
-               print "<div class=\"title\">readme</div>\n" .
-                     "<div class=\"readme\">\n";
-               insert_file("$projectroot/$project/README.html");
-               print "\n</div>\n"; # class="readme"
+       if (!$prevent_xss) {
+          open my $fd, "-|", git_cmd(), "ls-tree", "master", "--name-only"
+            or die_error(500, "Error checking ls_tree for readme");
+          my @readme_files = map { chomp; $_ } <$fd>;
+          close $fd
+            or die_error(500, "Error checking ls_tree for readme");
+          if (@readme_files) {
+            $hash_base ||= git_get_head_hash($project);
+            foreach my $readme_file (@readme_files) {
+              if ($readme_file =~ /^readme($|.+)/i && check_tree_filter($readme_file)) {
+                my $hash = git_get_hash_by_path($hash_base, $readme_file)
+                  or die_error(500, "Error getting hash for readme file $readme_file");
+                print "<div class=\"title readme-title\">$readme_file<nav class=\"formats_nav\">";
+                print $cgi->a({-href => href(action=>"blob", hash=>$hash)}, "source") . " | ";
+                print $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw");
+                print "</nav>";
+                print "</div>\n" .
+                  "<div class=\"readme\">\n";
+                open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+                  or die_error(500, "Cat of readme '$hash' failed");
+                my $filecontents = "";
+               while (my $line = <$fd>) {
+                  $filecontents .= $line;
+                }
+
+                # Process markdown if necessary
+                if ($readme_file =~ /\.md$/i && check_pandoc()) {
+                  $filecontents = pandoc->parse('markdown' => $filecontents)->to_html;
+                }
+                print($filecontents);
+
+                close $fd
+                  or die_error(500, "Error processing readme file $hash");
+                print "\n</div>\n"; # class="readme"
+                last;
+              }
+            }
+          }
        }
 
        # we need to request one more than 16 (0..15) to check if
@@ -6862,6 +6975,17 @@ sub git_summary {
        git_footer_html();
 }
 
+
+# Check if Pandoc module is installed, and import it if it is available.
+# This is a bit hacky, but Pandoc rendering is considered an "optional" 
+# feature, so we need to make sure no errors are thrown if it's not available.
+# Added by Andrew Lorimer, October 2019
+
+sub check_pandoc {
+  eval "use Pandoc; 1" or return 0;
+  return 1;
+}
+
 sub git_tag {
        my %tag = parse_tag($hash);
 
@@ -7217,12 +7341,10 @@ sub git_blob_plain {
                # blobs defined by non-textual hash id's can be cached
                $expires = "+1d";
        }
-        my @treefilter_pattern = gitweb_get_feature('tree_filter');
-        if (not $file_name =~ /@treefilter_pattern/) {
+        if (! check_tree_filter($file_name)) {
           die_error(403, "Access denied ($file_name)");
         }
 
-
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(500, "Open git-cat-file blob '$hash' failed");
 
@@ -7284,8 +7406,7 @@ sub git_blob {
                # blobs defined by non-textual hash id's can be cached
                $expires = "+1d";
        }
-        my @treefilter_pattern = gitweb_get_feature('tree_filter');
-        if (not $file_name =~ /@treefilter_pattern/) {
+        if (! check_tree_filter($file_name)) {
           die_error(403, "Access denied ($file_name)");
         }
 
@@ -7438,8 +7559,11 @@ sub git_tree {
            defined $file_name && $file_name =~ m![^/]+$!) {
                 print "<tr>\n";
                my $up = $file_name;
-               $up =~ s!/?[^/]+$!!;
-               undef $up unless $up;
+                #$up =~ s!/?[^/]+$!!;
+                $up =~ s/.(?<=(\/|^.))[^\/]*\/?$//;
+                #if ($up eq "") {
+                #  $hash_base .= ":";
+                #}
                # based on git_print_tree_entry
                 print '<td class="mode">';
                 print '<svg width="14" height="14" '
@@ -7451,20 +7575,19 @@ sub git_tree {
                print $cgi->a({-href => href(action=>"tree",
                                             hash_base=>$hash_base,
                                             file_name=>$up)},
-                             "parent");
+                             "parent (dest: $up; base: $hash_base)");
                print "</td>\n";
                print "<td class=\"link\"></td>\n";
 
                print "</tr>\n";
        }
-        my @treefilter_pattern = gitweb_get_feature('tree_filter');
        foreach my $line (@entries) {
-               my $t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
-                if ($t->{'name'} =~ /@treefilter_pattern/) {
-                  print "<tr>\n";
-                  git_print_tree_entry($t, $basedir, $hash_base, $have_blame);
-                  print "</tr>\n";
-                }
+          my $t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
+          if (check_tree_filter($t->{'name'})) {
+            print "<tr>\n";
+            git_print_tree_entry($t, $basedir, $hash_base, $have_blame);
+            print "</tr>\n";
+          }
        }
        print "</table>\n";
        git_footer_html();
@@ -8314,11 +8437,12 @@ sub git_search_help {
        git_header_html();
        git_print_page_nav('','', $hash,$hash,$hash);
        print <<EOT;
-<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
-regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
-the pattern entered is recognized as the POSIX extended
+<p>By default, the search string is matched exactly (but without regard to case, unless 
+<em>pickaxe</em> is selected). However, when you check the <em>re</em> checkbox,
+the pattern is interpreted as a POSIX extended
 <a href="https://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
 insensitive).</p>
+<h3>Search options</h3>
 <dl>
 <dt><b>commit</b></dt>
 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
@@ -8327,18 +8451,18 @@ sub git_search_help {
        if ($have_grep) {
                print <<EOT;
 <dt><b>grep</b></dt>
-<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
-    a different one) are searched for the given pattern. On large trees, this search can take
+<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing a different one) 
+are searched for the given pattern. On large trees, this search can take
 a while and put some strain on the server, so please use it with some consideration. Note that
-due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
+due to a git-grep peculiarity, currently if regex mode is turned off, the matches are
 case-sensitive.</dd>
 EOT
        }
        print <<EOT;
 <dt><b>author</b></dt>
-<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
+<dd>Name and email of the author and date of the patch will be scanned for the given pattern.</dd>
 <dt><b>committer</b></dt>
-<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
+<dd>Name and email of the committer and date of commit will be scanned for the given pattern.</dd>
 EOT
        my $have_pickaxe = gitweb_check_feature('pickaxe');
        if ($have_pickaxe) {
@@ -8371,6 +8495,9 @@ sub git_feed {
        if ($format ne 'rss' && $format ne 'atom') {
                die_error(400, "Unknown web feed format");
        }
+        if (not gitweb_check_feature($format)) {
+          die_error(403, "Feed disabled");
+        }
 
        # log/feed of current (HEAD) branch, log of given branch, history of file/directory
        my $head = $hash || 'HEAD';