X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/2f46e604cc45c467d4921f7dda43c879cc55964e479842774f06ac78a746bdd1..64ed658324c681ab03a354df8dac0d502969c19a9c92e06b19454a9648c8181d:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index 89af5f9..1c279cc 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -18,6 +18,12 @@ use File::Find qw(); use File::Basename qw(basename); binmode STDOUT, ':utf8'; +our $t0; +if (eval { require Time::HiRes; 1; }) { + $t0 = [Time::HiRes::gettimeofday()]; +} +our $number_of_git_cmds = 0; + BEGIN { CGI->compile() if $ENV{'MOD_PERL'}; } @@ -91,11 +97,13 @@ our $stylesheet = undef; our $logo = "++GITWEB_LOGO++"; # URI of GIT favicon, assumed to be image/png type our $favicon = "++GITWEB_FAVICON++"; +# URI of gitweb.js (JavaScript code for gitweb) +our $javascript = "++GITWEB_JS++"; # URI and label (title) of GIT logo link #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; #our $logo_label = "git documentation"; -our $logo_url = "http://git.or.cz/"; +our $logo_url = "http://git-scm.com/"; our $logo_label = "git homepage"; # source of projects list @@ -161,7 +169,8 @@ our %known_snapshot_formats = ( # 'suffix' => filename suffix, # 'format' => --format for git-archive, # 'compressor' => [compressor command and arguments] - # (array reference, optional)} + # (array reference, optional) + # 'disabled' => boolean (optional)} # 'tgz' => { 'display' => 'tar.gz', @@ -177,6 +186,14 @@ our %known_snapshot_formats = ( 'format' => 'tar', 'compressor' => ['bzip2']}, + 'txz' => { + 'display' => 'tar.xz', + 'type' => 'application/x-xz', + 'suffix' => '.tar.xz', + 'format' => 'tar', + 'compressor' => ['xz'], + 'disabled' => 1}, + 'zip' => { 'display' => 'zip', 'type' => 'application/x-zip', @@ -189,6 +206,7 @@ our %known_snapshot_formats = ( our %known_snapshot_format_aliases = ( 'gzip' => 'tgz', 'bzip2' => 'tbz2', + 'xz' => 'txz', # backward compatibility: legacy gitweb config support 'x-gzip' => undef, 'gz' => undef, @@ -379,19 +397,29 @@ our %feature = ( # shortlog or commit will display an avatar associated with # the email of the committer(s) and/or author(s). - # Currently only the gravatar provider is available, and it - # depends on Digest::MD5. If an unknown provider is specified, - # the feature is disabled. + # Currently available providers are gravatar and picon. + # If an unknown provider is specified, the feature is disabled. + + # Gravatar depends on Digest::MD5. + # Picon currently relies on the indiana.edu database. # To enable system wide have in $GITWEB_CONFIG - # $feature{'avatar'}{'default'} = ['gravatar']; + # $feature{'avatar'}{'default'} = ['']; + # where is either gravatar or picon. # To have project specific config enable override in $GITWEB_CONFIG # $feature{'avatar'}{'override'} = 1; - # and in project config gitweb.avatar = gravatar; + # and in project config gitweb.avatar = ; 'avatar' => { 'sub' => \&feature_avatar, 'override' => 0, 'default' => ['']}, + + # Enable displaying how much time and how many git commands + # it took to generate and display page. Disabled by default. + # Project specific override is not supported. + 'timed' => { + 'override' => 0, + 'default' => [0]}, ); sub gitweb_get_feature { @@ -403,7 +431,7 @@ sub gitweb_get_feature { @{$feature{$name}{'default'}}); if (!$override) { return @defaults; } if (!defined $sub) { - warn "feature $name is not overrideable"; + warn "feature $name is not overridable"; return @defaults; } return $sub->(@defaults); @@ -492,7 +520,8 @@ sub filter_snapshot_fmts { exists $known_snapshot_format_aliases{$_} ? $known_snapshot_format_aliases{$_} : $_} @fmts; @fmts = grep { - exists $known_snapshot_formats{$_} } @fmts; + exists $known_snapshot_formats{$_} && + !$known_snapshot_formats{$_}{'disabled'}} @fmts; } our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; @@ -505,6 +534,7 @@ if (-e $GITWEB_CONFIG) { # version of the core git binary our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown"; +$number_of_git_cmds++; $projects_list ||= $projectroot; @@ -548,6 +578,8 @@ our %cgi_param_mapping = @cgi_param_mapping; # we will also need to know the possible actions, for validation our %actions = ( "blame" => \&git_blame, + "blame_incremental" => \&git_blame_incremental, + "blame_data" => \&git_blame_data, "blobdiff" => \&git_blobdiff, "blobdiff_plain" => \&git_blobdiff_plain, "blob" => \&git_blob, @@ -854,6 +886,8 @@ our @snapshot_fmts = gitweb_get_feature('snapshot'); our ($git_avatar) = gitweb_get_feature('avatar'); if ($git_avatar eq 'gravatar') { $git_avatar = '' unless (eval { require Digest::MD5; 1; }); +} elsif ($git_avatar eq 'picon') { + # no dependencies } else { $git_avatar = ''; } @@ -936,10 +970,13 @@ sub href { if (defined $params{'hash_parent_base'}) { $href .= esc_url($params{'hash_parent_base'}); # skip the file_parent if it's the same as the file_name - delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'}; - if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) { - $href .= ":/".esc_url($params{'file_parent'}); - delete $params{'file_parent'}; + if (defined $params{'file_parent'}) { + if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) { + delete $params{'file_parent'}; + } elsif ($params{'file_parent'} !~ /\.\./) { + $href .= ":/".esc_url($params{'file_parent'}); + delete $params{'file_parent'}; + } } $href .= ".."; delete $params{'hash_parent'}; @@ -1506,13 +1543,48 @@ sub format_subject_html { $long =~ s/[[:cntrl:]]/?/g; return $cgi->a({-href => $href, -class => "list subject", -title => to_utf8($long)}, - esc_html($short) . $extra); + esc_html($short)) . $extra; } else { return $cgi->a({-href => $href, -class => "list subject"}, - esc_html($long) . $extra); + esc_html($long)) . $extra; } } +# Rather than recomputing the url for an email multiple times, we cache it +# after the first hit. This gives a visible benefit in views where the avatar +# for the same email is used repeatedly (e.g. shortlog). +# The cache is shared by all avatar engines (currently gravatar only), which +# are free to use it as preferred. Since only one avatar engine is used for any +# given page, there's no risk for cache conflicts. +our %avatar_cache = (); + +# Compute the picon url for a given email, by using the picon search service over at +# http://www.cs.indiana.edu/picons/search.html +sub picon_url { + my $email = lc shift; + if (!$avatar_cache{$email}) { + my ($user, $domain) = split('@', $email); + $avatar_cache{$email} = + "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" . + "$domain/$user/" . + "users+domains+unknown/up/single"; + } + return $avatar_cache{$email}; +} + +# Compute the gravatar url for a given email, if it's not in the cache already. +# Gravatar stores only the part of the URL before the size, since that's the +# one computationally more expensive. This also allows reuse of the cache for +# different sizes (for this particular engine). +sub gravatar_url { + my $email = lc shift; + my $size = shift; + $avatar_cache{$email} ||= + "http://www.gravatar.com/avatar/" . + Digest::MD5::md5_hex($email) . "?s="; + return $avatar_cache{$email} . $size; +} + # Insert an avatar for the given $email at the given $size if the feature # is enabled. sub git_get_avatar { @@ -1523,11 +1595,11 @@ sub git_get_avatar { my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'}; my $url = ""; if ($git_avatar eq 'gravatar') { - $url = "http://www.gravatar.com/avatar/" . - Digest::MD5::md5_hex(lc $email) . "?s=$size"; + $url = gravatar_url($email, $size); + } elsif ($git_avatar eq 'picon') { + $url = picon_url($email); } - # Currently only gravatars are supported, but other forms such as - # picons can be added by putting an else up here and defining $url + # Other providers can be added by extending the if chain, defining $url # as needed. If no variant puts something in $url, we assume avatars # are completely disabled/unavailable. if ($url) { @@ -1535,6 +1607,7 @@ sub git_get_avatar { "" . $post_white; } else { return ""; @@ -1915,6 +1988,7 @@ sub get_feed_info { # returns path to the core git executable and the --git-dir parameter as list sub git_cmd { + $number_of_git_cmds++; return $GIT, '--git-dir='.$git_dir; } @@ -2530,7 +2604,7 @@ sub parse_commit_text { } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) { push @parents, $1; } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { - $co{'author'} = $1; + $co{'author'} = to_utf8($1); $co{'author_epoch'} = $2; $co{'author_tz'} = $3; if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) { @@ -2540,10 +2614,9 @@ sub parse_commit_text { $co{'author_name'} = $co{'author'}; } } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) { - $co{'committer'} = $1; + $co{'committer'} = to_utf8($1); $co{'committer_epoch'} = $2; $co{'committer_tz'} = $3; - $co{'committer_name'} = $co{'committer'}; if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) { $co{'committer_name'} = $1; $co{'committer_email'} = $2; @@ -3165,6 +3238,20 @@ sub git_footer_html { } print "\n"; # class="page_footer" + if (defined $t0 && gitweb_check_feature('timed')) { + print "
\n"; + print 'This page took '. + ''. + Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + ' seconds '. + ' and '. + ''. + $number_of_git_cmds. + ' git commands '. + " to generate.\n"; + print "
\n"; # class="page_footer" + } + if (-f $site_footer) { insert_file($site_footer); } @@ -4718,7 +4805,9 @@ sub git_tag { git_footer_html(); } -sub git_blame { +sub git_blame_common { + my $format = shift || 'porcelain'; + # permissions gitweb_check_feature('blame') or die_error(403, "Blame view not allowed"); @@ -4740,10 +4829,43 @@ sub git_blame { } } - # run git-blame --porcelain - open my $fd, "-|", git_cmd(), "blame", '-p', - $hash_base, '--', $file_name - or die_error(500, "Open git-blame failed"); + my $fd; + if ($format eq 'incremental') { + # get file contents (as base) + open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash + or die_error(500, "Open git-cat-file failed"); + } elsif ($format eq 'data') { + # run git-blame --incremental + open $fd, "-|", git_cmd(), "blame", "--incremental", + $hash_base, "--", $file_name + or die_error(500, "Open git-blame --incremental failed"); + } else { + # run git-blame --porcelain + open $fd, "-|", git_cmd(), "blame", '-p', + $hash_base, '--', $file_name + or die_error(500, "Open git-blame --porcelain failed"); + } + + # incremental blame data returns early + if ($format eq 'data') { + print $cgi->header( + -type=>"text/plain", -charset => "utf-8", + -status=> "200 OK"); + local $| = 1; # output autoflush + print while <$fd>; + close $fd + or print "ERROR $!\n"; + + print 'END'; + if (defined $t0 && gitweb_check_feature('timed')) { + print ' '. + Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + ' '.$number_of_git_cmds; + } + print "\n"; + + return; + } # page header git_header_html(); @@ -4754,93 +4876,170 @@ sub git_blame { $cgi->a({-href => href(action=>"history", -replay=>1)}, "history") . " | " . - $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + $cgi->a({-href => href(action=>$action, 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, $ftype, $hash_base); # page body - my @rev_color = qw(light2 dark2); + if ($format eq 'incremental') { + print "\n"; + + print qq!
\n!; + } + + print qq!
\n!; + print qq!
... / ...
\n! + if ($format eq 'incremental'); + print qq!\n!. + #qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!; + + my @rev_color = qw(light dark); my $num_colors = scalar(@rev_color); my $current_color = 0; - my %metainfo = (); - print < -
CommitLineData
- -HTML - LINE: - while (my $line = <$fd>) { - chomp $line; - # the header: [] - # no for subsequent lines in group of lines - my ($full_rev, $orig_lineno, $lineno, $group_size) = - ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/); - if (!exists $metainfo{$full_rev}) { - $metainfo{$full_rev} = {}; - } - my $meta = $metainfo{$full_rev}; - my $data; - while ($data = <$fd>) { - chomp $data; - last if ($data =~ s/^\t//); # contents of line - if ($data =~ /^(\S+) (.*)$/) { - $meta->{$1} = $2; - } - } - my $short_rev = substr($full_rev, 0, 8); - my $author = $meta->{'author'}; - my %date = - parse_date($meta->{'author-time'}, $meta->{'author-tz'}); - my $date = $date{'iso-tz'}; - if ($group_size) { - $current_color = ($current_color + 1) % $num_colors; - } - print "\n"; - if ($group_size) { - print "\n"; + if ($format eq 'incremental') { + my $color_class = $rev_color[$current_color]; + + #contents of a file + my $linenr = 0; + LINE: + while (my $line = <$fd>) { + chomp $line; + $linenr++; + + print qq!!. + qq!!. + qq!!; + print qq!\n"; + print qq!\n!; } - my $parent_commit; - if (!exists $meta->{'parent'}) { - open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") - or die_error(500, "Open git-rev-parse failed"); - $parent_commit = <$dd>; - close $dd; - chomp($parent_commit); - $meta->{'parent'} = $parent_commit; - } else { - $parent_commit = $meta->{'parent'}; - } - my $blamed = href(action => 'blame', - file_name => $meta->{'filename'}, - hash_base => $parent_commit); - print ""; - print "\n"; - print "\n"; + + } else { # porcelain, i.e. ordinary blame + my %metainfo = (); # saves information about commits + + # blame data + LINE: + while (my $line = <$fd>) { + chomp $line; + # the header: [] + # no for subsequent lines in group of lines + my ($full_rev, $orig_lineno, $lineno, $group_size) = + ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/); + if (!exists $metainfo{$full_rev}) { + $metainfo{$full_rev} = { 'nprevious' => 0 }; + } + my $meta = $metainfo{$full_rev}; + my $data; + while ($data = <$fd>) { + chomp $data; + last if ($data =~ s/^\t//); # contents of line + if ($data =~ /^(\S+)(?: (.*))?$/) { + $meta->{$1} = $2 unless exists $meta->{$1}; + } + if ($data =~ /^previous /) { + $meta->{'nprevious'}++; + } + } + my $short_rev = substr($full_rev, 0, 8); + my $author = $meta->{'author'}; + my %date = + parse_date($meta->{'author-time'}, $meta->{'author-tz'}); + my $date = $date{'iso-tz'}; + if ($group_size) { + $current_color = ($current_color + 1) % $num_colors; + } + my $tr_class = $rev_color[$current_color]; + $tr_class .= ' boundary' if (exists $meta->{'boundary'}); + $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0); + $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1); + print "\n"; + if ($group_size) { + print "\n"; + } + # 'previous' + if (exists $meta->{'previous'} && + $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) { + $meta->{'parent'} = $1; + $meta->{'file_parent'} = unquote($2); + } + my $linenr_commit = + exists($meta->{'parent'}) ? + $meta->{'parent'} : $full_rev; + my $linenr_filename = + exists($meta->{'file_parent'}) ? + $meta->{'file_parent'} : unquote($meta->{'filename'}); + my $blamed = href(action => 'blame', + file_name => $linenr_filename, + hash_base => $linenr_commit); + print ""; + print "\n"; + print "\n"; + } # end while + } - print "
CommitLineData
1); - print ">"; - print $cgi->a({-href => href(action=>"commit", - hash=>$full_rev, - file_name=>$file_name)}, - esc_html($short_rev)); - print "
!. + qq!$linenr! . esc_html($line) . "
"; - print $cgi->a({ -href => "$blamed#l$orig_lineno", - -class => "linenr" }, - esc_html($lineno)); - print "" . esc_html($data) . "
1); + print ">"; + print $cgi->a({-href => href(action=>"commit", + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($short_rev)); + if ($group_size >= 2) { + my @author_initials = ($author =~ /\b([[:upper:]])\B/g); + if (@author_initials) { + print "
" . + esc_html(join('', @author_initials)); + # or join('.', ...) + } + } + print "
"; + print $cgi->a({ -href => "$blamed#l$orig_lineno", + -class => "linenr" }, + esc_html($lineno)); + print "" . esc_html($data) . "
\n"; - print "
"; + + # footer + print "\n". + "\n"; # class="blame" + print "\n"; # class="blame_body" close $fd or print "Reading blob failed\n"; - # page footer + if ($format eq 'incremental') { + print qq!\n!. + qq!\n!; + } + git_footer_html(); } +sub git_blame { + git_blame_common(); +} + +sub git_blame_incremental { + git_blame_common('incremental'); +} + +sub git_blame_data { + git_blame_common('data'); +} + sub git_tags { my $head = git_get_head_hash($project); git_header_html(); @@ -5121,6 +5320,8 @@ sub git_snapshot { die_error(400, "Invalid snapshot format parameter"); } elsif (!exists($known_snapshot_formats{$format})) { die_error(400, "Unknown snapshot format"); + } elsif ($known_snapshot_formats{$format}{'disabled'}) { + die_error(403, "Snapshot format not allowed"); } elsif (!grep($_ eq $format, @snapshot_fmts)) { die_error(403, "Unsupported snapshot format"); }