X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/d003f6928cacd7aa42f6a5e9ea285373c548c81d6da67818975245db047d3849..f6ebec3087bb9b2307b389bcb45ace362932f058c7ca6ac5001ea7fec3b89cce:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index d3950f5..0a8c9e2 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -27,6 +27,13 @@ our $version = "++GIT_VERSION++"; our $my_url = $cgi->url(); our $my_uri = $cgi->url(-absolute => 1); +# if we're called with PATH_INFO, we have to strip that +# from the URL to find our real URL +if (my $path_info = $ENV{"PATH_INFO"}) { + $my_url =~ s,\Q$path_info\E$,,; + $my_uri =~ s,\Q$path_info\E$,,; +} + # core git executable to use # this can just be "git" if your webserver has a sensible PATH our $GIT = "++GIT_BINDIR++/git"; @@ -276,6 +283,44 @@ our %feature = ( 'forks' => { 'override' => 0, 'default' => [0]}, + + # Insert custom links to the action bar of all project pages. + # This enables you mainly to link to third-party scripts integrating + # into gitweb; e.g. git-browser for graphical history representation + # or custom web-based repository administration interface. + + # The 'default' value consists of a list of triplets in the form + # (label, link, position) where position is the label after which + # to inster the link and link is a format string where %n expands + # to the project name, %f to the project path within the filesystem, + # %h to the current hash (h gitweb parameter) and %b to the current + # hash base (hb gitweb parameter). + + # To enable system wide have in $GITWEB_CONFIG e.g. + # $feature{'actions'}{'default'} = [('graphiclog', + # '/git-browser/by-commit.html?r=%n', 'summary')]; + # Project specific override is not supported. + 'actions' => { + 'override' => 0, + 'default' => []}, + + # Allow gitweb scan project content tags described in ctags/ + # of project repository, and display the popular Web 2.0-ish + # "tag cloud" near the project list. Note that this is something + # COMPLETELY different from the normal Git tags. + + # gitweb by itself can show existing tags, but it does not handle + # tagging itself; you need an external application for that. + # For an example script, check Girocco's cgi/tagproj.cgi. + # You may want to install the HTML::TagCloud Perl module to get + # a pretty tag cloud instead of just a list of tags. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'ctags'}{'default'} = ['path_to_tag_script']; + # Project specific override is not supported. + 'ctags' => { + 'override' => 0, + 'default' => [0]}, ); sub gitweb_check_feature { @@ -378,7 +423,7 @@ if (-e $GITWEB_CONFIG) { } # version of the core git binary -our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown"; +our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown"; $projects_list ||= $projectroot; @@ -387,7 +432,7 @@ $projects_list ||= $projectroot; our $action = $cgi->param('a'); if (defined $action) { if ($action =~ m/[^0-9a-zA-Z\.\-_]/) { - die_error(undef, "Invalid action parameter"); + die_error(400, "Invalid action parameter"); } } @@ -400,21 +445,21 @@ if (defined $project) { ($export_ok && !(-e "$projectroot/$project/$export_ok")) || ($strict_export && !project_in_list($project))) { undef $project; - die_error(undef, "No such project"); + die_error(404, "No such project"); } } our $file_name = $cgi->param('f'); if (defined $file_name) { if (!validate_pathname($file_name)) { - die_error(undef, "Invalid file parameter"); + die_error(400, "Invalid file parameter"); } } our $file_parent = $cgi->param('fp'); if (defined $file_parent) { if (!validate_pathname($file_parent)) { - die_error(undef, "Invalid file parent parameter"); + die_error(400, "Invalid file parent parameter"); } } @@ -422,21 +467,21 @@ if (defined $file_parent) { our $hash = $cgi->param('h'); if (defined $hash) { if (!validate_refname($hash)) { - die_error(undef, "Invalid hash parameter"); + die_error(400, "Invalid hash parameter"); } } our $hash_parent = $cgi->param('hp'); if (defined $hash_parent) { if (!validate_refname($hash_parent)) { - die_error(undef, "Invalid hash parent parameter"); + die_error(400, "Invalid hash parent parameter"); } } our $hash_base = $cgi->param('hb'); if (defined $hash_base) { if (!validate_refname($hash_base)) { - die_error(undef, "Invalid hash base parameter"); + die_error(400, "Invalid hash base parameter"); } } @@ -448,10 +493,10 @@ our @extra_options = $cgi->param('opt'); if (defined @extra_options) { foreach my $opt (@extra_options) { if (not exists $allowed_options{$opt}) { - die_error(undef, "Invalid option parameter"); + die_error(400, "Invalid option parameter"); } if (not grep(/^$action$/, @{$allowed_options{$opt}})) { - die_error(undef, "Invalid option parameter for this action"); + die_error(400, "Invalid option parameter for this action"); } } } @@ -459,7 +504,7 @@ if (defined @extra_options) { our $hash_parent_base = $cgi->param('hpb'); if (defined $hash_parent_base) { if (!validate_refname($hash_parent_base)) { - die_error(undef, "Invalid hash parent base parameter"); + die_error(400, "Invalid hash parent base parameter"); } } @@ -467,14 +512,14 @@ if (defined $hash_parent_base) { our $page = $cgi->param('pg'); if (defined $page) { if ($page =~ m/[^0-9]/) { - die_error(undef, "Invalid page parameter"); + die_error(400, "Invalid page parameter"); } } our $searchtype = $cgi->param('st'); if (defined $searchtype) { if ($searchtype =~ m/[^a-z]/) { - die_error(undef, "Invalid searchtype parameter"); + die_error(400, "Invalid searchtype parameter"); } } @@ -484,7 +529,7 @@ our $searchtext = $cgi->param('s'); our $search_regexp; if (defined $searchtext) { if (length($searchtext) < 2) { - die_error(undef, "At least two characters are required for search parameter"); + die_error(403, "At least two characters are required for search parameter"); } $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext; } @@ -540,7 +585,7 @@ $git_dir = "$projectroot/$project" if $project; # dispatch my %actions = ( - "blame" => \&git_blame2, + "blame" => \&git_blame, "blobdiff" => \&git_blobdiff, "blobdiff_plain" => \&git_blobdiff_plain, "blob" => \&git_blob, @@ -581,11 +626,11 @@ if (!defined $action) { } } if (!defined($actions{$action})) { - die_error(undef, "Unknown action"); + die_error(400, "Unknown action"); } if ($action !~ m/^(opml|project_list|project_index)$/ && !$project) { - die_error(undef, "Project needed"); + die_error(400, "Project needed"); } $actions{$action}->(); exit; @@ -776,7 +821,7 @@ sub quot_cec { ); my $chr = ( (exists $es{$cntrl}) ? $es{$cntrl} - : sprintf('\%03o', ord($cntrl)) ); + : sprintf('\%2x', ord($cntrl)) ); if ($opts{-nohtml}) { return $chr; } else { @@ -1091,13 +1136,23 @@ sub format_log_line_html { } # format marker of refs pointing to given object + +# the destination action is chosen based on object type and current context: +# - for annotated tags, we choose the tag view unless it's the current view +# already, in which case we go to shortlog view +# - for other refs, we keep the current view if we're in history, shortlog or +# log view, and select shortlog otherwise sub format_ref_marker { my ($refs, $id) = @_; my $markers = ''; if (defined $refs->{$id}) { foreach my $ref (@{$refs->{$id}}) { + # this code exploits the fact that non-lightweight tags are the + # only indirect objects, and that they are the only objects for which + # we want to use tag instead of shortlog as action my ($type, $name) = qw(); + my $indirect = ($ref =~ s/\^\{\}$//); # e.g. tags/v2.6.11 or heads/next if ($ref =~ m!^(.*?)s?/(.*)$!) { $type = $1; @@ -1107,8 +1162,29 @@ sub format_ref_marker { $name = $ref; } - $markers .= " " . - esc_html($name) . ""; + my $class = $type; + $class .= " indirect" if $indirect; + + my $dest_action = "shortlog"; + + if ($indirect) { + $dest_action = "tag" unless $action eq "tag"; + } elsif ($action =~ /^(history|(short)?log)$/) { + $dest_action = $action; + } + + my $dest = ""; + $dest .= "refs/" unless $ref =~ m!^refs/!; + $dest .= $ref; + + my $link = $cgi->a({ + -href => href( + action=>$dest_action, + hash=>$dest + )}, $name); + + $markers .= " " . + $link . ""; } } @@ -1501,9 +1577,13 @@ sub git_cmd { return $GIT, '--git-dir='.$git_dir; } -# returns path to the core git executable and the --git-dir parameter as string -sub git_cmd_str { - return join(' ', git_cmd()); +# quote the given arguments for passing them to the shell +# quote_command("command", "arg 1", "arg with ' and ! characters") +# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'" +# Try to avoid using this function wherever possible. +sub quote_command { + return join(' ', + map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ )); } # get HEAD ref of given project as hash @@ -1662,7 +1742,7 @@ sub git_get_hash_by_path { $path =~ s,/+$,,; open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path - or die_error(undef, "Open git-ls-tree failed"); + or die_error(500, "Open git-ls-tree failed"); my $line = <$fd>; close $fd or return undef; @@ -1721,6 +1801,67 @@ sub git_get_project_description { return $descr; } +sub git_get_project_ctags { + my $path = shift; + my $ctags = {}; + + $git_dir = "$projectroot/$path"; + foreach (<$git_dir/ctags/*>) { + open CT, $_ or next; + my $val = ; + chomp $val; + close CT; + my $ctag = $_; $ctag =~ s#.*/##; + $ctags->{$ctag} = $val; + } + $ctags; +} + +sub git_populate_project_tagcloud { + my $ctags = shift; + + # First, merge different-cased tags; tags vote on casing + my %ctags_lc; + foreach (keys %$ctags) { + $ctags_lc{lc $_}->{count} += $ctags->{$_}; + if (not $ctags_lc{lc $_}->{topcount} + or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) { + $ctags_lc{lc $_}->{topcount} = $ctags->{$_}; + $ctags_lc{lc $_}->{topname} = $_; + } + } + + my $cloud; + if (eval { require HTML::TagCloud; 1; }) { + $cloud = HTML::TagCloud->new; + foreach (sort keys %ctags_lc) { + # Pad the title with spaces so that the cloud looks + # less crammed. + my $title = $ctags_lc{$_}->{topname}; + $title =~ s/ / /g; + $title =~ s/^/ /g; + $title =~ s/$/ /g; + $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count}); + } + } else { + $cloud = \%ctags_lc; + } + $cloud; +} + +sub git_show_project_tagcloud { + my ($cloud, $count) = @_; + print STDERR ref($cloud)."..\n"; + if (ref $cloud eq 'HTML::TagCloud') { + return $cloud->html_and_css($count); + } else { + my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud; + return '

