X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/0e8c2e1d97cde0d2549466c258a5b065b80fd03a68d75a5d176626fb725d3ff3..e0794c82b3b3769fd4c646ba0c7613f28b3983b5c45c016948b9b29c6dffc7f8:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index 3398de3..c191482 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -20,6 +20,10 @@ use File::Basename qw(basename); use Time::HiRes qw(gettimeofday tv_interval); binmode STDOUT, ':utf8'; +if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) { + eval 'sub CGI::multi_param { CGI::param(@_) }' +} + our $t0 = [ gettimeofday() ]; our $number_of_git_cmds = 0; @@ -54,6 +58,11 @@ sub evaluate_uri { # to build the base URL ourselves: our $path_info = decode_utf8($ENV{"PATH_INFO"}); if ($path_info) { + # $path_info has already been URL-decoded by the web server, but + # $my_url and $my_uri have not. URL-decode them so we can properly + # strip $path_info. + $my_url = unescape($my_url); + $my_uri = unescape($my_uri); if ($my_url =~ s,\Q$path_info\E$,, && $my_uri =~ s,\Q$path_info\E$,, && defined $ENV{'SCRIPT_NAME'}) { @@ -80,6 +89,9 @@ our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++"; # string of the home link on top of all pages our $home_link_str = "++GITWEB_HOME_LINK_STR++"; +# extra breadcrumbs preceding the home link +our @extra_breadcrumbs = (); + # name of your site or organization to appear in page titles # replace this with something more descriptive for clearer bookmarks our $site_name = "++GITWEB_SITENAME++" @@ -134,6 +146,12 @@ our $default_projects_order = "project"; # (only effective if this variable evaluates to true) our $export_ok = "++GITWEB_EXPORT_OK++"; +# don't generate age column on the projects list page +our $omit_age_column = 0; + +# don't generate information about owners of repositories +our $omit_owner=0; + # show repository only if this subroutine returns true # when given the path to the project, for example: # sub { return -e "$_[0]/git-daemon-export-ok"; } @@ -260,16 +278,15 @@ our %highlight_basename = ( our %highlight_ext = ( # main extensions, defining name of syntax; # see files in /usr/share/highlight/langDefs/ directory - map { $_ => $_ } - qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make), + (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)), # alternate extensions, see /etc/highlight/filetypes.conf - 'h' => 'c', - map { $_ => 'sh' } qw(bash zsh ksh), - map { $_ => 'cpp' } qw(cxx c++ cc), - map { $_ => 'php' } qw(php3 php4 php5 phps), - map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi' - map { $_ => 'make'} qw(mak mk), - map { $_ => 'xml' } qw(xhtml html htm), + (map { $_ => 'c' } qw(c h)), + (map { $_ => 'sh' } qw(sh bash zsh ksh)), + (map { $_ => 'cpp' } qw(cpp cxx c++ cc)), + (map { $_ => 'php' } qw(php php3 php4 php5 phps)), + (map { $_ => 'pl' } qw(pl perl pm)), # perhaps also 'cgi' + (map { $_ => 'make'} qw(make mak mk)), + (map { $_ => 'xml' } qw(xml xhtml html htm)), ); # You define site-wide feature defaults here; override them with @@ -531,11 +548,25 @@ our %feature = ( # $feature{'remote_heads'}{'default'} = [1]; # To have project specific config enable override in $GITWEB_CONFIG # $feature{'remote_heads'}{'override'} = 1; - # and in project config gitweb.remote_heads = 0|1; + # and in project config gitweb.remoteheads = 0|1; 'remote_heads' => { 'sub' => sub { feature_bool('remote_heads', @_) }, 'override' => 0, 'default' => [0]}, + + # Enable showing branches under other refs in addition to heads + + # To set system wide extra branch refs have in $GITWEB_CONFIG + # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice']; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'extra-branch-refs'}{'override'} = 1; + # and in project config gitweb.extrabranchrefs = dirs of choice + # Every directory is separated with whitespace. + + 'extra-branch-refs' => { + 'sub' => \&feature_extra_branch_refs, + 'override' => 0, + 'default' => []}, ); sub gitweb_get_feature { @@ -614,6 +645,21 @@ sub feature_avatar { return @val ? @val : @_; } +sub feature_extra_branch_refs { + my (@branch_refs) = @_; + my $values = git_get_project_config('extrabranchrefs'); + + if ($values) { + $values = config_to_multi ($values); + @branch_refs = (); + foreach my $value (@{$values}) { + push @branch_refs, split /\s+/, $value; + } + } + + return @branch_refs; +} + # checking HEAD file with -e is fragile if the repository was # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed # and then pruned. @@ -644,6 +690,18 @@ sub filter_snapshot_fmts { !$known_snapshot_formats{$_}{'disabled'}} @fmts; } +sub filter_and_validate_refs { + my @refs = @_; + my %unique_refs = (); + + foreach my $ref (@refs) { + die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref)); + # 'heads' are added implicitly in get_branch_refs(). + $unique_refs{$ref} = 1 if ($ref ne 'heads'); + } + return sort keys %unique_refs; +} + # If it is set to code reference, it is code that it is to be run once per # request, allowing updating configurations that change with each request, # while running other code in config file only once. @@ -674,7 +732,7 @@ sub evaluate_gitweb_config { our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++"; our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++"; - # Protect agains duplications of file names, to not read config twice. + # Protect against duplications of file names, to not read config twice. # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so # there possibility of duplication of filename there doesn't matter. $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON); @@ -818,7 +876,7 @@ sub evaluate_query_params { while (my ($name, $symbol) = each %cgi_param_mapping) { if ($symbol eq 'opt') { - $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ]; + $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ]; } else { $input_params{$name} = decode_utf8($cgi->param($symbol)); } @@ -982,7 +1040,7 @@ our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_bas sub evaluate_and_validate_params { our $action = $input_params{'action'}; if (defined $action) { - if (!validate_action($action)) { + if (!is_valid_action($action)) { die_error(400, "Invalid action parameter"); } } @@ -990,7 +1048,7 @@ sub evaluate_and_validate_params { # parameters which are pathnames our $project = $input_params{'project'}; if (defined $project) { - if (!validate_project($project)) { + if (!is_valid_project($project)) { undef $project; die_error(404, "No such project"); } @@ -998,21 +1056,21 @@ sub evaluate_and_validate_params { our $project_filter = $input_params{'project_filter'}; if (defined $project_filter) { - if (!validate_pathname($project_filter)) { + if (!is_valid_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)) { + if (!is_valid_pathname($file_name)) { die_error(400, "Invalid file parameter"); } } our $file_parent = $input_params{'file_parent'}; if (defined $file_parent) { - if (!validate_pathname($file_parent)) { + if (!is_valid_pathname($file_parent)) { die_error(400, "Invalid file parent parameter"); } } @@ -1020,21 +1078,21 @@ sub evaluate_and_validate_params { # parameters which are refnames our $hash = $input_params{'hash'}; if (defined $hash) { - if (!validate_refname($hash)) { + if (!is_valid_refname($hash)) { die_error(400, "Invalid hash parameter"); } } our $hash_parent = $input_params{'hash_parent'}; if (defined $hash_parent) { - if (!validate_refname($hash_parent)) { + if (!is_valid_refname($hash_parent)) { die_error(400, "Invalid hash parent parameter"); } } our $hash_base = $input_params{'hash_base'}; if (defined $hash_base) { - if (!validate_refname($hash_base)) { + if (!is_valid_refname($hash_base)) { die_error(400, "Invalid hash base parameter"); } } @@ -1054,7 +1112,7 @@ sub evaluate_and_validate_params { our $hash_parent_base = $input_params{'hash_parent_base'}; if (defined $hash_parent_base) { - if (!validate_refname($hash_parent_base)) { + if (!is_valid_refname($hash_parent_base)) { die_error(400, "Invalid hash parent base parameter"); } } @@ -1077,12 +1135,21 @@ sub evaluate_and_validate_params { our $search_use_regexp = $input_params{'search_use_regexp'}; our $searchtext = $input_params{'searchtext'}; - our $search_regexp; + our $search_regexp = undef; if (defined $searchtext) { 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; + } } } @@ -1092,7 +1159,7 @@ sub evaluate_git_dir { our $git_dir = "$projectroot/$project" if $project; } -our (@snapshot_fmts, $git_avatar); +our (@snapshot_fmts, $git_avatar, @extra_branch_refs); sub configure_gitweb_features { # list of supported snapshot formats our @snapshot_fmts = gitweb_get_feature('snapshot'); @@ -1110,6 +1177,13 @@ sub configure_gitweb_features { } else { $git_avatar = ''; } + + our @extra_branch_refs = gitweb_get_feature('extra-branch-refs'); + @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs); +} + +sub get_branch_refs { + return ('heads', @extra_branch_refs); } # custom error handler: 'die ' is Internal Server Error @@ -1118,7 +1192,7 @@ sub handle_errors_html { # to avoid infinite loop where error occurs in die_error, # change handler to default handler, disabling handle_errors_html - set_message("Error occured when inside die_error:\n$msg"); + set_message("Error occurred when inside die_error:\n$msg"); # you cannot jump out of die_error when called as error handler; # the subroutine set via CGI::Carp::set_message is called _after_ @@ -1397,28 +1471,31 @@ sub href { ## ====================================================================== ## validation, quoting/unquoting and escaping -sub validate_action { - my $input = shift || return undef; +sub is_valid_action { + my $input = shift; return undef unless exists $actions{$input}; - return $input; + return 1; } -sub validate_project { - my $input = shift || return undef; - if (!validate_pathname($input) || +sub is_valid_project { + my $input = shift; + + return unless defined $input; + if (!is_valid_pathname($input) || !(-d "$projectroot/$input") || !check_export_ok("$projectroot/$input") || ($strict_export && !project_in_list($input))) { return undef; } else { - return $input; + return 1; } } -sub validate_pathname { - my $input = shift || return undef; +sub is_valid_pathname { + my $input = shift; - # no '.' or '..' as elements of path, i.e. no '.' nor '..' + return undef unless defined $input; + # no '.' or '..' as elements of path, i.e. no '.' or '..' # at the beginning, at the end, and between slashes. # also this catches doubled slashes if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { @@ -1428,24 +1505,33 @@ sub validate_pathname { if ($input =~ m!\0!) { return undef; } - return $input; + return 1; } -sub validate_refname { - my $input = shift || return undef; +sub is_valid_ref_format { + my $input = shift; - # textual hashes are O.K. - if ($input =~ m/^[0-9a-fA-F]{40}$/) { - return $input; - } - # it must be correct pathname - $input = validate_pathname($input) - or return undef; + return undef unless defined $input; # restrictions on ref name according to git-check-ref-format if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) { return undef; } - return $input; + return 1; +} + +sub is_valid_refname { + my $input = shift; + + return undef unless defined $input; + # textual hashes are O.K. + if ($input =~ m/^[0-9a-fA-F]{40}$/) { + return 1; + } + # it must be correct pathname + is_valid_pathname($input) or return undef; + # check git-check-ref-format restrictions + is_valid_ref_format($input) or return undef; + return 1; } # decode sequences of octets in utf8 into Perl's internal form, @@ -1538,7 +1624,7 @@ sub sanitize { return undef unless defined $str; $str = to_utf8($str); - $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg; + $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg; return $str; } @@ -1724,39 +1810,89 @@ sub chop_and_escape_str { # 'foobar' sub esc_html_hl_regions { my ($str, $css_class, @sel) = @_; - return esc_html($str) unless @sel; + my %opts = grep { ref($_) ne 'ARRAY' } @sel; + @sel = grep { ref($_) eq 'ARRAY' } @sel; + return esc_html($str, %opts) 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]))); + my ($begin, $end) = @$s; + + # Don't create empty elements. + next if $end <= $begin; + + my $escaped = esc_html(substr($str, $begin, $end - $begin), + %opts); - $pos = $s->[1]; + $out .= esc_html(substr($str, $pos, $begin - $pos), %opts) + if ($begin - $pos > 0); + $out .= $cgi->span({-class => $css_class}, $escaped); + + $pos = $end; } - $out .= esc_html(substr($str, $pos)) + $out .= esc_html(substr($str, $pos), %opts) if ($pos < length($str)); return $out; } -# highlight match (if any), and escape HTML -sub esc_html_match_hl { +# return positions of beginning and end of each match +sub matchpos_list { my ($str, $regexp) = @_; - return esc_html($str) unless defined $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 @@ -2000,7 +2136,7 @@ sub picon_url { if (!$avatar_cache{$email}) { my ($user, $domain) = split('@', $email); $avatar_cache{$email} = - "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" . + "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" . "$domain/$user/" . "users+domains+unknown/up/single"; } @@ -2015,7 +2151,7 @@ sub gravatar_url { my $email = lc shift; my $size = shift; $avatar_cache{$email} ||= - "http://www.gravatar.com/avatar/" . + "//www.gravatar.com/avatar/" . Digest::MD5::md5_hex($email) . "?s="; return $avatar_cache{$email} . $size; } @@ -2372,26 +2508,32 @@ sub format_cc_diff_chunk_header { } # process patch (diff) line (not to be used for diff headers), -# returning class and HTML-formatted (but not wrapped) line -sub process_diff_line { - my $line = shift; - my ($from, $to) = @_; - - my $diff_class = diff_line_class($line, $from, $to); - - chomp $line; - $line = untabify($line); +# returning HTML-formatted (but not wrapped) line. +# If the line is passed as a reference, it is treated as HTML and not +# esc_html()'ed. +sub format_diff_line { + my ($line, $diff_class, $from, $to) = @_; + + if (ref($line)) { + $line = $$line; + } else { + chomp $line; + $line = untabify($line); - if ($from && $to && $line =~ m/^\@{2} /) { - $line = format_unidiff_chunk_header($line, $from, $to); - return $diff_class, $line; + if ($from && $to && $line =~ m/^\@{2} /) { + $line = format_unidiff_chunk_header($line, $from, $to); + } elsif ($from && $to && $line =~ m/^\@{3}/) { + $line = format_cc_diff_chunk_header($line, $from, $to); + } else { + $line = esc_html($line, -nbsp=>1); + } + } - } elsif ($from && $to && $line =~ m/^\@{3}/) { - $line = format_cc_diff_chunk_header($line, $from, $to); - return $diff_class, $line; + my $diff_classes = "diff"; + $diff_classes .= " $diff_class" if ($diff_class); + $line = "
$line
\n"; - } - return $diff_class, esc_html($line, -nbsp=>1); + return $line; } # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)", @@ -2438,6 +2580,7 @@ sub format_snapshot_links { sub get_feed_info { my $format = shift || 'Atom'; my %res = (action => lc($format)); + my $matched_ref = 0; # feed links are possible only for project views return unless (defined $project); @@ -2445,12 +2588,17 @@ sub get_feed_info { # or don't have specific feed yet (so they should use generic) return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x); - my $branch; - # branches refs uses 'refs/heads/' prefix (fullname) to differentiate - # from tag links; this also makes possible to detect branch links - if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) || - (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) { - $branch = $1; + my $branch = undef; + # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix + # (fullname) to differentiate from tag links; this also makes + # possible to detect branch links + for my $ref (get_branch_refs()) { + if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) || + (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) { + $branch = $1; + $matched_ref = $ref; + last; + } } # find log type for feed description (title) my $type = 'log'; @@ -2463,7 +2611,7 @@ sub get_feed_info { } $res{-title} = $type; - $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef); + $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef); $res{'file_name'} = $file_name; return %res; @@ -2622,12 +2770,15 @@ sub git_get_project_config { # only subsection, if exists, is case sensitive, # and not lowercased by 'git config -z -l' if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) { + $lo =~ s/_//g; $key = join(".", lc($hi), $mi, lc($lo)); + return if ($lo =~ /\W/ || $hi =~ /\W/); } else { $key = lc($key); + $key =~ s/_//g; + return if ($key =~ /\W/); } $key =~ s/^gitweb\.//; - return if ($key =~ m/\W/); # type sanity check if (defined $type) { @@ -2948,9 +3099,11 @@ sub git_get_projects_list { } if (check_export_ok("$projectroot/$path")) { my $pr = { - path => $path, - owner => to_utf8($owner), + path => $path }; + if ($owner) { + $pr->{'owner'} = to_utf8($owner); + } push @list, $pr; } } @@ -3024,15 +3177,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' : (), - $searchtext ? ('path', 'descr') : ()); + $search_re ? ('path', 'descr') : ()); my @projects; PROJECT: foreach my $pr (@$projlist) { @@ -3043,10 +3196,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; @@ -3111,7 +3264,7 @@ sub git_get_last_activity { '--format=%(committer)', '--sort=-committerdate', '--count=1', - 'refs/heads') or return; + map { "refs/$_" } get_branch_refs ()) or return; my $most_recent = <$fd>; close $fd or return; if (defined $most_recent && @@ -3562,7 +3715,7 @@ sub parse_from_to_diffinfo { sub git_get_heads_list { my ($limit, @classes) = @_; - @classes = ('heads') unless @classes; + @classes = get_branch_refs() unless @classes; my @patterns = map { "refs/$_" } @classes; my @headslist; @@ -3580,9 +3733,16 @@ sub git_get_heads_list { my ($committer, $epoch, $tz) = ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); $ref_item{'fullname'} = $name; - $name =~ s!^refs/(?:head|remote)s/!!; + my $strip_refs = join '|', map { quotemeta } get_branch_refs(); + $name =~ s!^refs/($strip_refs|remotes)/!!; + $ref_item{'name'} = $name; + # for refs neither in 'heads' nor 'remotes' we want to + # show their ref dir + my $ref_dir = (defined $1) ? $1 : ''; + if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { + $ref_item{'name'} .= ' (' . $ref_dir . ')'; + } - $ref_item{'name'} = $name; $ref_item{'id'} = $hash; $ref_item{'title'} = $title || '(no commit message)'; $ref_item{'epoch'} = $epoch; @@ -3776,6 +3936,9 @@ sub run_highlighter { close $fd; open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ". + quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse', + '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);', + '--', "-fe=$fallback_encoding")." | ". quote_command($highlight_bin). " --replace-tabs=8 --fragment --syntax $syntax |" or die_error(500, "Couldn't open file or run syntax highlighter"); @@ -3837,6 +4000,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 => esc_url($home_link)}, $home_link_str) . " / "; + for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) { + print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / "; + } if (defined $project) { my @dirname = split '/', $project; my $projectbasename = pop @dirname; @@ -3942,7 +4108,7 @@ sub print_search_form { if ($use_pathinfo) { $action .= "/".esc_url($project); } - print $cgi->startform(-method => "get", -action => $action) . + print $cgi->start_form(-method => "get", -action => $action) . "
\n" . (!$use_pathinfo && $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . @@ -3950,8 +4116,8 @@ sub print_search_form { $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")}, "?")) . - " search:\n", + " " . $cgi->a({-href => href(action=>"search_help"), + -title => "search help" }, "?") . " search:\n", $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" . "" . $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', @@ -4411,30 +4577,33 @@ sub git_print_log { } # print log - my $signoff = 0; - my $empty = 0; + my $skip_blank_line = 0; foreach my $line (@$log) { - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - $signoff = 1; - $empty = 0; + if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) { if (! $opts{'-remove_signoff'}) { print "" . esc_html($line) . "
\n"; - next; - } else { - # remove signoff lines - next; + $skip_blank_line = 1; } - } else { - $signoff = 0; + next; + } + + if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) { + if (! $opts{'-remove_signoff'}) { + print "" . esc_html($1) . ": " . + "" . esc_html($2) . "" . + "
\n"; + $skip_blank_line = 1; + } + next; } # print only one empty line # do not print empty line after signoff if ($line eq "") { - next if ($empty || $signoff); - $empty = 1; + next if ($skip_blank_line); + $skip_blank_line = 1; } else { - $empty = 0; + $skip_blank_line = 0; } print format_log_line_html($line) . "
\n"; @@ -4442,7 +4611,7 @@ sub git_print_log { if ($opts{'-final_empty_line'}) { # end with single empty line - print "
\n" unless $empty; + print "
\n" unless $skip_blank_line; } } @@ -4944,10 +5113,186 @@ sub git_difftree_body { print "\n"; } -sub print_sidebyside_diff_chunk { - my @chunk = @_; +# Print context lines and then rem/add lines in a side-by-side manner. +sub print_sidebyside_diff_lines { + my ($ctx, $rem, $add) = @_; + + # print context block before add/rem block + if (@$ctx) { + print join '', + '
', + '
', + @$ctx, + '
', + '
', + @$ctx, + '
', + '
'; + } + + if (!@$add) { + # pure removal + print join '', + '
', + '
', + @$rem, + '
', + '
'; + } elsif (!@$rem) { + # pure addition + print join '', + '
', + '
', + @$add, + '
', + '
'; + } else { + print join '', + '
', + '
', + @$rem, + '
', + '
', + @$add, + '
', + '
'; + } +} + +# Print context lines and then rem/add lines in inline manner. +sub print_inline_diff_lines { + my ($ctx, $rem, $add) = @_; + + print @$ctx, @$rem, @$add; +} + +# Format removed and added line, mark changed part and HTML-format them. +# Implementation is based on contrib/diff-highlight +sub format_rem_add_lines_pair { + my ($rem, $add, $num_parents) = @_; + + # We need to untabify lines before split()'ing them; + # otherwise offsets would be invalid. + chomp $rem; + chomp $add; + $rem = untabify($rem); + $add = untabify($add); + + my @rem = split(//, $rem); + my @add = split(//, $add); + my ($esc_rem, $esc_add); + # Ignore leading +/- characters for each parent. + my ($prefix_len, $suffix_len) = ($num_parents, 0); + my ($prefix_has_nonspace, $suffix_has_nonspace); + + my $shorter = (@rem < @add) ? @rem : @add; + while ($prefix_len < $shorter) { + last if ($rem[$prefix_len] ne $add[$prefix_len]); + + $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/); + $prefix_len++; + } + + while ($prefix_len + $suffix_len < $shorter) { + last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]); + + $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/); + $suffix_len++; + } + + # Mark lines that are different from each other, but have some common + # part that isn't whitespace. If lines are completely different, don't + # mark them because that would make output unreadable, especially if + # diff consists of multiple lines. + if ($prefix_has_nonspace || $suffix_has_nonspace) { + $esc_rem = esc_html_hl_regions($rem, 'marked', + [$prefix_len, @rem - $suffix_len], -nbsp=>1); + $esc_add = esc_html_hl_regions($add, 'marked', + [$prefix_len, @add - $suffix_len], -nbsp=>1); + } else { + $esc_rem = esc_html($rem, -nbsp=>1); + $esc_add = esc_html($add, -nbsp=>1); + } + + return format_diff_line(\$esc_rem, 'rem'), + format_diff_line(\$esc_add, 'add'); +} + +# HTML-format diff context, removed and added lines. +sub format_ctx_rem_add_lines { + my ($ctx, $rem, $add, $num_parents) = @_; + my (@new_ctx, @new_rem, @new_add); + my $can_highlight = 0; + my $is_combined = ($num_parents > 1); + + # Highlight if every removed line has a corresponding added line. + if (@$add > 0 && @$add == @$rem) { + $can_highlight = 1; + + # Highlight lines in combined diff only if the chunk contains + # diff between the same version, e.g. + # + # - a + # - b + # + c + # + d + # + # Otherwise the highlightling would be confusing. + if ($is_combined) { + for (my $i = 0; $i < @$add; $i++) { + my $prefix_rem = substr($rem->[$i], 0, $num_parents); + my $prefix_add = substr($add->[$i], 0, $num_parents); + + $prefix_rem =~ s/-/+/g; + + if ($prefix_rem ne $prefix_add) { + $can_highlight = 0; + last; + } + } + } + } + + if ($can_highlight) { + for (my $i = 0; $i < @$add; $i++) { + my ($line_rem, $line_add) = format_rem_add_lines_pair( + $rem->[$i], $add->[$i], $num_parents); + push @new_rem, $line_rem; + push @new_add, $line_add; + } + } else { + @new_rem = map { format_diff_line($_, 'rem') } @$rem; + @new_add = map { format_diff_line($_, 'add') } @$add; + } + + @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx; + + return (\@new_ctx, \@new_rem, \@new_add); +} + +# Print context lines and then rem/add lines. +sub print_diff_lines { + my ($ctx, $rem, $add, $diff_style, $num_parents) = @_; + my $is_combined = $num_parents > 1; + + ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add, + $num_parents); + + if ($diff_style eq 'sidebyside' && !$is_combined) { + print_sidebyside_diff_lines($ctx, $rem, $add); + } else { + # default 'inline' style and unknown styles + print_inline_diff_lines($ctx, $rem, $add); + } +} + +sub print_diff_chunk { + my ($diff_style, $num_parents, $from, $to, @chunk) = @_; my (@ctx, @rem, @add); + # The class of the previous line. + my $prev_class = ''; + return unless @chunk; # incomplete last line might be among removed or added lines, @@ -4966,55 +5311,19 @@ sub print_sidebyside_diff_chunk { # print chunk headers if ($class && $class eq 'chunk_header') { - print $line; + print format_diff_line($line, $class, $from, $to); next; } - ## print from accumulator when type of class of lines change - # empty contents block on start rem/add block, or end of chunk - if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) { - print join '', - '
', - '
', - @ctx, - '
', - '
', - @ctx, - '
', - '
'; - @ctx = (); - } - # empty add/rem block on start context block, or end of chunk - if ((@rem || @add) && (!$class || $class eq 'ctx')) { - if (!@add) { - # pure removal - print join '', - '
', - '
', - @rem, - '
', - '
'; - } elsif (!@rem) { - # pure addition - print join '', - '
', - '
', - @add, - '
', - '
'; - } else { - # assume that it is change - print join '', - '
', - '
', - @rem, - '
', - '
', - @add, - '
', - '
'; - } - @rem = @add = (); + ## print from accumulator when have some add/rem lines or end + # of chunk (flush context lines), or when have add and rem + # lines and new block is reached (otherwise add/rem lines could + # be reordered) + if (!$class || ((@rem || @add) && $class eq 'ctx') || + (@rem && @add && $class ne $prev_class)) { + print_diff_lines(\@ctx, \@rem, \@add, + $diff_style, $num_parents); + @ctx = @rem = @add = (); } ## adding lines to accumulator @@ -5030,6 +5339,8 @@ sub print_sidebyside_diff_chunk { if ($class eq 'ctx') { push @ctx, $line; } + + $prev_class = $class; } } @@ -5151,27 +5462,19 @@ sub git_patchset_body { next PATCH if ($patch_line =~ m/^diff /); - my ($class, $line) = process_diff_line($patch_line, \%from, \%to); - my $diff_classes = "diff"; - $diff_classes .= " $class" if ($class); - $line = "
$line
\n"; + my $class = diff_line_class($patch_line, \%from, \%to); - if ($diff_style eq 'sidebyside' && !$is_combined) { - if ($class eq 'chunk_header') { - print_sidebyside_diff_chunk(@chunk); - @chunk = ( [ $class, $line ] ); - } else { - push @chunk, [ $class, $line ]; - } - } else { - # default 'inline' style and unknown styles - print $line; + if ($class eq 'chunk_header') { + print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk); + @chunk = (); } + + push @chunk, [ $class, $patch_line ]; } } continue { if (@chunk) { - print_sidebyside_diff_chunk(@chunk); + print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk); @chunk = (); } print "
\n"; # class="patch" @@ -5207,7 +5510,7 @@ sub git_patchset_body { # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . sub git_project_search_form { - my ($searchtext, $search_use_regexp); + my ($searchtext, $search_use_regexp) = @_; my $limit = ''; if ($project_filter) { @@ -5215,7 +5518,7 @@ sub git_project_search_form { } print "
\n"; - print $cgi->startform(-method => 'get', -action => $my_uri) . + print $cgi->start_form(-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); @@ -5311,23 +5614,30 @@ sub fill_project_list_info { sub sort_projects_list { my ($projlist, $order) = @_; - my @projects; - 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}; - return @$projlist unless defined $oi; - if ($oi->{'type'} eq 'str') { - @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist; - } else { - @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist; + sub order_str { + my $key = shift; + return sub { $a->{$key} cmp $b->{$key} }; } - return @projects; + sub order_num_then_undef { + my $key = shift; + return sub { + defined $a->{$key} ? + (defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) : + (defined $b->{$key} ? 1 : 0) + }; + } + + my %orderings = ( + project => order_str('path'), + descr => order_str('descr_long'), + owner => order_str('owner'), + age => order_num_then_undef('age'), + ); + + my $ordering = $orderings{$order}; + return defined $ordering ? sort $ordering @$projlist : @$projlist; } # returns a hash of categories, containing the list of project @@ -5401,14 +5711,25 @@ 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" . - "" . chop_and_escape_str($pr->{'owner'}, 15) . "\n"; - print "{'age'}) . "\">" . - (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\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"; + unless ($omit_owner) { + print "" . chop_and_escape_str($pr->{'owner'}, 15) . "\n"; + } + unless ($omit_age_column) { + print "{'age'}) . "\">" . + (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\n"; + } + print"" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . @@ -5428,18 +5749,21 @@ 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); # 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); + my @all_fields = ('descr', 'descr_long', 'ctags', 'category'); + push @all_fields, ('age', 'age_string') unless($omit_age_column); + push @all_fields, 'owner' unless($omit_owner); + @projects = fill_project_list_info(\@projects, @all_fields); $order ||= $default_projects_order; $from = 0 unless defined $from; @@ -5470,8 +5794,8 @@ sub git_project_list_body { } 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_sort_th('owner', $order, 'Owner') unless $omit_owner; + print_sort_th('age', $order, 'Last Change') unless $omit_age_column; print "\n" . # for links "\n"; } @@ -6224,8 +6548,10 @@ sub git_summary { print "
 
\n"; print "\n" . - "\n" . - "\n"; + "\n"; + if ($owner and not $omit_owner) { + print "\n"; + } if (defined $cd{'rfc2822'}) { print "" . "\n"; @@ -6386,6 +6712,7 @@ sub git_blame_common { $hash_base, '--', $file_name or die_error(500, "Open git-blame --porcelain failed"); } + binmode $fd, ':utf8'; # incremental blame data returns early if ($format eq 'data') { @@ -6775,7 +7102,7 @@ sub git_blob { git_print_page_path($file_name, "blob", $hash_base); print "
\n"; if ($mimetype =~ m!^image/!) { - print qq!!.esc_attr($file_name).qq! project my $name = to_utf8($project); $name =~ s,([^/])/*\.git$,$1,; - $name = basename($name); - # sanitize name - $name =~ s/[[:cntrl:]]/?/g; + $name = sanitize_for_filename(basename($name)); my $ver = $hash; if ($hash =~ /^[0-9a-fA-F]+$/) { @@ -6933,13 +7267,25 @@ sub snapshot_name { $ver = $1; } else { # branches and other need shortened SHA-1 hash - if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) { - $ver = $1; + my $strip_refs = join '|', map { quotemeta } get_branch_refs(); + if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) { + my $ref_dir = (defined $1) ? $1 : ''; + $ver = $2; + + $ref_dir = sanitize_for_filename($ref_dir); + # for refs neither in heads nor remotes we want to + # add a ref dir to archive name + if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { + $ver = $ref_dir . '-' . $ver; + } } $ver .= '-' . git_get_short_hash($project, $hash); } + # special case of sanitization for filename - we change + # slashes to dots instead of dashes # in case of hierarchical branch names $ver =~ s!/!.!g; + $ver =~ s/[^[:alnum:]_.-]//g; # name = project-version_string $name = "$name-$ver"; @@ -6947,6 +7293,28 @@ sub snapshot_name { return wantarray ? ($name, $name) : $name; } +sub exit_if_unmodified_since { + my ($latest_epoch) = @_; + our $cgi; + + my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); + if (defined $if_modified) { + my $since; + if (eval { require HTTP::Date; 1; }) { + $since = HTTP::Date::str2time($if_modified); + } elsif (eval { require Time::ParseDate; 1; }) { + $since = Time::ParseDate::parsedate($if_modified, GMT => 1); + } + if (defined $since && $latest_epoch <= $since) { + my %latest_date = parse_date($latest_epoch); + print $cgi->header( + -last_modified => $latest_date{'rfc2822'}, + -status => '304 Not Modified'); + goto DONE_GITWEB; + } + } +} + sub git_snapshot { my $format = $input_params{'snapshot_format'}; if (!@snapshot_fmts) { @@ -6973,6 +7341,10 @@ sub git_snapshot { my ($name, $prefix) = snapshot_name($project, $hash); my $filename = "$name$known_snapshot_formats{$format}{'suffix'}"; + + my %co = parse_commit($hash); + exit_if_unmodified_since($co{'committer_epoch'}) if %co; + my $cmd = quote_command( git_cmd(), 'archive', "--format=$known_snapshot_formats{$format}{'format'}", @@ -6982,9 +7354,15 @@ sub git_snapshot { } $filename =~ s/(["\\])/\\$1/g; + my %latest_date; + if (%co) { + %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + } + print $cgi->header( -type => $known_snapshot_formats{$format}{'type'}, -content_disposition => 'inline; filename="' . $filename . '"', + %co ? (-last_modified => $latest_date{'rfc2822'}) : (), -status => '200 OK'); open my $fd, "-|", $cmd @@ -7202,7 +7580,7 @@ sub git_object { git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null' or die_error(404, "Object does not exist"); $type = <$fd>; - chomp $type; + defined $type && chomp $type; close $fd or die_error(404, "Object does not exist"); @@ -7213,7 +7591,7 @@ sub git_object { system(git_cmd(), "cat-file", '-e', $hash_base) == 0 or die_error(404, "Base object does not exist"); - # here errors should not hapen + # here errors should not happen open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name or die_error(500, "Open git-ls-tree failed"); my $line = <$fd>; @@ -7764,33 +8142,14 @@ sub git_feed { if (defined($commitlist[0])) { %latest_commit = %{$commitlist[0]}; my $latest_epoch = $latest_commit{'committer_epoch'}; - %latest_date = parse_date($latest_epoch, $latest_commit{'comitter_tz'}); - my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); - if (defined $if_modified) { - my $since; - if (eval { require HTTP::Date; 1; }) { - $since = HTTP::Date::str2time($if_modified); - } elsif (eval { require Time::ParseDate; 1; }) { - $since = Time::ParseDate::parsedate($if_modified, GMT => 1); - } - if (defined $since && $latest_epoch <= $since) { - print $cgi->header( - -type => $content_type, - -charset => 'utf-8', - -last_modified => $latest_date{'rfc2822'}, - -status => '304 Not Modified'); - return; - } - } - print $cgi->header( - -type => $content_type, - -charset => 'utf-8', - -last_modified => $latest_date{'rfc2822'}); - } else { - print $cgi->header( - -type => $content_type, - -charset => 'utf-8'); + exit_if_unmodified_since($latest_epoch); + %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'}); } + print $cgi->header( + -type => $content_type, + -charset => 'utf-8', + %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (), + -status => '200 OK'); # Optimization: skip generating the body if client asks only # for Last-Modified date. @@ -7811,6 +8170,7 @@ sub git_feed { $feed_type = 'history'; } $title .= " $feed_type"; + $title = esc_html($title); my $descr = git_get_project_description($project); if (defined $descr) { $descr = esc_html($descr);
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
last change".format_timestamp_html(\%cd)."