X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/2976ca13640b0b8da3cb557834a978cf0a4eaca7949ee5497b5a1b65445ceb08..9d39b8d324ee469dea6eeaf90ffb751f8b1a0178ee2eddb7d9da205cec6d5f8f:/gitweb.perl?ds=sidebyside diff --git a/gitweb.perl b/gitweb.perl index c738fa9..999414b 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 { @@ -370,10 +415,15 @@ sub filter_snapshot_fmts { } our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; -do $GITWEB_CONFIG if -e $GITWEB_CONFIG; +if (-e $GITWEB_CONFIG) { + do $GITWEB_CONFIG; +} else { + our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++"; + do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM; +} # 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; @@ -382,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"); } } @@ -395,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"); } } @@ -417,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"); } } @@ -443,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"); } } } @@ -454,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"); } } @@ -462,24 +512,26 @@ 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"); } } +our $search_use_regexp = $cgi->param('sr'); + 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 = quotemeta $searchtext; + $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext; } # now read PATH_INFO and use it as alternative to parameters @@ -505,7 +557,7 @@ sub evaluate_path_info { } # do not change any parameters if an action is given using the query string return if $action; - $path_info =~ s,^$project/*,,; + $path_info =~ s,^\Q$project\E/*,,; my ($refname, $pathname) = split(/:/, $path_info, 2); if (defined $pathname) { # we got "project.git/branch:filename" or "project.git/branch:dir/" @@ -533,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, @@ -574,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; @@ -586,7 +638,7 @@ exit; ## ====================================================================== ## action links -sub href(%) { +sub href (%) { my %params = @_; # default is to use -absolute url() i.e. $my_uri my $href = $params{-full} ? $my_url : $my_uri; @@ -609,9 +661,12 @@ sub href(%) { searchtype => "st", snapshot_format => "sf", extra_options => "opt", + search_use_regexp => "sr", ); my %mapping = @mapping; + $params{'project'} = $project unless exists $params{'project'}; + if ($params{-replay}) { while (my ($name, $symbol) = each %mapping) { if (!exists $params{$name}) { @@ -621,12 +676,10 @@ sub href(%) { } } - $params{'project'} = $project unless exists $params{'project'}; - my ($use_pathinfo) = gitweb_check_feature('pathinfo'); if ($use_pathinfo) { # use PATH_INFO for project name - $href .= "/$params{'project'}" if defined $params{'project'}; + $href .= "/".esc_url($params{'project'}) if defined $params{'project'}; delete $params{'project'}; # Summary just uses the project path URL @@ -696,10 +749,9 @@ sub validate_refname { # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning sub to_utf8 { my $str = shift; - my $res; - eval { $res = decode_utf8($str, Encode::FB_CROAK); }; - if (defined $res) { - return $res; + if (utf8::valid($str)) { + utf8::decode($str); + return $str; } else { return decode($fallback_encoding, $str, Encode::FB_DEFAULT); } @@ -755,29 +807,40 @@ sub esc_path { # Make control characters "printable", using character escape codes (CEC) sub quot_cec { my $cntrl = shift; + my %opts = @_; my %es = ( # character escape codes, aka escape sequences - "\t" => '\t', # tab (HT) - "\n" => '\n', # line feed (LF) - "\r" => '\r', # carrige return (CR) - "\f" => '\f', # form feed (FF) - "\b" => '\b', # backspace (BS) - "\a" => '\a', # alarm (bell) (BEL) - "\e" => '\e', # escape (ESC) - "\013" => '\v', # vertical tab (VT) - "\000" => '\0', # nul character (NUL) - ); + "\t" => '\t', # tab (HT) + "\n" => '\n', # line feed (LF) + "\r" => '\r', # carrige return (CR) + "\f" => '\f', # form feed (FF) + "\b" => '\b', # backspace (BS) + "\a" => '\a', # alarm (bell) (BEL) + "\e" => '\e', # escape (ESC) + "\013" => '\v', # vertical tab (VT) + "\000" => '\0', # nul character (NUL) + ); my $chr = ( (exists $es{$cntrl}) ? $es{$cntrl} - : sprintf('\%03o', ord($cntrl)) ); - return "$chr"; + : sprintf('\%2x', ord($cntrl)) ); + if ($opts{-nohtml}) { + return $chr; + } else { + return "$chr"; + } } # Alternatively use unicode control pictures codepoints, # Unicode "printable representation" (PR) sub quot_upr { my $cntrl = shift; + my %opts = @_; + my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl)); - return "$chr"; + if ($opts{-nohtml}) { + return $chr; + } else { + return "$chr"; + } } # git may return quoted and escaped filenames @@ -802,7 +865,7 @@ sub unquote { return chr(oct($seq)); } elsif (exists $es{$seq}) { # C escape sequence, aka character escape code - return $es{$seq} + return $es{$seq}; } # quoted ordinary character return $seq; @@ -839,37 +902,82 @@ sub project_in_list { ## ---------------------------------------------------------------------- ## HTML aware string manipulation +# Try to chop given string on a word boundary between position +# $len and $len+$add_len. If there is no word boundary there, +# chop at $len+$add_len. Do not chop if chopped part plus ellipsis +# (marking chopped part) would be longer than given string. sub chop_str { my $str = shift; my $len = shift; my $add_len = shift || 10; + my $where = shift || 'right'; # 'left' | 'center' | 'right' + + # Make sure perl knows it is utf8 encoded so we don't + # cut in the middle of a utf8 multibyte char. + $str = to_utf8($str); # allow only $len chars, but don't cut a word if it would fit in $add_len # if it doesn't fit, cut it if it's still longer than the dots we would add - $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/; - my $body = $1; - my $tail = $2; - if (length($tail) > 4) { - $tail = " ..."; - $body =~ s/&[^;]*$//; # remove chopped character entities + # remove chopped character entities entirely + + # when chopping in the middle, distribute $len into left and right part + # return early if chopping wouldn't make string shorter + if ($where eq 'center') { + return $str if ($len + 5 >= length($str)); # filler is length 5 + $len = int($len/2); + } else { + return $str if ($len + 4 >= length($str)); # filler is length 4 + } + + # regexps: ending and beginning with word part up to $add_len + my $endre = qr/.{$len}\w{0,$add_len}/; + my $begre = qr/\w{0,$add_len}.{$len}/; + + if ($where eq 'left') { + $str =~ m/^(.*?)($begre)$/; + my ($lead, $body) = ($1, $2); + if (length($lead) > 4) { + $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/); + $lead = " ..."; + } + return "$lead$body"; + + } elsif ($where eq 'center') { + $str =~ m/^($endre)(.*)$/; + my ($left, $str) = ($1, $2); + $str =~ m/^(.*?)($begre)$/; + my ($mid, $right) = ($1, $2); + if (length($mid) > 5) { + $left =~ s/&[^;]*$//; + $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/); + $mid = " ... "; + } + return "$left$mid$right"; + + } else { + $str =~ m/^($endre)(.*)$/; + my $body = $1; + my $tail = $2; + if (length($tail) > 4) { + $body =~ s/&[^;]*$//; + $tail = "... "; + } + return "$body$tail"; } - return "$body$tail"; } # takes the same arguments as chop_str, but also wraps a around the # result with a title attribute if it does get chopped. Additionally, the # string is HTML-escaped. sub chop_and_escape_str { - my $str = shift; - my $len = shift; - my $add_len = shift || 10; + my ($str) = @_; - my $chopped = chop_str($str, $len, $add_len); + my $chopped = chop_str(@_); if ($chopped eq $str) { return esc_html($chopped); } else { - return qq{} . - esc_html($chopped) . qq{}; + $str =~ s/([[:cntrl:]])/?/g; + return $cgi->span({-title=>$str}, esc_html($chopped)); } } @@ -1028,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; @@ -1044,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 . ""; } } @@ -1390,6 +1529,46 @@ sub format_snapshot_links { } } +## ...................................................................... +## functions returning values to be passed, perhaps after some +## transformation, to other functions; e.g. returning arguments to href() + +# returns hash to be passed to href to generate gitweb URL +# in -title key it returns description of link +sub get_feed_info { + my $format = shift || 'Atom'; + my %res = (action => lc($format)); + + # feed links are possible only for project views + 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); + + 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; + } + # find log type for feed description (title) + my $type = 'log'; + if (defined $file_name) { + $type = "history of $file_name"; + $type .= "/" if ($action eq 'tree'); + $type .= " on '$branch'" if (defined $branch); + } else { + $type = "log of $branch" if (defined $branch); + } + + $res{-title} = $type; + $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef); + $res{'file_name'} = $file_name; + + return %res; +} + ## ---------------------------------------------------------------------- ## git utility subroutines, invoking git commands @@ -1398,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 @@ -1433,20 +1616,121 @@ sub git_get_type { return $type; } +# repository configuration +our $config_file = ''; +our %config; + +# store multiple values for single key as anonymous array reference +# single values stored directly in the hash, not as [ ] +sub hash_set_multi { + my ($hash, $key, $value) = @_; + + if (!exists $hash->{$key}) { + $hash->{$key} = $value; + } elsif (!ref $hash->{$key}) { + $hash->{$key} = [ $hash->{$key}, $value ]; + } else { + push @{$hash->{$key}}, $value; + } +} + +# return hash of git project configuration +# optionally limited to some section, e.g. 'gitweb' +sub git_parse_project_config { + my $section_regexp = shift; + my %config; + + local $/ = "\0"; + + open my $fh, "-|", git_cmd(), "config", '-z', '-l', + or return; + + while (my $keyval = <$fh>) { + chomp $keyval; + my ($key, $value) = split(/\n/, $keyval, 2); + + hash_set_multi(\%config, $key, $value) + if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o); + } + close $fh; + + return %config; +} + +# convert config value to boolean, 'true' or 'false' +# no value, number > 0, 'true' and 'yes' values are true +# rest of values are treated as false (never as error) +sub config_to_bool { + my $val = shift; + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + return (!defined $val || # section.key + ($val =~ /^\d+$/ && $val) || # section.key = 1 + ($val =~ /^(?:true|yes)$/i)); # section.key = true +} + +# convert config value to simple decimal number +# an optional value suffix of 'k', 'm', or 'g' will cause the value +# to be multiplied by 1024, 1048576, or 1073741824 +sub config_to_int { + my $val = shift; + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) { + $unit = lc($unit); + # unknown unit is treated as 1 + return $num * ($unit eq 'g' ? 1073741824 : + $unit eq 'm' ? 1048576 : + $unit eq 'k' ? 1024 : 1); + } + return $val; +} + +# convert config value to array reference, if needed +sub config_to_multi { + my $val = shift; + + return ref($val) ? $val : (defined($val) ? [ $val ] : []); +} + sub git_get_project_config { my ($key, $type) = @_; + # key sanity check return unless ($key); $key =~ s/^gitweb\.//; return if ($key =~ m/\W/); - my @x = (git_cmd(), 'config'); - if (defined $type) { push @x, $type; } - push @x, "--get"; - push @x, "gitweb.$key"; - my $val = qx(@x); - chomp $val; - return ($val); + # type sanity check + if (defined $type) { + $type =~ s/^--//; + $type = undef + unless ($type eq 'bool' || $type eq 'int'); + } + + # get config + if (!defined $config_file || + $config_file ne "$git_dir/config") { + %config = git_parse_project_config('gitweb'); + $config_file = "$git_dir/config"; + } + + # ensure given type + if (!defined $type) { + return $config{"gitweb.$key"}; + } elsif ($type eq 'bool') { + # backward compatibility: 'git config --bool' returns true/false + return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false'; + } elsif ($type eq 'int') { + return config_to_int($config{"gitweb.$key"}); + } + return $config{"gitweb.$key"}; } # get hash of given path at given ref @@ -1458,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; @@ -1506,7 +1790,9 @@ sub git_get_path_by_hash { sub git_get_project_description { my $path = shift; - open my $fd, "$projectroot/$path/description" or return undef; + $git_dir = "$projectroot/$path"; + open my $fd, "$git_dir/description" + or return git_get_project_config('description'); my $descr = <$fd>; close $fd; if (defined $descr) { @@ -1515,10 +1801,75 @@ 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; - open my $fd, "$projectroot/$path/cloneurl" or return; + $git_dir = "$projectroot/$path"; + open my $fd, "$git_dir/cloneurl" + or return wantarray ? + @{ config_to_multi(git_get_project_config('url')) } : + config_to_multi(git_get_project_config('url')); my @git_project_url_list = map { chomp; $_ } <$fd>; close $fd; @@ -1654,6 +2005,7 @@ sub git_get_project_owner { my $owner; return undef unless $project; + $git_dir = "$projectroot/$project"; if (!defined $gitweb_project_owner) { git_get_project_list_from_file(); @@ -1662,8 +2014,11 @@ sub git_get_project_owner { if (exists $gitweb_project_owner->{$project}) { $owner = $gitweb_project_owner->{$project}; } + if (!defined $owner){ + $owner = git_get_project_config('owner'); + } if (!defined $owner) { - $owner = get_file_owner("$projectroot/$project"); + $owner = get_file_owner("$git_dir"); } return $owner; @@ -1701,7 +2056,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 { @@ -1750,7 +2105,7 @@ sub parse_date { $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min; $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", - 1900+$year, $mon, $mday, $hour ,$min, $sec; + 1900+$year, 1+$mon, $mday, $hour ,$min, $sec; $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/; my $local = $epoch + ((int $1 + ($2/60)) * 3600); @@ -1875,7 +2230,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 @@ -1910,7 +2265,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; @@ -1918,7 +2273,7 @@ sub parse_commit { } sub parse_commits { - my ($commit_id, $maxcount, $skip, $arg, $filename) = @_; + my ($commit_id, $maxcount, $skip, $filename, @args) = @_; my @cos; $maxcount ||= 1; @@ -1928,14 +2283,14 @@ sub parse_commits { open my $fd, "-|", git_cmd(), "rev-list", "--header", - ($arg ? ($arg) : ()), + @args, ("--max-count=" . $maxcount), ("--skip=" . $skip), @extra_options, $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; @@ -1945,49 +2300,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; @@ -2000,7 +2312,7 @@ sub parse_difftree_raw_line { $res{'to_mode'} = $2; $res{'from_id'} = $3; $res{'to_id'} = $4; - $res{'status'} = $res{'status_str'} = $5; + $res{'status'} = $5; $res{'similarity'} = $6; if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7); @@ -2016,7 +2328,6 @@ sub parse_difftree_raw_line { $res{'to_mode'} = pop @{$res{'from_mode'}}; $res{'from_id'} = [ split(' ', $3) ]; $res{'to_id'} = pop @{$res{'from_id'}}; - $res{'status_str'} = $4; $res{'status'} = [ split('', $4) ]; $res{'to_file'} = unquote($5); } @@ -2128,6 +2439,7 @@ sub git_get_heads_list { my ($hash, $name, $title) = split(' ', $refinfo, 3); my ($committer, $epoch, $tz) = ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); + $ref_item{'fullname'} = $name; $name =~ s!^refs/heads/!!; $ref_item{'name'} = $name; @@ -2165,6 +2477,7 @@ sub git_get_tags_list { my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6); my ($creator, $epoch, $tz) = ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/); + $ref_item{'fullname'} = $name; $name =~ s!^refs/tags/!!; $ref_item{'type'} = $type; @@ -2267,8 +2580,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) { @@ -2282,6 +2594,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 @@ -2340,30 +2663,49 @@ EOF } } if (defined $project) { - printf(''."\n", - esc_param($project), href(action=>"rss")); - printf(''."\n", - esc_param($project), href(action=>"rss", - extra_options=>"--no-merges")); - printf(''."\n", - esc_param($project), href(action=>"atom")); - printf(''."\n", - esc_param($project), href(action=>"atom", - extra_options=>"--no-merges")); + my %href_params = get_feed_info(); + if (!exists $href_params{'-title'}) { + $href_params{'-title'} = 'log'; + } + + foreach my $format qw(RSS Atom) { + my $type = lc($format); + my %link_attr = ( + '-rel' => 'alternate', + '-title' => "$project - $href_params{'-title'} - $format feed", + '-type' => "application/$type+xml" + ); + + $href_params{'action'} = $type; + $link_attr{'-href'} = href(%href_params); + print "\n"; + + $href_params{'extra_options'} = '--no-merges'; + $link_attr{'-href'} = href(%href_params); + $link_attr{'-title'} .= ' (no merges)'; + print "\n"; + } + } else { printf(''."\n", + 'href="%s" type="text/plain; charset=utf-8" />'."\n", $site_name, href(project=>undef, action=>"project_index")); printf(''."\n", + 'href="%s" type="text/x-opml" />'."\n", $site_name, href(project=>undef, action=>"opml")); } if (defined $favicon) { - print qq(\n); + print qq(\n); } print "\n" . @@ -2390,7 +2732,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 = ""; } @@ -2405,45 +2747,58 @@ EOF my $action = $my_uri; my ($use_pathinfo) = gitweb_check_feature('pathinfo'); if ($use_pathinfo) { - $action .= "/$project"; - } else { - $cgi->param("p", $project); + $action .= "/".esc_url($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")}, "?")) . " search:\n", $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . + "" . + $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', + -checked => $search_use_regexp) . + "" . "
" . $cgi->end_form() . "\n"; } } sub git_footer_html { + my $feed_class = 'rss_logo'; + print "
\n"; if (defined $project) { my $descr = git_get_project_description($project); if (defined $descr) { print "\n"; } - print $cgi->a({-href => href(action=>"rss"), - -class => "rss_logo"}, "RSS") . " "; - print $cgi->a({-href => href(action=>"atom"), - -class => "rss_logo"}, "Atom") . "\n"; + + my %href_params = get_feed_info(); + if (!%href_params) { + $feed_class .= ' generic'; + } + $href_params{'-title'} ||= 'log'; + + foreach my $format qw(RSS Atom) { + $href_params{'action'} = lc($format); + print $cgi->a({-href => href(%href_params), + -title => "$href_params{'-title'} $format feed", + -class => $feed_class}, $format)."\n"; + } + } else { print $cgi->a({-href => href(project=>undef, action=>"opml"), - -class => "rss_logo"}, "OPML") . " "; + -class => $feed_class}, "OPML") . " "; print $cgi->a({-href => href(project=>undef, action=>"project_index"), - -class => "rss_logo"}, "TXT") . "\n"; + -class => $feed_class}, "TXT") . "\n"; } - print "
\n" ; + print "\n"; # class="page_footer" if (-f $site_footer) { open (my $fd, $site_footer); @@ -2455,11 +2810,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 <

@@ -2494,20 +2864,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; @@ -2519,15 +2902,15 @@ sub format_paging_nav { if ($page > 0) { $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1), + $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= " ⋅ prev"; } - if ($nrevs >= (100 * ($page+1)-1)) { + if ($has_next_link) { $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1), + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } else { $paging_nav .= " ⋅ next"; @@ -2832,7 +3215,7 @@ sub fill_from_file_info { sub is_deleted { my $diffinfo = shift; - return $diffinfo->{'status_str'} =~ /D/; + return $diffinfo->{'to_id'} eq ('0' x 40); } # does patch correspond to [previous] difftree raw line @@ -3303,21 +3686,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'}) { @@ -3329,66 +3716,86 @@ 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 { + 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'}}; if ($alternate) { print "\n"; } else { @@ -3438,7 +3845,7 @@ sub git_shortlog_body { $from = 0 unless defined $from; $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); - 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"; + print "
\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my %co = %{$commitlist->[$i]}; @@ -3484,7 +3891,7 @@ sub git_history_body { $from = 0 unless defined $from; $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); - print "
\n"; + print "
\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my %co = %{$commitlist->[$i]}; @@ -3544,7 +3951,7 @@ sub git_tags_body { $from = 0 unless defined $from; $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); - print "
\n"; + print "
\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my $entry = $taglist->[$i]; @@ -3585,8 +3992,8 @@ sub git_tags_body { "
" . " | " . $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'}); if ($tag{'reftype'} eq "commit") { - print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . - " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log"); + print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") . + " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log"); } elsif ($tag{'reftype'} eq "blob") { print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw"); } @@ -3607,7 +4014,7 @@ sub git_heads_body { $from = 0 unless defined $from; $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to); - print "\n"; + print "
\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my $entry = $headlist->[$i]; @@ -3621,13 +4028,13 @@ sub git_heads_body { $alternate ^= 1; print "\n" . ($curr ? "\n" . "\n" . ""; } @@ -3644,7 +4051,7 @@ sub git_search_grep_body { $from = 0 unless defined $from; $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); - print "
$ref{'age'}" : "") . - $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'}), + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}), -class => "list name"},esc_html($ref{'name'})) . "" . - $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'})}, "shortlog") . " | " . - $cgi->a({-href => href(action=>"log", hash=>$ref{'name'})}, "log") . " | " . - $cgi->a({-href => href(action=>"tree", hash=>$ref{'name'}, hash_base=>$ref{'name'})}, "tree") . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " . + $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") . "
\n"; + print "
\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my %co = %{$commitlist->[$i]}; @@ -3662,24 +4069,32 @@ sub git_search_grep_body { print "\n" . "\n" . "\n" . "\n" . "\n"; @@ -3699,12 +4114,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(); @@ -3722,12 +4137,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(); @@ -3785,11 +4200,11 @@ sub git_summary { git_print_page_nav('summary','', $head); print "
 
\n"; - print "
$co{'age_string_date'}" . $author . "" . - $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, - chop_and_escape_str($co{'title'}, 50) . "
"); + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + chop_and_escape_str($co{'title'}, 50) . "
"); my $comment = $co{'comment'}; foreach my $line (@$comment) { - if ($line =~ m/^(.*)($search_regexp)(.*)$/i) { - my $lead = esc_html($1) || ""; - $lead = chop_str($lead, 30, 10); - my $match = esc_html($2) || ""; - my $trail = esc_html($3) || ""; - $trail = chop_str($trail, 30, 10); - my $text = "$lead$match$trail"; - print chop_str($text, 80, 5) . "
\n"; + if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) { + my ($lead, $match, $trail) = ($1, $2, $3); + $match = chop_str($match, 70, 5, 'center'); + my $contextlen = int((80 - length($match))/2); + $contextlen = 30 if ($contextlen > 30); + $lead = chop_str($lead, $contextlen, 10, 'left'); + $trail = chop_str($trail, $contextlen, 10, 'right'); + + $lead = esc_html($lead); + $match = esc_html($match); + $trail = esc_html($trail); + + print "$lead$match$trail
"; } } print "
" . $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") . + " | " . $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); print "
\n" . - "\n" . - "\n"; + print "
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
\n" . + "\n" . + "\n"; if (defined $cd{'rfc2822'}) { - print "\n"; + print "\n"; } # use per project git URL list in $projectroot/$project/cloneurl @@ -3799,15 +4214,31 @@ 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) . "
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") { if (open my $fd, "$projectroot/$project/README.html") { - print "
readme
\n"; + print "
readme
\n" . + "
\n"; print $_ while (<$fd>); + print "\n
\n"; # class="readme" close $fd; } } @@ -3838,10 +4269,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(); @@ -3854,12 +4285,12 @@ 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); print "
\n" . - "\n" . + "
\n" . "\n" . "\n" . "\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); @@ -3994,103 +4424,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 < -
- - - - - - -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( \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 < - - - - - - -HTML - } # while (my $line = <$fd>) - print "
object" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, @@ -3886,37 +4317,36 @@ 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", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blob", -replay=>1)}, "blob") . " | " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, - "history") . + $cgi->a({-href => href(action=>"history", -replay=>1)}, + "history") . " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "HEAD"); @@ -3971,7 +4401,7 @@ HTML print "
CommitAgeAuthorLineData
Unable to parse: $line
$long_rev)}" class="text">$short_rev..$age_str$author$lineno$data
\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(); @@ -4118,28 +4451,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; @@ -4148,9 +4482,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>; @@ -4166,9 +4500,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 @@ -4177,9 +4511,9 @@ 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)$)!) { + if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) { close $fd; return git_blob_plain($mimetype); } @@ -4192,18 +4526,15 @@ sub git_blob { if (defined $file_name) { if ($have_blame) { $formats_nav .= - $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blame", -replay=>1)}, "blame") . " | "; } $formats_nav .= - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"history", -replay=>1)}, "history") . " | " . - $cgi->a({-href => href(action=>"blob_plain", - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, "raw") . " | " . $cgi->a({-href => href(action=>"blob", @@ -4211,7 +4542,8 @@ sub git_blob { "HEAD"); } else { $formats_nav .= - $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw"); + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, + "raw"); } git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); @@ -4222,16 +4554,7 @@ sub git_blob { } git_print_page_path($file_name, "blob", $hash_base); print "
\n"; - if ($mimetype =~ m!^text/!) { - my $nr; - while (my $line = <$fd>) { - chomp $line; - $nr++; - $line = untabify($line); - printf "
%4i %s
\n", - $nr, $nr, $nr, esc_html($line, -nbsp=>1); - } - } elsif ($mimetype =~ m!^image/!) { + if ($mimetype =~ m!^image/!) { print qq!$file_name"blob_plain", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name) . qq!" />\n!; + } else { + my $nr; + while (my $line = <$fd>) { + chomp $line; + $nr++; + $line = untabify($line); + printf "
%4i %s
\n", + $nr, $nr, $nr, esc_html($line, -nbsp=>1); + } } close $fd or print "Reading blob failed.\n"; @@ -4258,11 +4590,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(); @@ -4274,8 +4607,7 @@ sub git_tree { my @views_nav = (); if (defined $file_name) { push @views_nav, - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"history", -replay=>1)}, "history"), $cgi->a({-href => href(action=>"tree", hash_base=>"HEAD", file_name=>$file_name)}, @@ -4299,10 +4631,10 @@ 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"; + print "
\n"; my $alternate = 1; # '..' (top directory) link if possible if (defined $hash_base && @@ -4353,23 +4685,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); @@ -4377,11 +4708,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( @@ -4390,7 +4722,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 @@ -4409,7 +4741,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); @@ -4449,7 +4781,7 @@ sub git_log { } if ($#commitlist >= 100) { print "
\n"; - print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1), + print $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); print "
\n"; } @@ -4458,10 +4790,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'}); @@ -4501,9 +4831,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; @@ -4524,7 +4854,7 @@ sub git_commit { git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash); } print "
\n" . - "
\n"; + "
\n"; print "\n". "" . "
author" . esc_html($co{'author'}) . "
$ad{'rfc2822'}"; @@ -4594,35 +4924,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, @@ -4647,12 +4977,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}/) { @@ -4661,23 +4991,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]); @@ -4698,7 +5028,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 @@ -4734,19 +5064,16 @@ 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; } # header if ($format eq 'html') { my $formats_nav = - $cgi->a({-href => href(action=>"blobdiff_plain", - hash=>$hash, hash_parent=>$hash_parent, - hash_base=>$hash_base, hash_parent_base=>$hash_parent_base, - file_name=>$file_name, file_parent=>$file_parent)}, + $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)}, "raw"); git_header_html(undef, $expires); if (defined $hash_base && (my %co = parse_commit($hash_base))) { @@ -4772,7 +5099,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 @@ -4807,10 +5134,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) { @@ -4820,8 +5145,7 @@ sub git_commitdiff { my $formats_nav; if ($format eq 'html') { $formats_nav = - $cgi->a({-href => href(action=>"commitdiff_plain", - hash=>$hash, hash_parent=>$hash_parent)}, + $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, "raw"); if (defined $hash_parent && @@ -4893,7 +5217,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; @@ -4905,10 +5229,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 @@ -4944,16 +5268,15 @@ sub git_commitdiff { -expires => $expires, -content_disposition => 'inline; filename="' . "$filename" . '"'); my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); - print <self_url() . "\n\n"; foreach my $line (@{$co{'comment'}}) { - print "$line\n"; + print to_utf8($line) . "\n"; } print "---\n\n"; } @@ -4992,22 +5315,30 @@ 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") + or die_error(404, "No such file or directory on given branch"); + if (!defined $hash && defined $file_name) { - $hash = git_get_hash_by_path($hash_base, $file_name); + # some commits could have deleted file in question, + # and not have it in tree, but one of them has to have it + for (my $i = 0; $i <= @commitlist; $i++) { + $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name); + last if defined $hash; + } } if (defined $hash) { $ftype = git_get_type($hash); } - - my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name); + if (!defined $ftype) { + die_error(500, "Unknown type of object"); + } my $paging_nav = ''; if ($page > 0) { @@ -5016,27 +5347,20 @@ sub git_history { file_name=>$file_name)}, "first"); $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page-1), + $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= "first"; $paging_nav .= " ⋅ prev"; } - if ($#commitlist >= 100) { - $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); - } else { - $paging_nav .= " ⋅ next"; - } my $next_link = ''; if ($#commitlist >= 100) { $next_link = - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page+1), + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); + $paging_nav .= " ⋅ $next_link"; + } else { + $paging_nav .= " ⋅ next"; } git_header_html(); @@ -5051,19 +5375,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; @@ -5073,16 +5394,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(); @@ -5096,40 +5413,36 @@ sub git_search { } elsif ($searchtype eq 'committer') { $greptype = "--committer="; } - $greptype .= $search_regexp; - my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype); + $greptype .= $searchtext; + my @commitlist = parse_commits($hash, 101, (100 * $page), undef, + $greptype, '--regexp-ignore-case', + $search_use_regexp ? '--extended-regexp' : '--fixed-strings'); my $paging_nav = ''; if ($page > 0) { $paging_nav .= $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype)}, + searchtext=>$searchtext, + searchtype=>$searchtype)}, "first"); $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page-1), + $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= "first"; $paging_nav .= " ⋅ prev"; } + my $next_link = ''; if ($#commitlist >= 100) { - $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page+1), + $next_link = + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); + $paging_nav .= " ⋅ $next_link"; } else { $paging_nav .= " ⋅ next"; } - my $next_link = ''; + if ($#commitlist >= 100) { - $next_link = - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); } git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); @@ -5141,53 +5454,22 @@ sub git_search { git_print_page_nav('','', $hash,$co{'tree'},$hash); git_print_header_div('commit', esc_html($co{'title'}), $hash); - print "\n"; + print "
\n"; my $alternate = 1; $/ = "\n"; - my $git_command = git_cmd_str(); - my $searchqtext = $searchtext; - $searchqtext =~ s/'/'\\''/; - open my $fd, "-|", "$git_command rev-list $hash | " . - "$git_command diff-tree -r --stdin -S\'$searchqtext\'"; + open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts, + '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext", + ($search_use_regexp ? '--pickaxe-regex' : ()); undef %co; my @files; while (my $line = <$fd>) { - if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) { - my %set; - $set{'file'} = $6; - $set{'from_id'} = $3; - $set{'to_id'} = $4; - $set{'id'} = $set{'to_id'}; - if ($set{'id'} =~ m/0{40}/) { - $set{'id'} = $set{'from_id'}; - } - if ($set{'id'} =~ m/0{40}/) { - next; - } - push @files, \%set; - } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){ + chomp $line; + next unless $line; + + my %set = parse_difftree_raw_line($line); + if (defined $set{'commit'}) { + # finish previous commit if (%co) { - if ($alternate) { - print "\n"; - } else { - print "\n"; - } - $alternate ^= 1; - my $author = chop_and_escape_str($co{'author_name'}, 15, 5); - print "\n" . - "\n" . - "\n" . "\n" . "\n"; } - %co = parse_commit($1); + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + %co = parse_commit($set{'commit'}); + my $author = chop_and_escape_str($co{'author_name'}, 15, 5); + print "\n" . + "\n" . + "\n" . + "\n" . + "\n"; + } + print "
$co{'age_string_date'}" . $author . "" . - $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), - -class => "list subject"}, - chop_and_escape_str($co{'title'}, 50) . "
"); - while (my $setref = shift @files) { - my %set = %$setref; - print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, - hash=>$set{'id'}, file_name=>$set{'file'}), - -class => "list"}, - "" . esc_path($set{'file'}) . "") . - "
\n"; - } print "
" . $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . @@ -5196,11 +5478,44 @@ sub git_search { print "
$co{'age_string_date'}$author" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + chop_and_escape_str($co{'title'}, 50) . "
"); + } elsif (defined $set{'to_id'}) { + next if ($set{'to_id'} =~ m/^0{40}$/); + + print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, + hash=>$set{'to_id'}, file_name=>$set{'to_file'}), + -class => "list"}, + "" . esc_path($set{'file'}) . "") . + "
\n"; } } close $fd; + # finish last commit (warning: repetition!) + if (%co) { + print "
" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "
\n"; } @@ -5208,11 +5523,13 @@ sub git_search { git_print_page_nav('','', $hash,$co{'tree'},$hash); git_print_header_div('commit', esc_html($co{'title'}), $hash); - print "\n"; + print "
\n"; my $alternate = 1; my $matches = 0; $/ = "\n"; - open my $fd, "-|", git_cmd(), 'grep', '-n', '-i', '-E', $searchtext, $co{'tree'}; + open my $fd, "-|", git_cmd(), 'grep', '-n', + $search_use_regexp ? ('-E', '-i') : '-F', + $searchtext, $co{'tree'}; my $lastfile = ''; while (my $line = <$fd>) { chomp $line; @@ -5242,7 +5559,7 @@ sub git_search { print "
Binary file
\n"; } else { $ltext = untabify($ltext); - if ($ltext =~ m/^(.*)($searchtext)(.*)$/i) { + if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) { $ltext = esc_html($1, -nbsp=>1); $ltext .= ''; $ltext .= esc_html($2, -nbsp=>1); @@ -5277,27 +5594,31 @@ sub git_search_help { git_header_html(); git_print_page_nav('','', $hash,$hash,$hash); print <Pattern is by default a normal string that is matched precisely (but without +regard to case, except in the case of pickaxe). However, when you check the re checkbox, +the pattern entered is recognized as the POSIX extended +regular expression (also case +insensitive).

commit
-
The commit messages and authorship information will be scanned for the given string.
+
The commit messages and authorship information will be scanned for the given pattern.
EOT my ($have_grep) = gitweb_check_feature('grep'); if ($have_grep) { print <grep
All files in the currently selected tree (HEAD unless you are explicitly browsing - a different one) are searched for the given -regular expression -(POSIX extended) and the matches are listed. On large -trees, this search can take a while and put some strain on the server, so please use it with -some consideration.
+ a different one) are searched for the given pattern. On large trees, this search can take +a while and put some strain on the server, so please use it with some consideration. Note that +due to git-grep peculiarity, currently if regexp mode is turned off, the matches are +case-sensitive. EOT } print <author -
Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.
+
Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.
committer
-
Name and e-mail of the committer and date of commit will be scanned for the given string.
+
Name and e-mail of the committer and date of commit will be scanned for the given pattern.
EOT my ($have_pickaxe) = gitweb_check_feature('pickaxe'); if ($have_pickaxe) { @@ -5305,7 +5626,8 @@ EOT
pickaxe
All commits that caused the string to appear or disappear from any file (changes that added, removed or "modified" the string) will be listed. This search can take a while and -takes a lot of strain on the server, so please use it wisely.
+takes a lot of strain on the server, so please use it wisely. Note that since you may be +interested even in changes just changing the case as well, this search is case sensitive. EOT } print "
\n"; @@ -5322,13 +5644,17 @@ 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 = - $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1), + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } @@ -5351,12 +5677,12 @@ 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 my $head = $hash || 'HEAD'; - my @commitlist = parse_commits($head, 150, 0, undef, $file_name); + my @commitlist = parse_commits($head, 150, 0, $file_name); my %latest_commit; my %latest_date; @@ -5476,7 +5802,7 @@ XML or next; # print element (entry, item) - my $co_url = href(-full=>1, action=>"commit", hash=>$commit); + my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit); if ($format eq 'rss') { print "\n" . "" . esc_html($co{'title'}) . "\n" .