' . join (', ', map { + "$cloud->{$_}->{topname}" + } splice(@tags, 0, $count)) . '

'; + } +} + sub git_get_project_url_list { my $path = shift; @@ -1769,9 +1910,7 @@ sub git_get_projects_list { my $subdir = substr($File::Find::name, $pfxlen + 1); # we check related file in $projectroot - if ($check_forks and $subdir =~ m#/.#) { - $File::Find::prune = 1; - } elsif (check_export_ok("$projectroot/$filter/$subdir")) { + if (check_export_ok("$projectroot/$filter/$subdir")) { push @list, { path => ($filter ? "$filter/" : '') . $subdir }; $File::Find::prune = 1; } @@ -1915,7 +2054,7 @@ sub git_get_references { while (my $line = <$fd>) { chomp $line; - if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) { + if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) { if (defined $refs{$1}) { push @{$refs{$1}}, $2; } else { @@ -2089,7 +2228,7 @@ sub parse_commit_text { last; } } - if ($co{'title'} eq "") { + if (! defined $co{'title'} || $co{'title'} eq "") { $co{'title'} = $co{'title_short'} = '(no commit message)'; } # remove added spaces @@ -2124,7 +2263,7 @@ sub parse_commit { "--max-count=1", $commit_id, "--", - or die_error(undef, "Open git-rev-list failed"); + or die_error(500, "Open git-rev-list failed"); %co = parse_commit_text(<$fd>, 1); close $fd; @@ -2149,7 +2288,7 @@ sub parse_commits { $commit_id, "--", ($filename ? ($filename) : ()) - or die_error(undef, "Open git-rev-list failed"); + or die_error(500, "Open git-rev-list failed"); while (my $line = <$fd>) { my %co = parse_commit_text($line); push @cos, \%co; @@ -2159,49 +2298,6 @@ sub parse_commits { return wantarray ? @cos : \@cos; } -# parse ref from ref_file, given by ref_id, with given type -sub parse_ref { - my $ref_file = shift; - my $ref_id = shift; - my $type = shift || git_get_type($ref_id); - my %ref_item; - - $ref_item{'type'} = $type; - $ref_item{'id'} = $ref_id; - $ref_item{'epoch'} = 0; - $ref_item{'age'} = "unknown"; - if ($type eq "tag") { - my %tag = parse_tag($ref_id); - $ref_item{'comment'} = $tag{'comment'}; - if ($tag{'type'} eq "commit") { - my %co = parse_commit($tag{'object'}); - $ref_item{'epoch'} = $co{'committer_epoch'}; - $ref_item{'age'} = $co{'age_string'}; - } elsif (defined($tag{'epoch'})) { - my $age = time - $tag{'epoch'}; - $ref_item{'epoch'} = $tag{'epoch'}; - $ref_item{'age'} = age_string($age); - } - $ref_item{'reftype'} = $tag{'type'}; - $ref_item{'name'} = $tag{'name'}; - $ref_item{'refid'} = $tag{'object'}; - } elsif ($type eq "commit"){ - my %co = parse_commit($ref_id); - $ref_item{'reftype'} = "commit"; - $ref_item{'name'} = $ref_file; - $ref_item{'title'} = $co{'title'}; - $ref_item{'refid'} = $ref_id; - $ref_item{'epoch'} = $co{'committer_epoch'}; - $ref_item{'age'} = $co{'age_string'}; - } else { - $ref_item{'reftype'} = $type; - $ref_item{'name'} = $ref_file; - $ref_item{'refid'} = $ref_id; - } - - return %ref_item; -} - # parse line of git-diff-tree "raw" output sub parse_difftree_raw_line { my $line = shift; @@ -2482,8 +2578,7 @@ sub blob_mimetype { return $default_blob_plain_mimetype unless $fd; if (-T $fd) { - return 'text/plain' . - ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : ''); + return 'text/plain'; } elsif (! $filename) { return 'application/octet-stream'; } elsif ($filename =~ m/\.png$/i) { @@ -2497,6 +2592,17 @@ sub blob_mimetype { } } +sub blob_contenttype { + my ($fd, $file_name, $type) = @_; + + $type ||= blob_mimetype($fd, $file_name); + if ($type eq 'text/plain' && defined $default_text_plain_charset) { + $type .= "; charset=$default_text_plain_charset"; + } + + return $type; +} + ## ====================================================================== ## functions printing HTML: header, footer, error page @@ -2624,7 +2730,7 @@ EOF print "\n"; my ($have_search) = gitweb_check_feature('search'); - if ((defined $project) && ($have_search)) { + if (defined $project && $have_search) { if (!defined $searchtext) { $searchtext = ""; } @@ -2640,16 +2746,13 @@ EOF my ($use_pathinfo) = gitweb_check_feature('pathinfo'); if ($use_pathinfo) { $action .= "/".esc_url($project); - } else { - $cgi->param("p", $project); } - $cgi->param("a", "search"); - $cgi->param("h", $search_hash); print $cgi->startform(-method => "get", -action => $action) . "
\n" . - (!$use_pathinfo && $cgi->hidden(-name => "p") . "\n") . - $cgi->hidden(-name => "a") . "\n" . - $cgi->hidden(-name => "h") . "\n" . + (!$use_pathinfo && + $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . + $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" . + $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" . $cgi->popup_menu(-name => 'st', -default => 'commit', -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) . $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) . @@ -2705,11 +2808,26 @@ sub git_footer_html { ""; } +# die_error(, ) +# 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 +# requested object exists but has wrong type. +# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on +# this server or project. +# 404: Requested object/revision/project doesn't exist. +# 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). sub die_error { - my $status = shift || "403 Forbidden"; - my $error = shift || "Malformed query, file missing or permission denied"; - - git_header_html($status); + 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'); + git_header_html($http_responses{$status}); print <

