X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=gitweb%2Fgitweb.perl;h=ca6f038c946c9a075ec866cc4a6de240481537e6;hb=9eefd8ae8a955f470d931c13b8711d98f4e9d56e;hp=d5dbd6428b599bc937c50cb87de85099fc1af04c;hpb=26f1e9bd68b6eb32d1376ae392354551c5ae17bc;p=git.git diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index d5dbd6428..ca6f038c9 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -760,6 +760,7 @@ our @cgi_param_mapping = ( search_use_regexp => "sr", ctag => "by_tag", diff_style => "ds", + project_filter => "pf", # this must be last entry (for manipulation from JavaScript) javascript => "js" ); @@ -976,7 +977,7 @@ sub evaluate_path_info { our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base, $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp, - $searchtext, $search_regexp); + $searchtext, $search_regexp, $project_filter); sub evaluate_and_validate_params { our $action = $input_params{'action'}; if (defined $action) { @@ -994,6 +995,13 @@ sub evaluate_and_validate_params { } } + our $project_filter = $input_params{'project_filter'}; + if (defined $project_filter) { + if (!validate_pathname($project_filter)) { + die_error(404, "Invalid project_filter parameter"); + } + } + our $file_name = $input_params{'file_name'}; if (defined $file_name) { if (!validate_pathname($file_name)) { @@ -1073,7 +1081,16 @@ sub evaluate_and_validate_params { if (length($searchtext) < 2) { die_error(403, "At least two characters are required for search parameter"); } - $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext; + if ($search_use_regexp) { + $search_regexp = $searchtext; + if (!eval { qr/$search_regexp/; 1; }) { + (my $error = $@) =~ s/ at \S+ line \d+.*\n?//; + die_error(400, "Invalid search regexp '$search_regexp'", + esc_html($error)); + } + } else { + $search_regexp = quotemeta $searchtext; + } } } @@ -1123,8 +1140,10 @@ sub dispatch { if (!defined $action) { if (defined $hash) { $action = git_get_type($hash); + $action or die_error(404, "Object does not exist"); } elsif (defined $hash_base && defined $file_name) { $action = git_get_type("$hash_base:$file_name"); + $action or die_error(404, "File or directory does not exist"); } elsif (defined $project) { $action = 'summary'; } else { @@ -1705,6 +1724,88 @@ sub chop_and_escape_str { } } +# Highlight selected fragments of string, using given CSS class, +# and escape HTML. It is assumed that fragments do not overlap. +# Regions are passed as list of pairs (array references). +# +# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns +# 'foobar' +sub esc_html_hl_regions { + my ($str, $css_class, @sel) = @_; + return esc_html($str) unless @sel; + + my $out = ''; + my $pos = 0; + + for my $s (@sel) { + $out .= esc_html(substr($str, $pos, $s->[0] - $pos)) + if ($s->[0] - $pos > 0); + $out .= $cgi->span({-class => $css_class}, + esc_html(substr($str, $s->[0], $s->[1] - $s->[0]))); + + $pos = $s->[1]; + } + $out .= esc_html(substr($str, $pos)) + if ($pos < length($str)); + + return $out; +} + +# return positions of beginning and end of each match +sub matchpos_list { + my ($str, $regexp) = @_; + return unless (defined $str && defined $regexp); + + my @matches; + while ($str =~ /$regexp/g) { + push @matches, [$-[0], $+[0]]; + } + return @matches; +} + +# highlight match (if any), and escape HTML +sub esc_html_match_hl { + my ($str, $regexp) = @_; + return esc_html($str) unless defined $regexp; + + my @matches = matchpos_list($str, $regexp); + return esc_html($str) unless @matches; + + return esc_html_hl_regions($str, 'match', @matches); +} + + +# highlight match (if any) of shortened string, and escape HTML +sub esc_html_match_hl_chopped { + my ($str, $chopped, $regexp) = @_; + return esc_html_match_hl($str, $regexp) unless defined $chopped; + + my @matches = matchpos_list($str, $regexp); + return esc_html($chopped) unless @matches; + + # filter matches so that we mark chopped string + my $tail = "... "; # see chop_str + unless ($chopped =~ s/\Q$tail\E$//) { + $tail = ''; + } + my $chop_len = length($chopped); + my $tail_len = length($tail); + my @filtered; + + for my $m (@matches) { + if ($m->[0] > $chop_len) { + push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0); + last; + } elsif ($m->[1] > $chop_len) { + push @filtered, [ $m->[0], $chop_len + $tail_len ]; + last; + } + push @filtered, $m; + } + + return esc_html_hl_regions($chopped . $tail, 'match', @filtered); +} + ## ---------------------------------------------------------------------- ## functions returning short strings @@ -2391,7 +2492,7 @@ sub get_feed_info { return unless (defined $project); # some views should link to OPML, or to generic project feed, # or don't have specific feed yet (so they should use generic) - return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x); + return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x); my $branch; # branches refs uses 'refs/heads/' prefix (fullname) to differentiate @@ -2827,10 +2928,9 @@ sub git_get_project_url_list { sub git_get_projects_list { my $filter = shift || ''; + my $paranoid = shift; my @list; - $filter =~ s/\.git$//; - if (-d $projects_list) { # search in directory my $dir = $projects_list; @@ -2839,7 +2939,7 @@ sub git_get_projects_list { my $pfxlen = length("$dir"); my $pfxdepth = ($dir =~ tr!/!!); # when filtering, search only given subdirectory - if ($filter) { + if ($filter && !$paranoid) { $dir .= "/$filter"; $dir =~ s!/+$!!; } @@ -2864,6 +2964,10 @@ sub git_get_projects_list { } my $path = substr($File::Find::name, $pfxlen + 1); + # paranoidly only filter here + if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) { + next; + } # we check related file in $projectroot if (check_export_ok("$projectroot/$path")) { push @list, { path => $path }; @@ -2969,11 +3073,15 @@ sub filter_forks_from_projects_list { sub search_projects_list { my ($projlist, %opts) = @_; my $tagfilter = $opts{'tagfilter'}; - my $searchtext = $opts{'searchtext'}; + my $search_re = $opts{'search_regexp'}; return @$projlist - unless ($tagfilter || $searchtext); + unless ($tagfilter || $search_re); + # searching projects require filling to be run before it; + fill_project_list_info($projlist, + $tagfilter ? 'ctags' : (), + $search_re ? ('path', 'descr') : ()); my @projects; PROJECT: foreach my $pr (@$projlist) { @@ -2984,10 +3092,10 @@ sub search_projects_list { grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}}; } - if ($searchtext) { + if ($search_re) { next unless - $pr->{'path'} =~ /$searchtext/ || - $pr->{'descr_long'} =~ /$searchtext/; + $pr->{'path'} =~ /$search_re/ || + $pr->{'descr_long'} =~ /$search_re/; } push @projects, $pr; @@ -3729,7 +3837,12 @@ sub run_highlighter { sub get_page_title { my $title = to_utf8($site_name); - return $title unless (defined $project); + unless (defined $project) { + if (defined $project_filter) { + $title .= " - projects in '" . esc_path($project_filter) . "'"; + } + return $title; + } $title .= " - " . to_utf8($project); return $title unless (defined $action); @@ -3773,6 +3886,7 @@ sub print_feed_meta { '-type' => "application/$type+xml" ); + $href_params{'extra_options'} = undef; $href_params{'action'} = $type; $link_attr{'-href'} = href(%href_params); print "a({-href => href(project => undef, + project_filter => $dirprefix, + action => "project_list")}, + esc_html($part)) . " / "; + } +} + sub print_nav_breadcrumbs { my %opts = @_; print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; if (defined $project) { - print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); + my @dirname = split '/', $project; + my $projectbasename = pop @dirname; + print_nav_breadcrumbs_path(@dirname); + print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename)); if (defined $action) { my $action_print = $action ; if (defined $opts{-action_extra}) { @@ -3841,6 +3970,8 @@ sub print_nav_breadcrumbs { print " / $opts{-action_extra}"; } print "\n"; + } elsif (defined $project_filter) { + print_nav_breadcrumbs_path(split '/', $project_filter); } } @@ -3963,9 +4094,11 @@ sub git_footer_html { } } else { - print $cgi->a({-href => href(project=>undef, action=>"opml"), + print $cgi->a({-href => href(project=>undef, action=>"opml", + project_filter => $project_filter), -class => $feed_class}, "OPML") . " "; - print $cgi->a({-href => href(project=>undef, action=>"project_index"), + print $cgi->a({-href => href(project=>undef, action=>"project_index", + project_filter => $project_filter), -class => $feed_class}, "TXT") . "\n"; } print "\n"; # class="page_footer" @@ -5123,35 +5256,98 @@ sub git_patchset_body { # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -# fills project list info (age, description, owner, category, forks) +sub git_project_search_form { + my ($searchtext, $search_use_regexp) = @_; + + my $limit = ''; + if ($project_filter) { + $limit = " in '$project_filter/'"; + } + + print "
\n"; + print $cgi->startform(-method => 'get', -action => $my_uri) . + $cgi->hidden(-name => 'a', -value => 'project_list') . "\n"; + print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n" + if (defined $project_filter); + print $cgi->textfield(-name => 's', -value => $searchtext, + -title => "Search project by name and description$limit", + -size => 60) . "\n" . + "" . + $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', + -checked => $search_use_regexp) . + "\n" . + $cgi->submit(-name => 'btnS', -value => 'Search') . + $cgi->end_form() . "\n" . + $cgi->a({-href => href(project => undef, searchtext => undef, + project_filter => $project_filter)}, + esc_html("List all projects$limit")) . "
\n"; + print "
\n"; +} + +# entry for given @keys needs filling if at least one of keys in list +# is not present in %$project_info +sub project_info_needs_filling { + my ($project_info, @keys) = @_; + + # return List::MoreUtils::any { !exists $project_info->{$_} } @keys; + foreach my $key (@keys) { + if (!exists $project_info->{$key}) { + return 1; + } + } + return; +} + +# fills project list info (age, description, owner, category, forks, etc.) # for each project in the list, removing invalid projects from -# returned list +# returned list, or fill only specified info. +# +# Invalid projects are removed from the returned list if and only if you +# ask 'age' or 'age_string' to be filled, because they are the only fields +# that run unconditionally git command that requires repository, and +# therefore do always check if project repository is invalid. +# +# USAGE: +# * fill_project_list_info(\@project_list, 'descr_long', 'ctags') +# ensures that 'descr_long' and 'ctags' fields are filled +# * @project_list = fill_project_list_info(\@project_list) +# ensures that all fields are filled (and invalid projects removed) +# # NOTE: modifies $projlist, but does not remove entries from it sub fill_project_list_info { - my $projlist = shift; + my ($projlist, @wanted_keys) = @_; my @projects; + my $filter_set = sub { return @_; }; + if (@wanted_keys) { + my %wanted_keys = map { $_ => 1 } @wanted_keys; + $filter_set = sub { return grep { $wanted_keys{$_} } @_; }; + } my $show_ctags = gitweb_check_feature('ctags'); PROJECT: foreach my $pr (@$projlist) { - my (@activity) = git_get_last_activity($pr->{'path'}); - unless (@activity) { - next PROJECT; + if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) { + my (@activity) = git_get_last_activity($pr->{'path'}); + unless (@activity) { + next PROJECT; + } + ($pr->{'age'}, $pr->{'age_string'}) = @activity; } - ($pr->{'age'}, $pr->{'age_string'}) = @activity; - if (!defined $pr->{'descr'}) { + if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) { my $descr = git_get_project_description($pr->{'path'}) || ""; $descr = to_utf8($descr); $pr->{'descr_long'} = $descr; $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5); } - if (!defined $pr->{'owner'}) { + if (project_info_needs_filling($pr, $filter_set->('owner'))) { $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || ""; } - if ($show_ctags) { + if ($show_ctags && + project_info_needs_filling($pr, $filter_set->('ctags'))) { $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); } - if ($projects_list_group_categories && !defined $pr->{'category'}) { + if ($projects_list_group_categories && + project_info_needs_filling($pr, $filter_set->('category'))) { my $cat = git_get_project_category($pr->{'path'}) || $project_list_default_category; $pr->{'category'} = to_utf8($cat); @@ -5255,10 +5451,17 @@ sub git_project_list_rows { print "\n"; } print "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), - -class => "list"}, esc_html($pr->{'path'})) . "\n" . + -class => "list"}, + esc_html_match_hl($pr->{'path'}, $search_regexp)) . + "\n" . "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), - -class => "list", -title => $pr->{'descr_long'}}, - esc_html($pr->{'descr'})) . "\n" . + -class => "list", + -title => $pr->{'descr_long'}}, + $search_regexp + ? esc_html_match_hl_chopped($pr->{'descr_long'}, + $pr->{'descr'}, $search_regexp) + : esc_html($pr->{'descr'})) . + "\n" . "" . chop_and_escape_str($pr->{'owner'}, 15) . "\n"; print "{'age'}) . "\">" . (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\n" . @@ -5282,17 +5485,18 @@ sub git_project_list_body { my $show_ctags = gitweb_check_feature('ctags'); my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef; $check_forks = undef - if ($tagfilter || $searchtext); + if ($tagfilter || $search_regexp); # filtering out forks before filling info allows to do less work @projects = filter_forks_from_projects_list(\@projects) if ($check_forks); - @projects = fill_project_list_info(\@projects); - # searching projects require filling to be run before it + # search_projects_list pre-fills required info @projects = search_projects_list(\@projects, - 'searchtext' => $searchtext, + 'search_regexp' => $search_regexp, 'tagfilter' => $tagfilter) - if ($tagfilter || $searchtext); + if ($tagfilter || $search_regexp); + # fill the rest + @projects = fill_project_list_info(\@projects); $order ||= $default_projects_order; $from = 0 unless defined $from; @@ -5980,7 +6184,7 @@ sub git_project_list { die_error(400, "Unknown order parameter"); } - my @list = git_get_projects_list(); + my @list = git_get_projects_list($project_filter, $strict_export); if (!@list) { die_error(404, "No projects found"); } @@ -5991,11 +6195,8 @@ sub git_project_list { insert_file($home_text); print "\n"; } - print $cgi->startform(-method => "get") . - "

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

" . - $cgi->end_form() . "\n"; + + git_project_search_form($searchtext, $search_use_regexp); git_project_list_body(\@list, $order); git_footer_html(); } @@ -6006,7 +6207,9 @@ sub git_forks { die_error(400, "Unknown order parameter"); } - my @list = git_get_projects_list($project); + my $filter = $project; + $filter =~ s/\.git$//; + my @list = git_get_projects_list($filter); if (!@list) { die_error(404, "No forks found"); } @@ -6019,7 +6222,7 @@ sub git_forks { } sub git_project_index { - my @projects = git_get_projects_list(); + my @projects = git_get_projects_list($project_filter, $strict_export); if (!@projects) { die_error(404, "No projects found"); } @@ -6065,7 +6268,9 @@ sub git_summary { if ($check_forks) { # find forks of a project - @forklist = git_get_projects_list($project); + my $filter = $project; + $filter =~ s/\.git$//; + @forklist = git_get_projects_list($filter); # filter out forks of forks @forklist = filter_forks_from_projects_list(\@forklist) if (@forklist); @@ -7856,7 +8061,7 @@ sub git_atom { } sub git_opml { - my @list = git_get_projects_list(); + my @list = git_get_projects_list($project_filter, $strict_export); if (!@list) { die_error(404, "No projects found"); } @@ -7867,11 +8072,17 @@ sub git_opml { -content_disposition => 'inline; filename="opml.xml"'); my $title = esc_html($site_name); + my $filter = " within subdirectory "; + if (defined $project_filter) { + $filter .= esc_html($project_filter); + } else { + $filter = ""; + } print < - $title OPML Export + $title OPML Export$filter