@@ -2744,20 +2862,33 @@ sub git_print_page_nav { } } } + $arg{'tree'}{'hash'} = $treehead if defined $treehead; $arg{'tree'}{'hash_base'} = $treebase if defined $treebase; + my @actions = gitweb_check_feature('actions'); + while (@actions) { + my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions)); + @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs; + # munch munch + $link =~ s#%n#$project#g; + $link =~ s#%f#$git_dir#g; + $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g; + $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g; + $arg{$label}{'_href'} = $link; + } + print "
\n" . (join " | ", map { $_ eq $current ? - $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_") + $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_") } @navs); print "
\n$extra
\n" . "
\n"; } sub format_paging_nav { - my ($action, $hash, $head, $page, $nrevs) = @_; + my ($action, $hash, $head, $page, $has_next_link) = @_; my $paging_nav; @@ -2775,7 +2906,7 @@ sub format_paging_nav { $paging_nav .= " ⋅ prev"; } - if ($nrevs >= (100 * ($page+1)-1)) { + if ($has_next_link) { $paging_nav .= " ⋅ " . $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); @@ -3553,21 +3684,25 @@ sub git_patchset_body { # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -sub git_project_list_body { - my ($projlist, $order, $from, $to, $extra, $no_header) = @_; - - my ($check_forks) = gitweb_check_feature('forks'); - +# fills project list info (age, description, owner, forks) for each +# project in the list, removing invalid projects from returned list +# NOTE: modifies $projlist, but does not remove entries from it +sub fill_project_list_info { + my ($projlist, $check_forks) = @_; my @projects; + + my $show_ctags = gitweb_check_feature('ctags'); + PROJECT: foreach my $pr (@$projlist) { - my (@aa) = git_get_last_activity($pr->{'path'}); - unless (@aa) { - next; + my (@activity) = git_get_last_activity($pr->{'path'}); + unless (@activity) { + next PROJECT; } - ($pr->{'age'}, $pr->{'age_string'}) = @aa; + ($pr->{'age'}, $pr->{'age_string'}) = @activity; if (!defined $pr->{'descr'}) { my $descr = git_get_project_description($pr->{'path'}) || ""; - $pr->{'descr_long'} = to_utf8($descr); + $descr = to_utf8($descr); + $pr->{'descr_long'} = $descr; $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5); } if (!defined $pr->{'owner'}) { @@ -3579,66 +3714,98 @@ sub git_project_list_body { ($pname !~ /\/$/) && (-d "$projectroot/$pname")) { $pr->{'forks'} = "-d $projectroot/$pname"; - } - else { + } else { $pr->{'forks'} = 0; } } + $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); push @projects, $pr; } + return @projects; +} + +# print 'sort by' element, generating 'sort by $name' replay link +# if that order is not selected +sub print_sort_th { + my ($name, $order, $header) = @_; + $header ||= ucfirst($name); + + if ($order eq $name) { + print "$header\n"; + } else { + print "" . + $cgi->a({-href => href(-replay=>1, order=>$name), + -class => "header"}, $header) . + "\n"; + } +} + +sub git_project_list_body { + # actually uses global variable $project + my ($projlist, $order, $from, $to, $extra, $no_header) = @_; + + my ($check_forks) = gitweb_check_feature('forks'); + my @projects = fill_project_list_info($projlist, $check_forks); + $order ||= $default_projects_order; $from = 0 unless defined $from; $to = $#projects if (!defined $to || $#projects < $to); + my %order_info = ( + project => { key => 'path', type => 'str' }, + descr => { key => 'descr_long', type => 'str' }, + owner => { key => 'owner', type => 'str' }, + age => { key => 'age', type => 'num' } + ); + my $oi = $order_info{$order}; + if ($oi->{'type'} eq 'str') { + @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects; + } else { + @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects; + } + + my $show_ctags = gitweb_check_feature('ctags'); + if ($show_ctags) { + my %ctags; + foreach my $p (@projects) { + foreach my $ct (keys %{$p->{'ctags'}}) { + $ctags{$ct} += $p->{'ctags'}->{$ct}; + } + } + my $cloud = git_populate_project_tagcloud(\%ctags); + print git_show_project_tagcloud($cloud, 64); + } + print "\n"; unless ($no_header) { print "\n"; if ($check_forks) { print "\n"; } - if ($order eq "project") { - @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects; - print "\n"; - } else { - print "\n"; - } - if ($order eq "descr") { - @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects; - print "\n"; - } else { - print "\n"; - } - if ($order eq "owner") { - @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects; - print "\n"; - } else { - print "\n"; - } - if ($order eq "age") { - @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects; - print "\n"; - } else { - print "\n"; - } - print "\n" . + print_sort_th('project', $order, 'Project'); + print_sort_th('descr', $order, 'Description'); + print_sort_th('owner', $order, 'Owner'); + print_sort_th('age', $order, 'Last Change'); + print "\n" . # for links "\n"; } my $alternate = 1; + my $tagfilter = $cgi->param('by_tag'); for (my $i = $from; $i <= $to; $i++) { my $pr = $projects[$i]; + + next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}}; + next if $searchtext and not $pr->{'path'} =~ /$searchtext/ + and not $pr->{'descr_long'} =~ /$searchtext/; + # Weed out forks or non-matching entries of search + if ($check_forks) { + my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#; + $forkbase="^$forkbase" if $forkbase; + next if not $searchtext and not $tagfilter and $show_ctags + and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe + } + if ($alternate) { print "\n"; } else { @@ -3957,12 +4124,12 @@ sub git_search_grep_body { sub git_project_list { my $order = $cgi->param('o'); if (defined $order && $order !~ m/none|project|descr|owner|age/) { - die_error(undef, "Unknown order parameter"); + die_error(400, "Unknown order parameter"); } my @list = git_get_projects_list(); if (!@list) { - die_error(undef, "No projects found"); + die_error(404, "No projects found"); } git_header_html(); @@ -3973,6 +4140,11 @@ sub git_project_list { close $fd; print "\n"; } + print $cgi->startform(-method => "get") . + "

Search:\n" . + $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . + "

" . + $cgi->end_form() . "\n"; git_project_list_body(\@list, $order); git_footer_html(); } @@ -3980,12 +4152,12 @@ sub git_project_list { sub git_forks { my $order = $cgi->param('o'); if (defined $order && $order !~ m/none|project|descr|owner|age/) { - die_error(undef, "Unknown order parameter"); + die_error(400, "Unknown order parameter"); } my @list = git_get_projects_list($project); if (!@list) { - die_error(undef, "No forks found"); + die_error(404, "No forks found"); } git_header_html(); @@ -4044,10 +4216,10 @@ sub git_summary { print "
 
\n"; print "
Project" . - $cgi->a({-href => href(project=>undef, order=>'project'), - -class => "header"}, "Project") . - "Description" . - $cgi->a({-href => href(project=>undef, order=>'descr'), - -class => "header"}, "Description") . - "Owner" . - $cgi->a({-href => href(project=>undef, order=>'owner'), - -class => "header"}, "Owner") . - "Last Change" . - $cgi->a({-href => href(project=>undef, order=>'age'), - -class => "header"}, "Last Change") . - "
\n" . - "\n" . - "\n"; + "\n" . + "\n"; if (defined $cd{'rfc2822'}) { - print "\n"; + print "\n"; } # use per project git URL list in $projectroot/$project/cloneurl @@ -4057,9 +4229,23 @@ sub git_summary { @url_list = map { "$_/$project" } @git_base_url_list unless @url_list; foreach my $git_url (@url_list) { next unless $git_url; - print "\n"; + print "\n"; $url_tag = ""; } + + # Tag cloud + my $show_ctags = (gitweb_check_feature('ctags'))[0]; + if ($show_ctags) { + my $ctags = git_get_project_ctags($project); + my $cloud = git_populate_project_tagcloud($ctags); + print "\n\n"; + } + print "
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
last change$cd{'rfc2822'}
last change$cd{'rfc2822'}
$url_tag$git_url
Content tags:
"; + print "
" unless %$ctags; + print "
Add:
"; + print "
" if %$ctags; + print git_show_project_tagcloud($cloud, 48); + print "
\n"; if (-s "$projectroot/$project/README.html") { @@ -4098,10 +4284,10 @@ sub git_summary { if (@forklist) { git_print_header_div('forks'); - git_project_list_body(\@forklist, undef, 0, 15, + git_project_list_body(\@forklist, 'age', 0, 15, $#forklist <= 15 ? undef : $cgi->a({-href => href(action=>"forks")}, "..."), - 'noheader'); + 'no_header'); } git_footer_html(); @@ -4114,7 +4300,7 @@ sub git_tag { my %tag = parse_tag($hash); if (! %tag) { - die_error(undef, "Unknown tag object"); + die_error(404, "Unknown tag object"); } git_print_header_div('commit', esc_html($tag{'name'}), $hash); @@ -4146,30 +4332,29 @@ sub git_tag { git_footer_html(); } -sub git_blame2 { +sub git_blame { my $fd; my $ftype; - my ($have_blame) = gitweb_check_feature('blame'); - if (!$have_blame) { - die_error('403 Permission denied', "Permission denied"); - } - die_error('404 Not Found', "File name not defined") if (!$file_name); + gitweb_check_feature('blame') + or die_error(403, "Blame view not allowed"); + + die_error(400, "No file name given") unless $file_name; $hash_base ||= git_get_head_hash($project); - die_error(undef, "Couldn't find base commit") unless ($hash_base); + die_error(404, "Couldn't find base commit") unless ($hash_base); my %co = parse_commit($hash_base) - or die_error(undef, "Reading commit failed"); + or die_error(404, "Commit not found"); if (!defined $hash) { $hash = git_get_hash_by_path($hash_base, $file_name, "blob") - or die_error(undef, "Error looking up file"); + or die_error(404, "Error looking up file"); } $ftype = git_get_type($hash); if ($ftype !~ "blob") { - die_error('400 Bad Request', "Object is not a blob"); + die_error(400, "Object is not a blob"); } open ($fd, "-|", git_cmd(), "blame", '-p', '--', $file_name, $hash_base) - or die_error(undef, "Open git-blame failed"); + or die_error(500, "Open git-blame failed"); git_header_html(); my $formats_nav = $cgi->a({-href => href(action=>"blob", -replay=>1)}, @@ -4231,7 +4416,7 @@ HTML print "\n"; } open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") - or die_error(undef, "Open git-rev-parse failed"); + or die_error(500, "Open git-rev-parse failed"); my $parent_commit = <$dd>; close $dd; chomp($parent_commit); @@ -4254,103 +4439,6 @@ HTML git_footer_html(); } -sub git_blame { - my $fd; - - my ($have_blame) = gitweb_check_feature('blame'); - if (!$have_blame) { - die_error('403 Permission denied', "Permission denied"); - } - die_error('404 Not Found', "File name not defined") if (!$file_name); - $hash_base ||= git_get_head_hash($project); - die_error(undef, "Couldn't find base commit") unless ($hash_base); - my %co = parse_commit($hash_base) - or die_error(undef, "Reading commit failed"); - if (!defined $hash) { - $hash = git_get_hash_by_path($hash_base, $file_name, "blob") - or die_error(undef, "Error lookup file"); - } - open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base) - or die_error(undef, "Open git-annotate failed"); - git_header_html(); - my $formats_nav = - $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, - "blob") . - " | " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, - "history") . - " | " . - $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, - "HEAD"); - git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); - git_print_page_path($file_name, 'blob', $hash_base); - print "
\n"; - print < - - Commit - Age - Author - Line - Data - -HTML - my @line_class = (qw(light dark)); - my $line_class_len = scalar (@line_class); - my $line_class_num = $#line_class; - while (my $line = <$fd>) { - my $long_rev; - my $short_rev; - my $author; - my $time; - my $lineno; - my $data; - my $age; - my $age_str; - my $age_class; - - chomp $line; - $line_class_num = ($line_class_num + 1) % $line_class_len; - - if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) { - $long_rev = $1; - $author = $2; - $time = $3; - $lineno = $4; - $data = $5; - } else { - print qq( Unable to parse: $line\n); - next; - } - $short_rev = substr ($long_rev, 0, 8); - $age = time () - $time; - $age_str = age_string ($age); - $age_str =~ s/ / /g; - $age_class = age_class($age); - $author = esc_html ($author); - $author =~ s/ / /g; - - $data = untabify($data); - $data = esc_html ($data); - - print < - $long_rev)}" class="text">$short_rev.. - $age_str - $author - $lineno - $data - -HTML - } # while (my $line = <$fd>) - print "\n\n"; - close $fd - or print "Reading blob failed.\n"; - print "
"; - git_footer_html(); -} - sub git_tags { my $head = git_get_head_hash($project); git_header_html(); @@ -4378,28 +4466,29 @@ sub git_heads { } sub git_blob_plain { + my $type = shift; my $expires; if (!defined $hash) { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); $hash = git_get_hash_by_path($base, $file_name, "blob") - or die_error(undef, "Error lookup file"); + or die_error(404, "Cannot find file"); } else { - die_error(undef, "No file name defined"); + die_error(400, "No file name defined"); } } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { # blobs defined by non-textual hash id's can be cached $expires = "+1d"; } - my $type = shift; open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash - or die_error(undef, "Couldn't cat $file_name, $hash"); + or die_error(500, "Open git-cat-file blob '$hash' failed"); - $type ||= blob_mimetype($fd, $file_name); + # content-type (can include charset) + $type = blob_contenttype($fd, $file_name, $type); - # save as filename, even when no $file_name is given + # "save as" filename, even when no $file_name is given my $save_as = "$hash"; if (defined $file_name) { $save_as = $file_name; @@ -4408,9 +4497,9 @@ sub git_blob_plain { } print $cgi->header( - -type => "$type", - -expires=>$expires, - -content_disposition => 'inline; filename="' . "$save_as" . '"'); + -type => $type, + -expires => $expires, + -content_disposition => 'inline; filename="' . $save_as . '"'); undef $/; binmode STDOUT, ':raw'; print <$fd>; @@ -4426,9 +4515,9 @@ sub git_blob { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); $hash = git_get_hash_by_path($base, $file_name, "blob") - or die_error(undef, "Error lookup file"); + or die_error(404, "Cannot find file"); } else { - die_error(undef, "No file name defined"); + die_error(400, "No file name defined"); } } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { # blobs defined by non-textual hash id's can be cached @@ -4437,7 +4526,7 @@ sub git_blob { my ($have_blame) = gitweb_check_feature('blame'); open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash - or die_error(undef, "Couldn't cat $file_name, $hash"); + or die_error(500, "Couldn't cat $file_name, $hash"); my $mimetype = blob_mimetype($fd, $file_name); if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) { close $fd; @@ -4516,11 +4605,12 @@ sub git_tree { $hash = $hash_base; } } + die_error(404, "No such tree") unless defined($hash); $/ = "\0"; open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash - or die_error(undef, "Open git-ls-tree failed"); + or die_error(500, "Open git-ls-tree failed"); my @entries = map { chomp; $_ } <$fd>; - close $fd or die_error(undef, "Reading tree failed"); + close $fd or die_error(404, "Reading tree failed"); $/ = "\n"; my $refs = git_get_references(); @@ -4556,8 +4646,8 @@ sub git_tree { if ($basedir ne '' && substr($basedir, -1) ne '/') { $basedir .= '/'; } + git_print_page_path($file_name, 'tree', $hash_base); } - git_print_page_path($file_name, 'tree', $hash_base); print "
\n"; print "\n"; my $alternate = 1; @@ -4610,23 +4700,22 @@ sub git_snapshot { my $format = $cgi->param('sf'); if (!@supported_fmts) { - die_error('403 Permission denied', "Permission denied"); + die_error(403, "Snapshots not allowed"); } # default to first supported snapshot format $format ||= $supported_fmts[0]; if ($format !~ m/^[a-z0-9]+$/) { - die_error(undef, "Invalid snapshot format parameter"); + die_error(400, "Invalid snapshot format parameter"); } elsif (!exists($known_snapshot_formats{$format})) { - die_error(undef, "Unknown snapshot format"); + die_error(400, "Unknown snapshot format"); } elsif (!grep($_ eq $format, @supported_fmts)) { - die_error(undef, "Unsupported snapshot format"); + die_error(403, "Unsupported snapshot format"); } if (!defined $hash) { $hash = git_get_head_hash($project); } - my $git_command = git_cmd_str(); my $name = $project; $name =~ s,([^/])/*\.git$,$1,; $name = basename($name); @@ -4634,11 +4723,12 @@ sub git_snapshot { $name =~ s/\047/\047\\\047\047/g; my $cmd; $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}"; - $cmd = "$git_command archive " . - "--format=$known_snapshot_formats{$format}{'format'} " . - "--prefix=\'$name\'/ $hash"; + $cmd = quote_command( + git_cmd(), 'archive', + "--format=$known_snapshot_formats{$format}{'format'}", + "--prefix=$name/", $hash); if (exists $known_snapshot_formats{$format}{'compressor'}) { - $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}}; + $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}); } print $cgi->header( @@ -4647,7 +4737,7 @@ sub git_snapshot { -status => '200 OK'); open my $fd, "-|", $cmd - or die_error(undef, "Execute git-archive failed"); + or die_error(500, "Execute git-archive failed"); binmode STDOUT, ':raw'; print <$fd>; binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi @@ -4666,7 +4756,7 @@ sub git_log { my @commitlist = parse_commits($hash, 101, (100 * $page)); - my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1))); + my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100); git_header_html(); git_print_page_nav('log','', $hash,undef,undef, $paging_nav); @@ -4715,10 +4805,8 @@ sub git_log { sub git_commit { $hash ||= $hash_base || "HEAD"; - my %co = parse_commit($hash); - if (!%co) { - die_error(undef, "Unknown commit object"); - } + my %co = parse_commit($hash) + or die_error(404, "Unknown commit object"); my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); @@ -4758,9 +4846,9 @@ sub git_commit { @diff_opts, (@$parents <= 1 ? $parent : '-c'), $hash, "--" - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); @difftree = map { chomp; $_ } <$fd>; - close $fd or die_error(undef, "Reading git-diff-tree failed"); + close $fd or die_error(404, "Reading git-diff-tree failed"); # non-textual hash id's can be cached my $expires; @@ -4851,35 +4939,35 @@ sub git_object { if ($hash || ($hash_base && !defined $file_name)) { my $object_id = $hash || $hash_base; - my $git_command = git_cmd_str(); - open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null" - or die_error('404 Not Found', "Object does not exist"); + open my $fd, "-|", quote_command( + git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null' + or die_error(404, "Object does not exist"); $type = <$fd>; chomp $type; close $fd - or die_error('404 Not Found', "Object does not exist"); + or die_error(404, "Object does not exist"); # - hash_base and file_name } elsif ($hash_base && defined $file_name) { $file_name =~ s,/+$,,; system(git_cmd(), "cat-file", '-e', $hash_base) == 0 - or die_error('404 Not Found', "Base object does not exist"); + or die_error(404, "Base object does not exist"); # here errors should not hapen open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name - or die_error(undef, "Open git-ls-tree failed"); + or die_error(500, "Open git-ls-tree failed"); my $line = <$fd>; close $fd; #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) { - die_error('404 Not Found', "File or directory for given base does not exist"); + die_error(404, "File or directory for given base does not exist"); } $type = $2; $hash = $3; } else { - die_error('404 Not Found', "Not enough information to find object"); + die_error(400, "Not enough information to find object"); } print $cgi->redirect(-uri => href(action=>$type, -full=>1, @@ -4904,12 +4992,12 @@ sub git_blobdiff { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base, "--", (defined $file_parent ? $file_parent : ()), $file_name - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); @difftree = map { chomp; $_ } <$fd>; close $fd - or die_error(undef, "Reading git-diff-tree failed"); + or die_error(404, "Reading git-diff-tree failed"); @difftree - or die_error('404 Not Found', "Blob diff not found"); + or die_error(404, "Blob diff not found"); } elsif (defined $hash && $hash =~ /[0-9a-fA-F]{40}/) { @@ -4918,23 +5006,23 @@ sub git_blobdiff { # read filtered raw output open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base, "--" - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); @difftree = # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c' # $hash == to_id grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ } map { chomp; $_ } <$fd>; close $fd - or die_error(undef, "Reading git-diff-tree failed"); + or die_error(404, "Reading git-diff-tree failed"); @difftree - or die_error('404 Not Found', "Blob diff not found"); + or die_error(404, "Blob diff not found"); } else { - die_error('404 Not Found', "Missing one of the blob diff parameters"); + die_error(400, "Missing one of the blob diff parameters"); } if (@difftree > 1) { - die_error('404 Not Found', "Ambiguous blob diff specification"); + die_error(400, "Ambiguous blob diff specification"); } %diffinfo = parse_difftree_raw_line($difftree[0]); @@ -4955,7 +5043,7 @@ sub git_blobdiff { '-p', ($format eq 'html' ? "--full-index" : ()), $hash_parent_base, $hash_base, "--", (defined $file_parent ? $file_parent : ()), $file_name - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); } # old/legacy style URI @@ -4991,9 +5079,9 @@ sub git_blobdiff { open $fd, "-|", git_cmd(), "diff", @diff_opts, '-p', ($format eq 'html' ? "--full-index" : ()), $hash_parent, $hash, "--" - or die_error(undef, "Open git-diff failed"); + or die_error(500, "Open git-diff failed"); } else { - die_error('404 Not Found', "Missing one of the blob diff parameters") + die_error(400, "Missing one of the blob diff parameters") unless %diffinfo; } @@ -5026,7 +5114,7 @@ sub git_blobdiff { print "X-Git-Url: " . $cgi->self_url() . "\n\n"; } else { - die_error(undef, "Unknown blobdiff format"); + die_error(400, "Unknown blobdiff format"); } # patch @@ -5061,10 +5149,8 @@ sub git_blobdiff_plain { sub git_commitdiff { my $format = shift || 'html'; $hash ||= $hash_base || "HEAD"; - my %co = parse_commit($hash); - if (!%co) { - die_error(undef, "Unknown commit object"); - } + my %co = parse_commit($hash) + or die_error(404, "Unknown commit object"); # choose format for commitdiff for merge if (! defined $hash_parent && @{$co{'parents'}} > 1) { @@ -5146,7 +5232,7 @@ sub git_commitdiff { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, "--no-commit-id", "--patch-with-raw", "--full-index", $hash_parent_param, $hash, "--" - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); while (my $line = <$fd>) { chomp $line; @@ -5158,10 +5244,10 @@ sub git_commitdiff { } elsif ($format eq 'plain') { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, '-p', $hash_parent_param, $hash, "--" - or die_error(undef, "Open git-diff-tree failed"); + or die_error(500, "Open git-diff-tree failed"); } else { - die_error(undef, "Unknown commitdiff format"); + die_error(400, "Unknown commitdiff format"); } # non-textual hash id's can be cached @@ -5244,19 +5330,15 @@ sub git_history { $page = 0; } my $ftype; - my %co = parse_commit($hash_base); - if (!%co) { - die_error(undef, "Unknown commit object"); - } + my %co = parse_commit($hash_base) + or die_error(404, "Unknown commit object"); my $refs = git_get_references(); my $limit = sprintf("--max-count=%i", (100 * ($page+1))); my @commitlist = parse_commits($hash_base, 101, (100 * $page), - $file_name, "--full-history"); - if (!@commitlist) { - die_error('404 Not Found', "No such file or directory on given branch"); - } + $file_name, "--full-history") + or die_error(404, "No such file or directory on given branch"); if (!defined $hash && defined $file_name) { # some commits could have deleted file in question, @@ -5270,7 +5352,7 @@ sub git_history { $ftype = git_get_type($hash); } if (!defined $ftype) { - die_error(undef, "Unknown type of object"); + die_error(500, "Unknown type of object"); } my $paging_nav = ''; @@ -5308,19 +5390,16 @@ sub git_history { } sub git_search { - my ($have_search) = gitweb_check_feature('search'); - if (!$have_search) { - die_error('403 Permission denied', "Permission denied"); - } + gitweb_check_feature('search') or die_error(403, "Search is disabled"); if (!defined $searchtext) { - die_error(undef, "Text field empty"); + die_error(400, "Text field is empty"); } if (!defined $hash) { $hash = git_get_head_hash($project); } my %co = parse_commit($hash); if (!%co) { - die_error(undef, "Unknown commit object"); + die_error(404, "Unknown commit object"); } if (!defined $page) { $page = 0; @@ -5330,16 +5409,12 @@ sub git_search { if ($searchtype eq 'pickaxe') { # pickaxe may take all resources of your box and run for several minutes # with every query - so decide by yourself how public you make this feature - my ($have_pickaxe) = gitweb_check_feature('pickaxe'); - if (!$have_pickaxe) { - die_error('403 Permission denied', "Permission denied"); - } + gitweb_check_feature('pickaxe') + or die_error(403, "Pickaxe is disabled"); } if ($searchtype eq 'grep') { - my ($have_grep) = gitweb_check_feature('grep'); - if (!$have_grep) { - die_error('403 Permission denied', "Permission denied"); - } + gitweb_check_feature('grep') + or die_error(403, "Grep is disabled"); } git_header_html(); @@ -5584,9 +5659,13 @@ sub git_shortlog { } my $refs = git_get_references(); - my @commitlist = parse_commits($hash, 101, (100 * $page)); + my $commit_hash = $hash; + if (defined $hash_parent) { + $commit_hash = "$hash_parent..$hash"; + } + my @commitlist = parse_commits($commit_hash, 101, (100 * $page)); - my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1))); + my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100); my $next_link = ''; if ($#commitlist >= 100) { $next_link = @@ -5613,7 +5692,7 @@ sub git_feed { # Atom: http://www.atomenabled.org/developers/syndication/ # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ if ($format ne 'rss' && $format ne 'atom') { - die_error(undef, "Unknown web feed format"); + die_error(400, "Unknown web feed format"); } # log/feed of current (HEAD) branch, log of given branch, history of file/directory