From 6de9433fd0525632094f3cc172a606f217fa2097 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sat, 25 Jul 2009 00:44:02 +0200 Subject: gitweb: Mark boundary commits in 'blame' view Use "boundary" class to mark boundary commits, which currently results in using bold weight font for SHA-1 of a commit (to be more exact for all text in the first cell in row, that contains SHA-1 of a commit). Detecting boundary commits is done by watching for "boundary" header in "git blame -p" output. Because this header doesn't carry additional data the regular expression for blame header fields had to be slightly adjusted. With current gitweb API only root (parentless) commits can be boundary commits. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 7fbd5ff89e..3078b9280b 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4826,7 +4826,7 @@ HTML while ($data = <$fd>) { chomp $data; last if ($data =~ s/^\t//); # contents of line - if ($data =~ /^(\S+) (.*)$/) { + if ($data =~ /^(\S+)(?: (.*))?$/) { $meta->{$1} = $2; } } @@ -4838,7 +4838,9 @@ HTML if ($group_size) { $current_color = ($current_color + 1) % $num_colors; } - print "\n"; + my $tr_class = $rev_color[$current_color]; + $tr_class .= ' boundary' if (exists $meta->{'boundary'}); + print "\n"; if ($group_size) { print " Date: Sat, 25 Jul 2009 00:44:03 +0200 Subject: gitweb: Use "previous" header of git-blame -p in 'blame' view Luben Tuikov changed 'lineno' link (line number link) from pointing to 'blame' view at given line at blamed commit, to the one at parent of blamed commit in 244a70e (Blame "linenr" link jumps to previous state at "orig_lineno", 2007-01-04). This made it possible to do data mining using 'blame' view, by going through history of a line using mentioned line number link. Original implementation called "git rev-parse ^" to find SHA-1 of a parent of a given commit once per each blamed line. In 39c19ce (gitweb: cache $parent_commit info in git_blame(), 2008-12-11) this was improved so rev-parse was called once per each unique commit in git-blame output. Alternate solution would be to relax validation for 'hb' parameter by allowing extended SHA-1 syntax of the form ^ (perhaps redirecting to gitweb URL with ^ resolved, in practice moving call to rev-parse to 'the other side of link'). This solution had a bug that it didn't work for boundary commits. Boundary commits don't have parents, so "git rev-parse ^" returned literal "^" (which didn't exists). Gitweb didn't detect this situation and passed this result literally as 'hb' parameter in 'linenr' link. Following such link currently gives 400 - Invalid hash base parameter error; 'hb' parameter is restricted via validate_refname to correct refnames and doesn't allow for extended SHA-1 syntax. This bug could have been fixed alternatively by checking if commit is boundary commit, or check if rev-parse result is unchanged (still ends in '^' prefix). The solution employing rev-parse to find parent of commit had inherent problem if blamed commit renamed file; then name of file would be different in its parent. Solving this outside git-blame would be difficult and costly (at least cost of additional fork for extra git command). Currently gitweb uses information in "previous" header, which was introduced by Junio C Hamano in 96e1170 (blame: show "previous" information in --porcelain/--incremental format, 2008-06-04) This (currently undocumented) header has the following format: "previous " Using "previous" header solves both problem of performance and the problem that blamed commit could have renaming blamed file. Because "previous" header can be repeated for the same commit when blamed commit is merge (has more than one parent), and we are interested usually in _first_ parent, currently we store only first value if blame header repeats. Using first parent (first "previous" line) was what gitweb did before; without this change gitweb would use last parent instead. If there is no previous commit 'linenr' link points to blamed commit and blamed filename, making it work correctly for boundary commits. Acked-by: Luben Tuikov Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 3078b9280b..b8a121bb98 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4827,7 +4827,7 @@ HTML chomp $data; last if ($data =~ s/^\t//); # contents of line if ($data =~ /^(\S+)(?: (.*))?$/) { - $meta->{$1} = $2; + $meta->{$1} = $2 unless exists $meta->{$1}; } } my $short_rev = substr($full_rev, 0, 8); @@ -4852,20 +4852,21 @@ HTML esc_html($short_rev)); print "\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'}; - } + # '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 => $meta->{'filename'}, - hash_base => $parent_commit); + file_name => $linenr_filename, + hash_base => $linenr_commit); print ""; print $cgi->a({ -href => "$blamed#l$orig_lineno", -class => "linenr" }, -- cgit v1.3 From 3665e7e7f2b210f6896815d563255c364a861a6e Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sat, 25 Jul 2009 00:44:04 +0200 Subject: gitweb: Mark commits with no "previous" in 'blame' view Use "no-previous" class to mark blamed commits which do not have "previous" header. Those are commits in which blamed file was created (added); this includes boundary commits. This means that 'linenr' link leads to blamed commit, not (one of) parent of blamed commit. Therefore currently line number for such commit uses bold weight font to denote this situation; the effect is subtle. Use "multiple-previous" class in the opposite situation, where blamed commit has multiple "previous" headers (is an evil merge). Currently this class is not used for styling. In this situation 'linenr' link leads to first of "previous" commits (first parent). Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.css | 3 ++- gitweb/gitweb.perl | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index f47709bac5..47633376d1 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -242,7 +242,8 @@ tr.dark:hover { background-color: #edece6; } -tr.boundary td.sha1 { +tr.boundary td.sha1, +tr.no-previous td.linenr { font-weight: bold; } diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index b8a121bb98..128bddd381 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4819,7 +4819,7 @@ HTML my ($full_rev, $orig_lineno, $lineno, $group_size) = ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/); if (!exists $metainfo{$full_rev}) { - $metainfo{$full_rev} = {}; + $metainfo{$full_rev} = { 'nprevious' => 0 }; } my $meta = $metainfo{$full_rev}; my $data; @@ -4829,6 +4829,9 @@ HTML 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'}; @@ -4840,6 +4843,8 @@ HTML } 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 " Date: Sat, 25 Jul 2009 00:44:05 +0200 Subject: gitweb: Add author initials in 'blame' view, a la "git gui blame" For example for "Junio C Hamano" initials would be "JH". Of course initials are added (below shortened SHA-1 of blamed commit) only if group of lines that blame the same commit has 2 or more lines in it. Initials are extracted using i18n /\b([[:upper:]])\B/g regexp. Additionally initials help to distinguish boundary commits, as they use bold weight font too (in addition to shortened SHA-1 of commit). Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 128bddd381..ea1ab5f846 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4855,6 +4855,14 @@ HTML 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 "\n"; } # 'previous' -- cgit v1.3 From aef37684ea713c96dc3e4913cf33962df1efb92b Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sat, 25 Jul 2009 00:44:06 +0200 Subject: gitweb: Use light/dark for class names also in 'blame' view Instead of using "light2" and "dark2" for class names in 'blame' view (in place of "light" and "dark" classes in other places) to avoid changing style on hover in 'blame' view while doing it for other views (like 'shortlog'), use more advanced CSS, relying on the fact that more specific selector wins. While at it add a few comments to gitweb CSS file, and consolidate some repeated info. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.css | 17 ++++++++++------- gitweb/gitweb.perl | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 47633376d1..8f68fe3091 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -226,22 +226,25 @@ th { text-align: left; } -tr.light:hover { - background-color: #edece6; -} - -tr.dark { - background-color: #f6f6f0; +/* do not change row style on hover for 'blame' view */ +tr.light, +table.blame .light:hover { + background-color: #ffffff; } -tr.dark2 { +tr.dark, +table.blame .dark:hover { background-color: #f6f6f0; } +/* currently both use the same, but it can change */ +tr.light:hover, tr.dark:hover { background-color: #edece6; } +/* boundary commits in 'blame' view */ +/* and commits without "previous" */ tr.boundary td.sha1, tr.no-previous td.linenr { font-weight: bold; diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index ea1ab5f846..2cb60bedc6 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4801,7 +4801,7 @@ sub git_blame { git_print_page_path($file_name, $ftype, $hash_base); # page body - my @rev_color = qw(light2 dark2); + my @rev_color = qw(light dark); my $num_colors = scalar(@rev_color); my $current_color = 0; my %metainfo = (); -- cgit v1.3 From b7da721f02638083cc3debfd9ae7221d76a96b26 Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Fri, 31 Jul 2009 08:48:49 +0200 Subject: gitweb: fix 'Use of uninitialized value' error in href() Equality between file_parent and file_name was being checked without a preliminary check for existence of the parameters. Fix by wrapping the equality check in appropriate if (defined ...), rearranging the lines to prevent excessive length. Signed-off-by: Giuseppe Bilotta Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 7fbd5ff89e..37120a3e60 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -940,10 +940,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'}; -- cgit v1.3 From 1bfd363184d5bd0eb81565bfa2cc1084f35dbbdf Mon Sep 17 00:00:00 2001 From: Mark A Rada Date: Thu, 6 Aug 2009 10:25:39 -0400 Subject: gitweb: support to globally disable a snapshot format Allow Gitweb administrators to set a 'disabled' key in the %known_snapshot_formats hash to disable a specific snapshot format. All formats are enabled by default to maintain backwards compatibility. Signed-off-by: Mark Rada Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 37120a3e60..a0cdf31666 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -160,7 +160,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', @@ -494,7 +495,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++"; @@ -5166,6 +5168,8 @@ sub git_snapshot { die_error(400, "Unknown snapshot format"); } elsif (!grep($_ eq $format, @snapshot_fmts)) { die_error(403, "Unsupported snapshot format"); + } elsif ($known_snapshot_formats{$format}{'disabled'}) { + die_error(403, "Snapshot format not allowed"); } if (!defined $hash) { -- cgit v1.3 From cbdefb5ac43a1a34e71121a7a6a6434f0b8aa1cf Mon Sep 17 00:00:00 2001 From: Mark A Rada Date: Thu, 6 Aug 2009 10:28:25 -0400 Subject: gitweb: add support for XZ compressed snapshots The XZ compression format uses the LZMA2 compression algorithm, which often yields higher compression ratios than both GZip and BZip2 at the cost of using more CPU time and RAM. XZ is the slowest for compression, but still much faster than BZip2 for decompression, almost comparable to GZip (see benchmarks below). Some simple benchmarks show the pros and cons of using XZ compression; starting with an already tarball'd archive of the repos listed below. Memory usage seemed to be consistent for any given algorithm at their respective default compression levels. CPU: AMD Sempron 3400+ (1 core @ 1.8GHz with 256K L2 cache) Virtual Memory Usage GZip: 4152K BZip2: 13352K XZ: 102M Linux 2.6 series (f5886c7f96f2542382d3a983c5f13e03d7fc5259) 349M gzip 23.70s user 0.47s system 99% cpu 24.227 total 76M gunzip 3.74s user 0.74s system 94% cpu 4.741 total bzip2 130.96s user 0.53s system 99% cpu 2:11.97 total 59M bunzip2 31.05s user 1.02s system 99% cpu 32.355 total xz 448.78s user 0.91s system 99% cpu 7:31.28 total 51M unxz 7.67s user 0.80s system 98% cpu 8.607 total Git (0a53e9ddeaddad63ad106860237bbf53411d11a7) 11M gzip 0.77s user 0.03s system 99% cpu 0.792 total 2.5M gunzip 0.12s user 0.02s system 98% cpu 0.142 total bzip2 3.42s user 0.02s system 99% cpu 3.454 total 2.1M bunzip2 0.95s user 0.03s system 99% cpu 0.984 total xz 12.88s user 0.14s system 98% cpu 13.239 total 1.9M unxz 0.27s user 0.03s system 99% cpu 0.298 total XZ (669413bb2db954bbfde3c4542fddbbab53891eb4) 1.8M gzip 0.12s user 0.00s system 95% cpu 0.132 total 442K gunzip 0.02s user 0.00s system 97% cpu 0.027 total bzip2 1.28s user 0.01s system 99% cpu 1.298 total 363K bunzip2 0.15s user 0.01s system 100% cpu 0.157 total xz 1.62s user 0.03s system 99% cpu 1.652 total 347K unxz 0.05s user 0.00s system 99% cpu 0.058 total From a time and memory perspective, nothing compares to GZip, but if given an average upload speed of 20KB/s, it would take ~400 seconds longer to transfer the BZip2'd kernel snapshot than the XZ snapshot; the transfer time difference is even greater between GZip and XZ. The real time savings are relatively the same for all test cases, but less dramatic for smaller repositories. XZ decompresses ~1.8-2 times slower than GZip, and ~2.7-3.75 times faster than BZip2; XZ gets relatively faster as snapshots get larger. However, XZ takes relatively longer to compress as snapshots get larger. The downside for XZ'd snapshots is the large CPU and memory load put on the server to generate the compressed snapshot, though XZ will eventually have threading support, and the real clock time for making XZ'd snapshots would decrease if the server had a beefy multi-core CPU. XZ compression is disabled by default to allow upgrades to take place without any surprises, as the CPU and memory requirements will be an issue for high load or lightweight servers. Also, the XZ format is still new (format declared stable ~6 months ago), and there have been no "stable" releases of the utils yet. Signed-off-by: Mark Rada Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index a0cdf31666..84659f5672 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -177,6 +177,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 +197,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, -- cgit v1.3 From 01b89f0cd5d1c36990bbd3595b7d54c5f67d01e3 Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Sun, 23 Aug 2009 10:28:09 +0200 Subject: gitweb: pull ref markes pull out of subject element Since 4afbaef (gitweb: ref markers link to named shortlogs, 2008-09-02), ref markers that accompany the subject in views such as shortlog and history point to something different from the subject itself. Therefore, they should not be included in the same element. Benefits of the change are: * better compliance to the XHTML standards, that forbid links within links even though the restriction cannot be imposed via DTD; this also benefits visualization in some older browsers; * when hovering the subject, only the subject itself is underlined; when hovering the ref markers, only the text in the hovered ref marker is underlined; previously, hovering any written part of the subject column led to complete underlying of everything at the same time, with unpleasing effects. Signed-off-by: Giuseppe Bilotta Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index be7358fdeb..36cf4a216a 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -1513,10 +1513,10 @@ 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; } } -- cgit v1.3 From 34b31a8d5f48283ec3039777b24706585b4f9d41 Mon Sep 17 00:00:00 2001 From: Mark Rada Date: Tue, 25 Aug 2009 00:59:48 -0400 Subject: gitweb: improve snapshot error handling The last check in the second block of checks in the &git_snapshot routine is never executed because the second to last check is a superset of the last check. Switch the order of the last two checks. It has the advantage of giving clients a more specific reason why they cannot get a snapshot format if the format they have chosen is disabled. Signed-off-by: Mark Rada Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 84659f5672..b453ed0452 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5175,10 +5175,10 @@ sub git_snapshot { die_error(400, "Invalid snapshot format parameter"); } elsif (!exists($known_snapshot_formats{$format})) { die_error(400, "Unknown snapshot format"); - } elsif (!grep($_ eq $format, @snapshot_fmts)) { - die_error(403, "Unsupported 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"); } if (!defined $hash) { -- cgit v1.3 From aa7dd05e6a622ec17d034980c4440b0aed583c6e Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 1 Sep 2009 13:39:16 +0200 Subject: gitweb: Add optional "time to generate page" info in footer Add "This page took XXX seconds and Y git commands to generate" to page footer, if global feature 'timed' is enabled (disabled by default). Requires Time::HiRes installed for high precision 'wallclock' time. Note that Time::HiRes is being required unconditionally; this is because setting $t0 variable needs to be done fairly early to have running time of the whole script. If Time::HiRes module were required only if 'timed' feature is enabled, the earliest place where starting time ($t0) could be calculated would be after reading gitweb config, making "time to generate page" info inaccurate. This code is based on example code by Petr 'Pasky' Baudis. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.css | 7 +++++++ gitweb/gitweb.perl | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 8f68fe3091..9893443edc 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -75,6 +75,13 @@ div.page_footer_text { font-style: italic; } +div#generating_info { + margin: 4px; + font-size: smaller; + text-align: center; + color: #505050; +} + div.page_body { padding: 8px; font-family: monospace; diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 2cb60bedc6..18cbf35391 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/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'}; } @@ -394,6 +400,13 @@ our %feature = ( '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 { @@ -507,6 +520,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; @@ -1955,6 +1969,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; } @@ -3205,6 +3220,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); } -- cgit v1.3 From 4af819d4cad206648b832f4d6103547981ab8004 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 1 Sep 2009 13:39:17 +0200 Subject: gitweb: Incremental blame (using JavaScript) Add 'blame_incremental' view, which uses "git blame --incremental" and JavaScript (Ajax), where 'blame' use "git blame --porcelain". * gitweb generates initial info by putting file contents (from "git cat-file") together with line numbers in blame table * then gitweb makes web browser JavaScript engine call startBlame() function from gitweb.js * startBlame() opens XMLHttpRequest connection to 'blame_data' view, which in turn calls "git blame --incremental" for a file, and streams output of git-blame to JavaScript (gitweb.js) * XMLHttpRequest event handler updates line info in blame view as soon as it gets data from 'blame_data' (from server), and it also updates progress info * when 'blame_data' ends, and gitweb.js finishes updating line info, it fixes colors to match (as far as possible) ordinary 'blame' view, and updates information about how long it took to generate page. Gitweb deals with streamed 'blame_data' server errors by displaying them in the progress info area (just in case). The 'blame_incremental' view tries to be equivalent to 'blame' action; there are however a few differences in output between 'blame' and 'blame_incremental' view: * 'blame_incremental' always used query form for this part of link(s) which is generated by JavaScript code. The difference is visible if we use path_info link (pass some or all arguments in path_info). Changing this would require implementing something akin to href() subroutine from gitweb.perl in JavaScript (in gitweb.js). * 'blame_incremental' always uses "rowspan" attribute, even if rowspan="1". This simplifies code, and is not visible to user. * The progress bar and progress info are still there even after JavaScript part of 'blame_incremental' finishes work. Note that currently no link generated by gitweb leads to this new view. This code is based on patch by Petr Baudis patch, which in turn was tweaked up version of Fredrik Kuivinen 's proof of concept patch. This patch adds GITWEB_JS compile configuration option, and modifies git-instaweb.sh to take gitweb.js into account. The code for git-instaweb.sh was taken from Pasky's patch. Signed-off-by: Fredrik Kuivinen Signed-off-by: Petr Baudis Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- Makefile | 6 +- git-instaweb.sh | 7 + gitweb/README | 4 + gitweb/gitweb.css | 11 + gitweb/gitweb.js | 726 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gitweb/gitweb.perl | 274 +++++++++++++------- 6 files changed, 940 insertions(+), 88 deletions(-) create mode 100644 gitweb/gitweb.js (limited to 'gitweb/gitweb.perl') diff --git a/Makefile b/Makefile index daf4296706..2ad3b1e40c 100644 --- a/Makefile +++ b/Makefile @@ -265,6 +265,7 @@ GITWEB_HOMETEXT = indextext.html GITWEB_CSS = gitweb.css GITWEB_LOGO = git-logo.png GITWEB_FAVICON = git-favicon.png +GITWEB_JS = gitweb.js GITWEB_SITE_HEADER = GITWEB_SITE_FOOTER = @@ -1407,13 +1408,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ + -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \ -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ $< >$@+ && \ chmod +x $@+ && \ mv $@+ $@ -git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css +git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/gitweb.js $(QUIET_GEN)$(RM) $@ $@+ && \ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ @@ -1422,6 +1424,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css -e '/@@GITWEB_CGI@@/d' \ -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \ -e '/@@GITWEB_CSS@@/d' \ + -e '/@@GITWEB_JS@@/r gitweb/gitweb.js' \ + -e '/@@GITWEB_JS@@/d' \ -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \ $@.sh > $@+ && \ chmod +x $@+ && \ diff --git a/git-instaweb.sh b/git-instaweb.sh index 5f4419b69b..6f381c88da 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -331,8 +331,15 @@ gitweb_css () { EOFGITWEB } +gitweb_js () { + cat > "$1" <<\EOFGITWEB +@@GITWEB_JS@@ +EOFGITWEB +} + gitweb_cgi "$GIT_DIR/gitweb/gitweb.cgi" gitweb_css "$GIT_DIR/gitweb/gitweb.css" +gitweb_js "$GIT_DIR/gitweb/gitweb.js" case "$httpd" in *lighttpd*) diff --git a/gitweb/README b/gitweb/README index 9056d1e090..cbcd993ecb 100644 --- a/gitweb/README +++ b/gitweb/README @@ -92,6 +92,10 @@ You can specify the following configuration variables when building GIT: web browsers that support favicons (website icons) may display them in the browser's URL bar and next to site name in bookmarks). Relative to base URI of gitweb. [Default: git-favicon.png] + * GITWEB_JS + Points to the localtion where you put gitweb.js on your web server + (or to be more generic URI of JavaScript code used by gitweb). + Relative to base URI of gitweb. [Default: gitweb.js] * GITWEB_CONFIG This Perl file will be loaded using 'do' and can be used to override any of the options above as well as some other options -- see the "Runtime diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 9893443edc..4a2e496568 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -348,6 +348,17 @@ td.mode { font-family: monospace; } +/* progress of blame_interactive */ +div#progress_bar { + height: 2px; + margin-bottom: -2px; + background-color: #d8d9d0; +} +div#progress_info { + float: right; + text-align: right; +} + /* styling of diffs (patchsets): commitdiff and blobdiff views */ div.diff.header, div.diff.extended_header { diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js new file mode 100644 index 0000000000..b15e2a7944 --- /dev/null +++ b/gitweb/gitweb.js @@ -0,0 +1,726 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2009, Jakub Narebski + +/** + * @fileOverview JavaScript code for gitweb (git web interface). + * @license GPLv2 or later + */ + +/* + * This code uses DOM methods instead of (nonstandard) innerHTML + * to modify page. + * + * innerHTML is non-standard IE extension, though supported by most + * browsers; however Firefox up to version 1.5 didn't implement it in + * a strict mode (application/xml+xhtml mimetype). + * + * Also my simple benchmarks show that using elem.firstChild.data = + * 'content' is slightly faster than elem.innerHTML = 'content'. It + * is however more fragile (text element fragment must exists), and + * less feature-rich (we cannot add HTML). + * + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the + * equivalent using DOM 2 Core is usually shown in comments. + */ + + +/* ============================================================ */ +/* generic utility functions */ + + +/** + * pad number N with nonbreakable spaces on the left, to WIDTH characters + * example: padLeftStr(12, 3, ' ') == ' 12' + * (' ' is nonbreakable space) + * + * @param {Number|String} input: number to pad + * @param {Number} width: visible width of output + * @param {String} str: string to prefix to string, e.g. ' ' + * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR + */ +function padLeftStr(input, width, str) { + var prefix = ''; + + width -= input.toString().length; + while (width > 1) { + prefix += str; + width--; + } + return prefix + input; +} + +/** + * Pad INPUT on the left to SIZE width, using given padding character CH, + * for example padLeft('a', 3, '_') is '__a'. + * + * @param {String} input: input value converted to string. + * @param {Number} width: desired length of output. + * @param {String} ch: single character to prefix to string. + * + * @returns {String} Modified string, at least SIZE length. + */ +function padLeft(input, width, ch) { + var s = input + ""; + while (s.length < width) { + s = ch + s; + } + return s; +} + +/** + * Create XMLHttpRequest object in cross-browser way + * @returns XMLHttpRequest object, or null + */ +function createRequestObject() { + try { + return new XMLHttpRequest(); + } catch (e) {} + try { + return window.createRequest(); + } catch (e) {} + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) {} + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + + return null; +} + +/* ============================================================ */ +/* utility/helper functions (and variables) */ + +var xhr; // XMLHttpRequest object +var projectUrl; // partial query + separator ('?' or ';') + +// 'commits' is an associative map. It maps SHA1s to Commit objects. +var commits = {}; + +/** + * constructor for Commit objects, used in 'blame' + * @class Represents a blamed commit + * @param {String} sha1: SHA-1 identifier of a commit + */ +function Commit(sha1) { + if (this instanceof Commit) { + this.sha1 = sha1; + this.nprevious = 0; /* number of 'previous', effective parents */ + } else { + return new Commit(sha1); + } +} + +/* ............................................................ */ +/* progress info, timing, error reporting */ + +var blamedLines = 0; +var totalLines = '???'; +var div_progress_bar; +var div_progress_info; + +/** + * Detects how many lines does a blamed file have, + * This information is used in progress info + * + * @returns {Number|String} Number of lines in file, or string '...' + */ +function countLines() { + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + if (table) { + return table.getElementsByTagName('tr').length - 1; // for header + } else { + return '...'; + } +} + +/** + * update progress info and length (width) of progress bar + * + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines + */ +function updateProgressInfo() { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (!div_progress_bar) { + div_progress_bar = document.getElementById('progress_bar'); + } + if (!div_progress_info && !div_progress_bar) { + return; + } + + var percentage = Math.floor(100.0*blamedLines/totalLines); + + if (div_progress_info) { + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + + ' (' + padLeftStr(percentage, 3, ' ') + '%)'; + } + + if (div_progress_bar) { + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); + div_progress_bar.style.width = percentage + '%'; + } +} + + +var t_interval_server = ''; +var cmds_server = ''; +var t0 = new Date(); + +/** + * write how much it took to generate data, and to run script + * + * @globals t0, t_interval_server, cmds_server + */ +function writeTimeInterval() { + var info_time = document.getElementById('generating_time'); + if (!info_time || !t_interval_server) { + return; + } + var t1 = new Date(); + info_time.firstChild.data += ' + (' + + t_interval_server + ' sec server blame_data / ' + + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; + + var info_cmds = document.getElementById('generating_cmd'); + if (!info_time || !cmds_server) { + return; + } + info_cmds.firstChild.data += ' + ' + cmds_server; +} + +/** + * show an error message alert to user within page (in prohress info area) + * @param {String} str: plain text error message (no HTML) + * + * @globals div_progress_info + */ +function errorInfo(str) { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (div_progress_info) { + div_progress_info.className = 'error'; + div_progress_info.firstChild.data = str; + } +} + +/* ............................................................ */ +/* coloring rows like 'blame' after 'blame_data' finishes */ + +/** + * returns true if given row element (tr) is first in commit group + * to be used only after 'blame_data' finishes (after processing) + * + * @param {HTMLElement} tr: table row + * @returns {Boolean} true if TR is first in commit group + */ +function isStartOfGroup(tr) { + return tr.firstChild.className === 'sha1'; +} + +var colorRe = /(?:light|dark)/; + +/** + * change colors to use zebra coloring (2 colors) instead of 3 colors + * concatenate neighbour commit groups belonging to the same commit + * + * @globals colorRe + */ +function fixColorsAndGroups() { + var colorClasses = ['light', 'dark']; + var linenum = 1; + var tr, prev_group; + var colorClass = 0; + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + while ((tr = document.getElementById('l'+linenum))) { + // index origin is 0, which is table header; start from 1 + //while ((tr = table.rows[linenum])) { // <- it is slower + if (isStartOfGroup(tr, linenum, document)) { + if (prev_group && + prev_group.firstChild.firstChild.href === + tr.firstChild.firstChild.href) { + // we have to concatenate groups + var prev_rows = prev_group.firstChild.rowSpan || 1; + var curr_rows = tr.firstChild.rowSpan || 1; + prev_group.firstChild.rowSpan = prev_rows + curr_rows; + //tr.removeChild(tr.firstChild); + tr.deleteCell(0); // DOM2 HTML way + } else { + colorClass = (colorClass + 1) % 2; + prev_group = tr; + } + } + var tr_class = tr.className; + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); + linenum++; + } +} + +/* ............................................................ */ +/* time and data */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/; + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + * + * @globals tzRe + */ +function formatDateISOLocal(epoch, timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/* ............................................................ */ +/* unquoting/unescaping filenames */ + +/**#@+ + * @constant + */ +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; +var octEscRe = /^[0-7]{1,3}$/; +var maybeQuotedRe = /^\"(.*)\"$/; +/**#@-*/ + +/** + * unquote maybe git-quoted filename + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' + * + * @param {String} str: git-quoted string + * @returns {String} Unquoted and unescaped string + * + * @globals escCodeRe, octEscRe, maybeQuotedRe + */ +function unquote(str) { + function unq(seq) { + var es = { + // character escape codes, aka escape sequences (from C) + // replacements are to some extent JavaScript specific + t: "\t", // tab (HT, TAB) + n: "\n", // newline (NL) + r: "\r", // return (CR) + f: "\f", // form feed (FF) + b: "\b", // backspace (BS) + a: "\x07", // alarm (bell) (BEL) + e: "\x1B", // escape (ESC) + v: "\v" // vertical tab (VT) + }; + + if (seq.search(octEscRe) !== -1) { + // octal char sequence + return String.fromCharCode(parseInt(seq, 8)); + } else if (seq in es) { + // C escape sequence, aka character escape code + return es[seq]; + } + // quoted ordinary character + return seq; + } + + var match = str.match(maybeQuotedRe); + if (match) { + str = match[1]; + // perhaps str = eval('"'+str+'"'); would be enough? + str = str.replace(escCodeRe, + function (substr, p1, offset, s) { return unq(p1); }); + } + return str; +} + +/* ============================================================ */ +/* main part: parsing response */ + +/** + * Function called for each blame entry, as soon as it finishes. + * It updates page via DOM manipulation, adding sha1 info, etc. + * + * @param {Commit} commit: blamed commit + * @param {Object} group: object representing group of lines, + * which blame the same commit (blame entry) + * + * @globals blamedLines + */ +function handleLine(commit, group) { + /* + This is the structure of the HTML fragment we are working + with: + + +
+ 123 + # times (my ext3 doesn't). + + */ + + var resline = group.resline; + + // format date and time string only once per commit + if (!commit.info) { + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ + commit.info = commit.author + ', ' + + formatDateISOLocal(commit.authorTime, commit.authorTimezone); + } + + // loop over lines in commit group + for (var i = 0; i < group.numlines; i++, resline++) { + var tr = document.getElementById('l'+resline); + if (!tr) { + break; + } + /* + + + 123 + # times (my ext3 doesn't). + + */ + var td_sha1 = tr.firstChild; + var a_sha1 = td_sha1.firstChild; + var a_linenr = td_sha1.nextSibling.firstChild; + + /* */ + var tr_class = 'light'; // or tr.className + if (commit.boundary) { + tr_class += ' boundary'; + } + if (commit.nprevious === 0) { + tr_class += ' no-previous'; + } else if (commit.nprevious > 1) { + tr_class += ' multiple-previous'; + } + tr.className = tr_class; + + /* ? */ + if (i === 0) { + td_sha1.title = commit.info; + td_sha1.rowSpan = group.numlines; + + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; + a_sha1.firstChild.data = commit.sha1.substr(0, 8); + if (group.numlines >= 2) { + var fragment = document.createDocumentFragment(); + var br = document.createElement("br"); + var text = document.createTextNode( + commit.author.match(/\b([A-Z])\B/g).join('')); + if (br && text) { + var elem = fragment || td_sha1; + elem.appendChild(br); + elem.appendChild(text); + if (fragment) { + td_sha1.appendChild(fragment); + } + } + } + } else { + //tr.removeChild(td_sha1); // DOM2 Core way + tr.deleteCell(0); // DOM2 HTML way + } + + /* 123 */ + var linenr_commit = + ('previous' in commit ? commit.previous : commit.sha1); + var linenr_filename = + ('file_parent' in commit ? commit.file_parent : commit.filename); + a_linenr.href = projectUrl + 'a=blame_incremental' + + ';hb=' + linenr_commit + + ';f=' + encodeURIComponent(linenr_filename) + + '#l' + (group.srcline + i); + + blamedLines++; + + //updateProgressInfo(); + } +} + +// ---------------------------------------------------------------------- + +var inProgress = false; // are we processing response + +/**#@+ + * @constant + */ +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; +var infoRe = /^([a-z-]+) ?(.*)/; +var endRe = /^END ?([^ ]*) ?(.*)/; +/**@-*/ + +var curCommit = new Commit(); +var curGroup = {}; + +var pollTimer = null; + +/** + * Parse output from 'git blame --incremental [...]', received via + * XMLHttpRequest from server (blamedataUrl), and call handleLine + * (which updates page) as soon as blame entry is completed. + * + * @param {String[]} lines: new complete lines from blamedata server + * + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server + * @globals sha1Re, infoRe, endRe + */ +function processBlameLines(lines) { + var match; + + for (var i = 0, len = lines.length; i < len; i++) { + + if ((match = sha1Re.exec(lines[i]))) { + var sha1 = match[1]; + var srcline = parseInt(match[2], 10); + var resline = parseInt(match[3], 10); + var numlines = parseInt(match[4], 10); + + var c = commits[sha1]; + if (!c) { + c = new Commit(sha1); + commits[sha1] = c; + } + curCommit = c; + + curGroup.srcline = srcline; + curGroup.resline = resline; + curGroup.numlines = numlines; + + } else if ((match = infoRe.exec(lines[i]))) { + var info = match[1]; + var data = match[2]; + switch (info) { + case 'filename': + curCommit.filename = unquote(data); + // 'filename' information terminates the entry + handleLine(curCommit, curGroup); + updateProgressInfo(); + break; + case 'author': + curCommit.author = data; + break; + case 'author-time': + curCommit.authorTime = parseInt(data, 10); + break; + case 'author-tz': + curCommit.authorTimezone = data; + break; + case 'previous': + curCommit.nprevious++; + // store only first 'previous' header + if (!'previous' in curCommit) { + var parts = data.split(' ', 2); + curCommit.previous = parts[0]; + curCommit.file_parent = unquote(parts[1]); + } + break; + case 'boundary': + curCommit.boundary = true; + break; + } // end switch + + } else if ((match = endRe.exec(lines[i]))) { + t_interval_server = match[1]; + cmds_server = match[2]; + + } else if (lines[i] !== '') { + // malformed line + + } // end if (match) + + } // end for (lines) +} + +/** + * Process new data and return pointer to end of processed part + * + * @param {String} unprocessed: new data (from nextReadPos) + * @param {Number} nextReadPos: end of last processed data + * @return {Number} end of processed data (new value for nextReadPos) + */ +function processData(unprocessed, nextReadPos) { + var lastLineEnd = unprocessed.lastIndexOf('\n'); + if (lastLineEnd !== -1) { + var lines = unprocessed.substring(0, lastLineEnd).split('\n'); + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; + + processBlameLines(lines); + } // end if + + return nextReadPos; +} + +/** + * Handle XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * + * @globals pollTimer, commits, inProgress + */ +function handleError(xhr) { + errorInfo('Server error: ' + + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); + + clearInterval(pollTimer); + commits = {}; // free memory + + inProgress = false; +} + +/** + * Called after XMLHttpRequest finishes (loads) + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) + * + * @globals pollTimer, commits, inProgress + */ +function responseLoaded(xhr) { + clearInterval(pollTimer); + + fixColorsAndGroups(); + writeTimeInterval(); + commits = {}; // free memory + + inProgress = false; +} + +/** + * handler for XMLHttpRequest onreadystatechange event + * @see startBlame + * + * @globals xhr, inProgress + */ +function handleResponse() { + + /* + * xhr.readyState + * + * Value Constant (W3C) Description + * ------------------------------------------------------------------- + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED send() has been called, and headers + * and status are available. + * 3 LOADING Downloading; responseText holds partial data. + * 4 DONE The operation is complete. + */ + + if (xhr.readyState !== 4 && xhr.readyState !== 3) { + return; + } + + // the server returned error + if (xhr.readyState === 3 && xhr.status !== 200) { + return; + } + if (xhr.readyState === 4 && xhr.status !== 200) { + handleError(xhr); + return; + } + + // In konqueror xhr.responseText is sometimes null here... + if (xhr.responseText === null) { + return; + } + + // in case we were called before finished processing + if (inProgress) { + return; + } else { + inProgress = true; + } + + // extract new whole (complete) lines, and process them + while (xhr.prevDataLength !== xhr.responseText.length) { + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + break; + } + + xhr.prevDataLength = xhr.responseText.length; + var unprocessed = xhr.responseText.substring(xhr.nextReadPos); + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); + } // end while + + // did we finish work? + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + responseLoaded(xhr); + } + + inProgress = false; +} + +// ============================================================ +// ------------------------------------------------------------ + +/** + * Incrementally update line data in blame_incremental view in gitweb. + * + * @param {String} blamedataUrl: URL to server script generating blame data. + * @param {String} bUrl: partial URL to project, used to generate links. + * + * Called from 'blame_incremental' view after loading table with + * file contents, a base for blame view. + * + * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer +*/ +function startBlame(blamedataUrl, bUrl) { + + xhr = createRequestObject(); + if (!xhr) { + errorInfo('ERROR: XMLHttpRequest not supported'); + return; + } + + t0 = new Date(); + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); + if ((div_progress_bar = document.getElementById('progress_bar'))) { + //div_progress_bar.setAttribute('style', 'width: 100%;'); + div_progress_bar.style.cssText = 'width: 100%;'; + } + totalLines = countLines(); + updateProgressInfo(); + + /* add extra properties to xhr object to help processing response */ + xhr.prevDataLength = -1; // used to detect if we have new data + xhr.nextReadPos = 0; // where unread part of response starts + + xhr.onreadystatechange = handleResponse; + //xhr.onreadystatechange = function () { handleResponse(xhr); }; + + xhr.open('GET', blamedataUrl); + xhr.setRequestHeader('Accept', 'text/plain'); + xhr.send(null); + + // not all browsers call onreadystatechange event on each server flush + // poll response using timer every second to handle this issue + pollTimer = setInterval(xhr.onreadystatechange, 1000); +} + +// end of gitweb.js diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 18cbf35391..6cdd8c39b4 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -96,6 +96,8 @@ 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/"; @@ -564,6 +566,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, @@ -4787,7 +4791,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"); @@ -4809,10 +4815,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(); @@ -4823,109 +4862,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 + 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} = { '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 ($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!; + } + + } 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 }; } - if ($data =~ /^previous /) { - $meta->{'nprevious'}++; + 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"; + if ($group_size) { + print "\n"; } - 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"; + # '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
!. + qq!$linenr! . esc_html($line) . "
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('.', ...) + 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 "
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) . "
"; + 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(); -- cgit v1.3 From c4ccf61f4caf8c293713586531b4a2ea709756c8 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 1 Sep 2009 13:39:19 +0200 Subject: gitweb: Create links leading to 'blame_incremental' using JavaScript The new 'blame_incremental' view requires JavaScript to run. Not all web browsers implement JavaScript (e.g. text browsers such as Lynx), and not all users have JavaScript enabled. Therefore instead of unconditionally linking to 'blame_incremental' view, we use JavaScript to convert those links to lead to view utilizing JavaScript, by adding 'js=1' to link. Currently the only action that takes 'js=1' into account is 'blame', which then acts as if it was called as 'blame_incremental' action. Possible enhancement would be to do JavaScript redirect by setting window.location instead of modifying $format and $action in git_blame_common() subroutine. The only JavaScript-aware/using view is currently 'blame_incremental'. While at it move reading JavaScript to git_footer_html() subroutine. Note that in this view we do not add 'js=1' currently (even though perhaps we should; note that for consistency we should also add 'js=1' in links added by JavaScript part of 'blame_incremental'). This idea was originally implemented by Petr Baudis in http://article.gmane.org/gmane.comp.version-control.git/47614 but it added \n!; + if ($action eq 'blame_incremental') { + print qq!\n!; + } else { + print qq!\n!; + } + print "\n" . ""; } @@ -4793,6 +4807,10 @@ sub git_tag { sub git_blame_common { my $format = shift || 'porcelain'; + if ($format eq 'porcelain' && $cgi->param('js')) { + $format = 'incremental'; + $action = 'blame_incremental'; # for page title etc + } # permissions gitweb_check_feature('blame') @@ -4872,7 +4890,7 @@ sub git_blame_common { if ($format eq 'incremental') { print "\n"; @@ -5003,14 +5021,6 @@ sub git_blame_common { close $fd or print "Reading blob failed\n"; - if ($format eq 'incremental') { - print qq!\n!. - qq!\n!; - } - git_footer_html(); } -- cgit v1.3 From e4b48eaab724d9fd5941c6a683a34ee38a864897 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Mon, 7 Sep 2009 14:40:00 +0200 Subject: gitweb: Add 'show-sizes' feature to show blob sizes in tree view Add support for 'show-sizes' feature to show (in separate column, between mode and filename) the size of blobs (files) in the 'tree' view. It passes '-l' option to "git ls-tree" invocation. For the 'tree' and 'commit' (submodule) entries, '-' is shown in place of size; for generated '..' "up directory" entry nothing is shown. The 'show-sizes' feature is enabled by default. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.css | 6 +++++ gitweb/gitweb.perl | 69 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 15 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 8f68fe3091..d60bfc1f64 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -341,6 +341,12 @@ td.mode { font-family: monospace; } +/* format of (optional) objects size in 'tree' view */ +td.size { + font-family: monospace; + text-align: right; +} + /* styling of diffs (patchsets): commitdiff and blobdiff views */ div.diff.header, div.diff.extended_header { diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 24b219310a..7b1c60e2c1 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -297,6 +297,19 @@ our %feature = ( 'override' => 0, 'default' => [1]}, + # Enable showing size of blobs in a 'tree' view, in a separate + # column, similar to what 'ls -l' does. This cost a bit of IO. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'show-sizes'}{'default'} = [0]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'show-sizes'}{'override'} = 1; + # and in project config gitweb.showsizes = 0|1; + 'show-sizes' => { + 'sub' => sub { feature_bool('showsizes', @_) }, + 'override' => 0, + 'default' => [1]}, + # Make gitweb use an alternative format of the URLs which can be # more readable and natural-looking: project name is embedded # directly in the path and the query string contains other @@ -2764,16 +2777,31 @@ sub parse_ls_tree_line { my %opts = @_; my %res; - #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' - $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s; + if ($opts{'-l'}) { + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s; - $res{'mode'} = $1; - $res{'type'} = $2; - $res{'hash'} = $3; - if ($opts{'-z'}) { - $res{'name'} = $4; + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + $res{'size'} = $4; + if ($opts{'-z'}) { + $res{'name'} = $5; + } else { + $res{'name'} = unquote($5); + } } else { - $res{'name'} = unquote($4); + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + if ($opts{'-z'}) { + $res{'name'} = $4; + } else { + $res{'name'} = unquote($4); + } } return wantarray ? %res : \%res; @@ -3564,6 +3592,9 @@ sub git_print_tree_entry { # and link is the action links of the entry. print "" . mode_str($t->{'mode'}) . "\n"; + if (exists $t->{'size'}) { + print "$t->{'size'}\n"; + } if ($t->{'type'} eq "blob") { print "" . $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, @@ -3609,12 +3640,14 @@ sub git_print_tree_entry { } elsif ($t->{'type'} eq "tree") { print ""; print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, - file_name=>"$basedir$t->{'name'}", %base_key)}, + file_name=>"$basedir$t->{'name'}", + %base_key)}, esc_path($t->{'name'})); print "\n"; print ""; print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, - file_name=>"$basedir$t->{'name'}", %base_key)}, + file_name=>"$basedir$t->{'name'}", + %base_key)}, "tree"); if (defined $hash_base) { print " | " . @@ -5088,10 +5121,14 @@ sub git_tree { } die_error(404, "No such tree") unless defined($hash); + my $show_sizes = gitweb_check_feature('show-sizes'); + my $have_blame = gitweb_check_feature('blame'); + my @entries = (); { local $/ = "\0"; - open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash + open my $fd, "-|", git_cmd(), "ls-tree", '-z', + ($show_sizes ? '-l' : ()), @extra_options, $hash or die_error(500, "Open git-ls-tree failed"); @entries = map { chomp; $_ } <$fd>; close $fd @@ -5102,7 +5139,6 @@ sub git_tree { my $ref = format_ref_marker($refs, $hash_base); git_header_html(); my $basedir = ''; - my $have_blame = gitweb_check_feature('blame'); if (defined $hash_base && (my %co = parse_commit($hash_base))) { my @views_nav = (); if (defined $file_name) { @@ -5118,7 +5154,8 @@ sub git_tree { # FIXME: Should be available when we have no hash base as well. push @views_nav, $snapshot_links; } - git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav)); + git_print_page_nav('tree','', $hash_base, undef, undef, + join(' | ', @views_nav)); git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); } else { undef $hash_base; @@ -5151,8 +5188,10 @@ sub git_tree { undef $up unless $up; # based on git_print_tree_entry print '' . mode_str('040000') . "\n"; + print ' '."\n" if $show_sizes; print ''; - print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base, + print $cgi->a({-href => href(action=>"tree", + hash_base=>$hash_base, file_name=>$up)}, ".."); print "\n"; @@ -5161,7 +5200,7 @@ sub git_tree { print "\n"; } foreach my $line (@entries) { - my %t = parse_ls_tree_line($line, -z => 1); + my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes); if ($alternate) { print "\n"; -- cgit v1.3 From fdb0c36e903d13c184f9a465035c75565c5c072a Mon Sep 17 00:00:00 2001 From: Mark Rada Date: Sat, 26 Sep 2009 13:46:08 -0400 Subject: gitweb: check given hash before trying to create snapshot Makes things nicer in cases when you hand craft the snapshot URL but make a typo in defining the hash variable (e.g. netx instead of next); you will now get an error message instead of a broken tarball. Tests for t9501 are included to demonstrate added functionality. Signed-off-by: Mark Rada Signed-off-by: Shawn O. Pearce --- gitweb/gitweb.perl | 7 ++++-- t/t9501-gitweb-standalone-http-status.sh | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 24b219310a..8d4a2ae600 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5196,8 +5196,11 @@ sub git_snapshot { die_error(403, "Unsupported snapshot format"); } - if (!defined $hash) { - $hash = git_get_head_hash($project); + my $type = git_get_type("$hash^{}"); + if (!$type) { + die_error(404, 'Object does not exist'); + } elsif ($type eq 'blob') { + die_error(400, 'Object is not a tree-ish'); } my $name = $project; diff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh index d0ff21d426..0688a57e1d 100644 --- a/t/t9501-gitweb-standalone-http-status.sh +++ b/t/t9501-gitweb-standalone-http-status.sh @@ -75,4 +75,43 @@ test_expect_success \ test_debug 'cat gitweb.output' +# ---------------------------------------------------------------------- +# snapshot hash ids + +test_expect_success 'snapshots: good tree-ish id' ' + gitweb_run "p=.git;a=snapshot;h=master;sf=tgz" && + grep "Status: 200 OK" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad tree-ish id' ' + gitweb_run "p=.git;a=snapshot;h=frizzumFrazzum;sf=tgz" && + grep "404 - Object does not exist" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad tree-ish id (tagged object)' ' + echo object > tag-object && + git add tag-object && + git commit -m "Object to be tagged" && + git tag tagged-object `git hash-object tag-object` && + gitweb_run "p=.git;a=snapshot;h=tagged-object;sf=tgz" && + grep "400 - Object is not a tree-ish" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: good object id' ' + ID=`git rev-parse --verify HEAD` && + gitweb_run "p=.git;a=snapshot;h=$ID;sf=tgz" && + grep "Status: 200 OK" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad object id' ' + gitweb_run "p=.git;a=snapshot;h=abcdef01234;sf=tgz" && + grep "404 - Object does not exist" gitweb.output +' +test_debug 'cat gitweb.output' + + test_done -- cgit v1.3 From 1655c98790682aed8892eb8a9eb6d44e00d5f69f Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Fri, 9 Oct 2009 14:26:44 +0200 Subject: gitweb: Do not show 'patch' link for merge commits The 'patch' view is about generating text/plain patch that can be given to "git am", and "git am" doesn't understand merges anyway. Therefore link to 'patch' view should not be shown for merge commits. Also call to git-format-patch inside the 'patch' action would fail when 'patch' action is called for a merge commit, with "Reading git-format-patch failed" text as 'patch' view body. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 24b219310a..c939e2434d 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5328,7 +5328,7 @@ sub git_commit { } @$parents ) . ')'; } - if (gitweb_check_feature('patches')) { + if (gitweb_check_feature('patches') && @$parents <= 1) { $formats_nav .= " | " . $cgi->a({-href => href(action=>"patch", -replay=>1)}, "patch"); @@ -5616,7 +5616,7 @@ sub git_commitdiff { $formats_nav = $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, "raw"); - if ($patch_max) { + if ($patch_max && @{$co{'parents'}} <= 1) { $formats_nav .= " | " . $cgi->a({-href => href(action=>"patch", -replay=>1)}, "patch"); @@ -5824,7 +5824,7 @@ sub git_commitdiff_plain { # format-patch-style patches sub git_patch { - git_commitdiff(-format => 'patch', -single=> 1); + git_commitdiff(-format => 'patch', -single => 1); } sub git_patches { -- cgit v1.3 From 452e2256d2d7cb5494ca10fcbbb6bdf29570f2c0 Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Tue, 13 Oct 2009 21:51:36 +0200 Subject: gitweb: fix esc_param The custom CGI escaping done in esc_param failed to escape UTF-8 properly. Fix by using CGI::escape on each sequence of matched characters instead of sprintf()ing a custom escaping for each byte. Additionally, the space -> + escape was being escaped due to greedy matching on the first substitution. Fix by adding space to the list of characters not handled on the first substitution. Finally, remove an unnecessary escaping of the + sign. Signed-off-by: Giuseppe Bilotta Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 24b219310a..4b21ad25df 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -1083,8 +1083,7 @@ sub to_utf8 { # correct, but quoted slashes look too horrible in bookmarks sub esc_param { my $str = shift; - $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg; - $str =~ s/\+/%2B/g; + $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg; $str =~ s/ /\+/g; return $str; } -- cgit v1.3 From e133d65c3e3d7f5f869edb2d6205af68af0fe38f Mon Sep 17 00:00:00 2001 From: Stephen Boyd Date: Thu, 15 Oct 2009 21:14:59 -0700 Subject: gitweb: linkify author/committer names with search It's nice to search for an author by merely clicking on their name in gitweb. This is usually faster than selecting the name, copying the selection, pasting it into the search box, selecting between author/committer and then hitting enter. Linkify the avatar icon in log/shortlog view because the icon is directly adjacent to the name and thus more related. The same is not true when in commit/tag view where the icon is farther away. Signed-off-by: Stephen Boyd Signed-off-by: Junio C Hamano --- gitweb/gitweb.css | 4 ++++ gitweb/gitweb.perl | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 8f68fe3091..27405e647d 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -32,6 +32,10 @@ img.avatar { vertical-align: middle; } +a.list img.avatar { + border-style: none; +} + div.page_header { height: 25px; padding: 8px; diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 4b21ad25df..c160a56c6c 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -1594,6 +1594,29 @@ sub git_get_avatar { } } +sub format_search_author { + my ($author, $searchtype, $displaytext) = @_; + my $have_search = gitweb_check_feature('search'); + + if ($have_search) { + my $performed = ""; + if ($searchtype eq 'author') { + $performed = "authored"; + } elsif ($searchtype eq 'committer') { + $performed = "committed"; + } + + return $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$author, + searchtype=>$searchtype), class=>"list", + title=>"Search for commits $performed by $author"}, + $displaytext); + + } else { + return $displaytext; + } +} + # format the author name of the given commit with the given tag # the author name is chopped and escaped according to the other # optional parameters (see chop_str). @@ -1602,8 +1625,10 @@ sub format_author_html { my $co = shift; my $author = chop_and_escape_str($co->{'author_name'}, @_); return "<$tag class=\"author\">" . - git_get_avatar($co->{'author_email'}, -pad_after => 1) . - $author . ""; + format_search_author($co->{'author_name'}, "author", + git_get_avatar($co->{'author_email'}, -pad_after => 1) . + $author) . + ""; } # format git diff header line, i.e. "diff --(git|combined|cc) ..." @@ -3372,10 +3397,11 @@ sub git_print_authorship { my $co = shift; my %opts = @_; my $tag = $opts{-tag} || 'div'; + my $author = $co->{'author_name'}; my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'}); print "<$tag class=\"author_date\">" . - esc_html($co->{'author_name'}) . + format_search_author($author, "author", esc_html($author)) . " [$ad{'rfc2822'}"; print_local_time(%ad) if ($opts{-localtime}); print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1) @@ -3394,8 +3420,12 @@ sub git_print_authorship_rows { @people = ('author', 'committer') unless @people; foreach my $who (@people) { my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"}); - print "$who" . esc_html($co->{$who}) . "" . - "" . + print "$who" . + format_search_author($co->{"${who}_name"}, $who, + esc_html($co->{"${who}_name"})) . " " . + format_search_author($co->{"${who}_email"}, $who, + esc_html("<" . $co->{"${who}_email"} . ">")) . + "" . git_get_avatar($co->{"${who}_email"}, -size => 'double') . "\n" . "" . -- cgit v1.3 From b9759f0762c17a5b7bc36af98613c67249931330 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Fri, 6 Nov 2009 16:08:41 +0100 Subject: gitweb: Fix blob linenr links in pathinfo mode In pathinfo mode, we use that refers to the base location of gitweb in order for various external media links to work well. However, this means that for the page to refer to itself, it must regenerate full link, and this is exactly what the blob view page did not do for line numbers. Signed-off-by: Petr Baudis Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 24b219310a..184b683aa5 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5065,7 +5065,8 @@ sub git_blob { chomp $line; $nr++; $line = untabify($line); - printf "
%4i %s
\n", + printf "\n", $nr, $nr, $nr, esc_html($line, -nbsp=>1); } } -- cgit v1.3 From b629275fd02aa07c2630d1a8c8a14011ff164043 Mon Sep 17 00:00:00 2001 From: Mark Rada Date: Sat, 7 Nov 2009 16:13:29 +0100 Subject: gitweb: Smarter snapshot names Teach gitweb how to produce nicer snapshot names by only using the short hash id. If clients make requests using a tree-ish that is not a partial or full SHA-1 hash, then the short hash will also be appended to whatever they asked for. If clients request snapshot of a tag (which means that $hash ('h') parameter has 'refs/tags/' prefix), use only tag name. Update tests cases in t9502-gitweb-standalone-parse-output. Gitweb uses the following format for snapshot filenames: -. where is project name with '.git' or '/.git' suffix stripped, unless '.git' is the whole project name. For snapshot prefix it uses: -/ as compared to / before (without version info). Current rules for : * if 'h' / $hash parameter is SHA-1 or shortened SHA-1, use SHA-1 shortened to to 7 characters * otherwise if 'h' / $hash parameter is tag name (it begins with 'refs/tags/' prefix, use tag name (with 'refs/tags/' stripped * otherwise if 'h' / $hash parameter starts with 'refs/heads/' prefix, strip this prefix, convert '/' into '.', and append shortened SHA-1 after '-', i.e. use - Signed-off-by: Mark Rada Signed-off-by: Shawn O. Pearce Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 76 ++++++++++++++++++++++++------- t/t9502-gitweb-standalone-parse-output.sh | 38 ++++++++++++++-- 2 files changed, 93 insertions(+), 21 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 8d4a2ae600..d8dfd950a4 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -1983,16 +1983,27 @@ sub quote_command { # get HEAD ref of given project as hash sub git_get_head_hash { - my $project = shift; + return git_get_full_hash(shift, 'HEAD'); +} + +sub git_get_full_hash { + return git_get_hash(@_); +} + +sub git_get_short_hash { + return git_get_hash(@_, '--short=7'); +} + +sub git_get_hash { + my ($project, $hash, @options) = @_; my $o_git_dir = $git_dir; my $retval = undef; $git_dir = "$projectroot/$project"; - if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") { - my $head = <$fd>; + if (open my $fd, '-|', git_cmd(), 'rev-parse', + '--verify', '-q', @options, $hash) { + $retval = <$fd>; + chomp $retval if defined $retval; close $fd; - if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) { - $retval = $1; - } } if (defined $o_git_dir) { $git_dir = $o_git_dir; @@ -5179,6 +5190,43 @@ sub git_tree { git_footer_html(); } +sub snapshot_name { + my ($project, $hash) = @_; + + # path/to/project.git -> project + # path/to/project/.git -> project + my $name = to_utf8($project); + $name =~ s,([^/])/*\.git$,$1,; + $name = basename($name); + # sanitize name + $name =~ s/[[:cntrl:]]/?/g; + + my $ver = $hash; + if ($hash =~ /^[0-9a-fA-F]+$/) { + # shorten SHA-1 hash + my $full_hash = git_get_full_hash($project, $hash); + if ($full_hash =~ /^$hash/ && length($hash) > 7) { + $ver = git_get_short_hash($project, $hash); + } + } elsif ($hash =~ m!^refs/tags/(.*)$!) { + # tags don't need shortened SHA-1 hash + $ver = $1; + } else { + # branches and other need shortened SHA-1 hash + if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) { + $ver = $1; + } + $ver .= '-' . git_get_short_hash($project, $hash); + } + # in case of hierarchical branch names + $ver =~ s!/!.!g; + + # name = project-version_string + $name = "$name-$ver"; + + return wantarray ? ($name, $name) : $name; +} + sub git_snapshot { my $format = $input_params{'snapshot_format'}; if (!@snapshot_fmts) { @@ -5203,24 +5251,20 @@ sub git_snapshot { die_error(400, 'Object is not a tree-ish'); } - my $name = $project; - $name =~ s,([^/])/*\.git$,$1,; - $name = basename($name); - my $filename = to_utf8($name); - $name =~ s/\047/\047\\\047\047/g; - my $cmd; - $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}"; - $cmd = quote_command( + my ($name, $prefix) = snapshot_name($project, $hash); + my $filename = "$name$known_snapshot_formats{$format}{'suffix'}"; + my $cmd = quote_command( git_cmd(), 'archive', "--format=$known_snapshot_formats{$format}{'format'}", - "--prefix=$name/", $hash); + "--prefix=$prefix/", $hash); if (exists $known_snapshot_formats{$format}{'compressor'}) { $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}); } + $filename =~ s/(["\\])/\\$1/g; print $cgi->header( -type => $known_snapshot_formats{$format}{'type'}, - -content_disposition => 'inline; filename="' . "$filename" . '"', + -content_disposition => 'inline; filename="' . $filename . '"', -status => '200 OK'); open my $fd, "-|", $cmd diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh index 741187b9e4..dd83890001 100755 --- a/t/t9502-gitweb-standalone-parse-output.sh +++ b/t/t9502-gitweb-standalone-parse-output.sh @@ -56,29 +56,57 @@ test_debug ' test_expect_success 'snapshot: full sha1' ' gitweb_run "p=.git;a=snapshot;h=$FULL_ID;sf=tar" && - check_snapshot ".git-$FULL_ID" ".git" + check_snapshot ".git-$SHORT_ID" ' test_debug 'cat gitweb.headers && cat file_list' test_expect_success 'snapshot: shortened sha1' ' gitweb_run "p=.git;a=snapshot;h=$SHORT_ID;sf=tar" && - check_snapshot ".git-$SHORT_ID" ".git" + check_snapshot ".git-$SHORT_ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: almost full sha1' ' + ID=$(git rev-parse --short=30 HEAD) && + gitweb_run "p=.git;a=snapshot;h=$ID;sf=tar" && + check_snapshot ".git-$SHORT_ID" ' test_debug 'cat gitweb.headers && cat file_list' test_expect_success 'snapshot: HEAD' ' gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tar" && - check_snapshot ".git-HEAD" ".git" + check_snapshot ".git-HEAD-$SHORT_ID" ' test_debug 'cat gitweb.headers && cat file_list' test_expect_success 'snapshot: short branch name (master)' ' gitweb_run "p=.git;a=snapshot;h=master;sf=tar" && - check_snapshot ".git-master" ".git" + ID=$(git rev-parse --verify --short=7 master) && + check_snapshot ".git-master-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: short tag name (first)' ' + gitweb_run "p=.git;a=snapshot;h=first;sf=tar" && + ID=$(git rev-parse --verify --short=7 first) && + check_snapshot ".git-first-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: full branch name (refs/heads/master)' ' + gitweb_run "p=.git;a=snapshot;h=refs/heads/master;sf=tar" && + ID=$(git rev-parse --verify --short=7 master) && + check_snapshot ".git-master-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: full tag name (refs/tags/first)' ' + gitweb_run "p=.git;a=snapshot;h=refs/tags/first;sf=tar" && + check_snapshot ".git-first" ' test_debug 'cat gitweb.headers && cat file_list' -test_expect_failure 'snapshot: hierarchical branch name (xx/test)' ' +test_expect_success 'snapshot: hierarchical branch name (xx/test)' ' gitweb_run "p=.git;a=snapshot;h=xx/test;sf=tar" && ! grep "filename=.*/" gitweb.headers ' -- cgit v1.3 From 42671caa7d619da9d490e77c438845c1e638c54c Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Fri, 13 Nov 2009 02:02:12 +0100 Subject: gitweb: Refactor 'log' action generation, adding git_log_body() Put the main part of 'log' view generation into git_log_body, similarly how it is done for 'shortlog' and 'history' views (and also for 'tags' and 'heads' views). This is preparation for extracting common code between 'log', 'shortlog' and 'history' actions. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 81 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 33 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 62325ea877..2e92fde294 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4361,6 +4361,46 @@ sub git_project_list_body { print "\n"; } +sub git_log_body { + # uses global variable $project + my ($commitlist, $from, $to, $refs, $extra) = @_; + + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + for (my $i = 0; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + next if !%co; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); + my %ad = parse_date($co{'author_epoch'}); + git_print_header_div('commit', + "$co{'age_string'}" . + esc_html($co{'title'}) . $ref, + $commit); + print "
\n" . + "
\n" . + $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . + "
\n" . + "
\n"; + git_print_authorship(\%co, -tag => 'span'); + print "
\n
\n"; + + print "
\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1); + print "
\n"; + } + if ($extra) { + print "
\n"; + print "$extra\n"; + print "
\n"; + } +} + sub git_shortlog_body { # uses global variable $project my ($commitlist, $from, $to, $refs, $extra) = @_; @@ -5310,7 +5350,12 @@ sub git_log { my @commitlist = parse_commits($hash, 101, (100 * $page)); my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100); - + my $next_link; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(-replay=>1, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } my ($patch_max) = gitweb_get_feature('patches'); if ($patch_max) { if ($patch_max < 0 || @commitlist <= $patch_max) { @@ -5329,39 +5374,9 @@ sub git_log { git_print_header_div('summary', $project); print "
Last change $co{'age_string'}.

\n"; } - my $to = ($#commitlist >= 99) ? (99) : ($#commitlist); - for (my $i = 0; $i <= $to; $i++) { - my %co = %{$commitlist[$i]}; - next if !%co; - my $commit = $co{'id'}; - my $ref = format_ref_marker($refs, $commit); - my %ad = parse_date($co{'author_epoch'}); - git_print_header_div('commit', - "$co{'age_string'}" . - esc_html($co{'title'}) . $ref, - $commit); - print "
\n" . - "
\n" . - $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . - " | " . - $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . - " | " . - $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . - "
\n" . - "
\n"; - git_print_authorship(\%co, -tag => 'span'); - print "
\n
\n"; - print "
\n"; - git_print_log($co{'comment'}, -final_empty_line=> 1); - print "
\n"; - } - if ($#commitlist >= 100) { - print "
\n"; - print $cgi->a({-href => href(-replay=>1, page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); - print "
\n"; - } + git_log_body(\@commitlist, 0, 99, $refs, $next_link); + git_footer_html(); } -- cgit v1.3 From 15f0b112d86001b0ae4be2555513c45b3cf65844 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Fri, 13 Nov 2009 02:02:13 +0100 Subject: gitweb: Refactor common parts of 'log' and 'shortlog' views Put the common parts of git_log and git_shortlog into git_log_generic subroutine: git_log and git_shortlog are now thin wrappers calling git_log_generic with appropriate arguments. The unification of code responsible for 'log' and 'shorlog' actions lead to the following changes in gitweb output * 'tree' link in page_nav now uses $hash parameter, as was the case for 'shortlog' but not for 'log' * 'log' view now respect $hash_parent limiting, like 'shortlog' did * 'log' view doesn't have special case for empty list anymore, and it always uses page_header linking to summary view, like 'shortlog' did. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 72 +++++++++++++++--------------------------------------- 1 file changed, 20 insertions(+), 52 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 2e92fde294..3ddd147257 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -5337,7 +5337,9 @@ sub git_snapshot { close $fd; } -sub git_log { +sub git_log_generic { + my ($fmt_name, $body_subr) = @_; + my $head = git_get_head_hash($project); if (!defined $hash) { $hash = $head; @@ -5347,16 +5349,21 @@ sub git_log { } 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('log', $hash, $head, $page, $#commitlist >= 100); - my $next_link; + my $paging_nav = format_paging_nav($fmt_name, $hash, $head, + $page, $#commitlist >= 100); + my $next_link = ''; if ($#commitlist >= 100) { $next_link = $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } - my ($patch_max) = gitweb_get_feature('patches'); + my $patch_max = gitweb_get_feature('patches'); if ($patch_max) { if ($patch_max < 0 || @commitlist <= $patch_max) { $paging_nav .= " ⋅ " . @@ -5366,20 +5373,18 @@ sub git_log { } git_header_html(); - git_print_page_nav('log','', $hash,undef,undef, $paging_nav); - - if (!@commitlist) { - my %co = parse_commit($hash); - - git_print_header_div('summary', $project); - print "
Last change $co{'age_string'}.

\n"; - } + git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav); + git_print_header_div('summary', $project); - git_log_body(\@commitlist, 0, 99, $refs, $next_link); + $body_subr->(\@commitlist, 0, 99, $refs, $next_link); git_footer_html(); } +sub git_log { + git_log_generic('log', \&git_log_body); +} + sub git_commit { $hash ||= $hash_base || "HEAD"; my %co = parse_commit($hash) @@ -6243,44 +6248,7 @@ EOT } sub git_shortlog { - my $head = git_get_head_hash($project); - if (!defined $hash) { - $hash = $head; - } - if (!defined $page) { - $page = 0; - } - my $refs = git_get_references(); - - 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, $#commitlist >= 100); - my $next_link = ''; - if ($#commitlist >= 100) { - $next_link = - $cgi->a({-href => href(-replay=>1, page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); - } - my $patch_max = gitweb_check_feature('patches'); - if ($patch_max) { - if ($patch_max < 0 || @commitlist <= $patch_max) { - $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"patches", -replay=>1)}, - "patches"); - } - } - - git_header_html(); - git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav); - git_print_header_div('summary', $project); - - git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link); - - git_footer_html(); + git_log_generic('shortlog', \&git_shortlog_body); } ## ...................................................................... -- cgit v1.3 From 69ca37d2abe2ae92e3456341aeab2ad6a7380bf0 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Fri, 13 Nov 2009 02:02:14 +0100 Subject: gitweb: Make 'history' view (re)use git_log_generic() Make git_history use git_log_generic, passing git_history_body as one of its paramaters. This required changes to git_log_generic, in particular passing more things as parameters. While refactoring common code of 'log', 'shortlog' and 'history' view, we did unify pagination, using always the form used by 'history' view, namely first * prev * next in place of HEAD * prev * next used by 'log' and 'shortlog' views. The 'history' view now supports commit limiting via 'hpb' parameter, similarly to 'shortlog' (and 'log') view. Performance of 'history' view got improved a bit, as it doesn't run git_get_hash_by_path for "current" version in a loop. Error detection and reporting for 'history' view changed a bit. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 146 +++++++++++++++++++++-------------------------------- 1 file changed, 57 insertions(+), 89 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 3ddd147257..681e635090 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3363,22 +3363,18 @@ sub git_print_page_nav { } sub format_paging_nav { - my ($action, $hash, $head, $page, $has_next_link) = @_; + my ($action, $page, $has_next_link) = @_; my $paging_nav; - if ($hash ne $head || $page) { - $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD"); - } else { - $paging_nav .= "HEAD"; - } - if ($page > 0) { - $paging_nav .= " ⋅ " . + $paging_nav .= + $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") . + " ⋅ " . $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { - $paging_nav .= " ⋅ prev"; + $paging_nav .= "first ⋅ prev"; } if ($has_next_link) { @@ -4447,7 +4443,8 @@ sub git_shortlog_body { sub git_history_body { # Warning: assumes constant type (blob or tree) during history - my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; + my ($commitlist, $from, $to, $refs, $extra, + $file_name, $file_hash, $ftype) = @_; $from = 0 unless defined $from; $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); @@ -4481,7 +4478,7 @@ sub git_history_body { $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff"); if ($ftype eq 'blob') { - my $blob_current = git_get_hash_by_path($hash_base, $file_name); + my $blob_current = $file_hash; my $blob_parent = git_get_hash_by_path($commit, $file_name); if (defined $blob_current && defined $blob_parent && $blob_current ne $blob_parent) { @@ -5338,25 +5335,48 @@ sub git_snapshot { } sub git_log_generic { - my ($fmt_name, $body_subr) = @_; + my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_; my $head = git_get_head_hash($project); - if (!defined $hash) { - $hash = $head; + if (!defined $base) { + $base = $head; } if (!defined $page) { $page = 0; } my $refs = git_get_references(); - my $commit_hash = $hash; - if (defined $hash_parent) { - $commit_hash = "$hash_parent..$hash"; + my $commit_hash = $base; + if (defined $parent) { + $commit_hash = "$parent..$base"; + } + my @commitlist = + parse_commits($commit_hash, 101, (100 * $page), + defined $file_name ? ($file_name, "--full-history") : ()); + + my $ftype; + if (!defined $file_hash && defined $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++) { + $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name); + last if defined $file_hash; + } + } + if (defined $file_hash) { + $ftype = git_get_type($file_hash); + } + if (defined $file_name && !defined $ftype) { + die_error(500, "Unknown type of object"); + } + my %co; + if (defined $file_name) { + %co = parse_commit($base) + or die_error(404, "Unknown commit object"); } - my @commitlist = parse_commits($commit_hash, 101, (100 * $page)); - my $paging_nav = format_paging_nav($fmt_name, $hash, $head, - $page, $#commitlist >= 100); + + my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100); my $next_link = ''; if ($#commitlist >= 100) { $next_link = @@ -5364,7 +5384,7 @@ sub git_log_generic { -accesskey => "n", -title => "Alt-n"}, "next"); } my $patch_max = gitweb_get_feature('patches'); - if ($patch_max) { + if ($patch_max && !defined $file_name) { if ($patch_max < 0 || @commitlist <= $patch_max) { $paging_nav .= " ⋅ " . $cgi->a({-href => href(action=>"patches", -replay=>1)}, @@ -5374,15 +5394,23 @@ sub git_log_generic { git_header_html(); git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav); - git_print_header_div('summary', $project); + if (defined $file_name) { + git_print_header_div('commit', esc_html($co{'title'}), $base); + } else { + git_print_header_div('summary', $project) + } + git_print_page_path($file_name, $ftype, $hash_base) + if (defined $file_name); - $body_subr->(\@commitlist, 0, 99, $refs, $next_link); + $body_subr->(\@commitlist, 0, 99, $refs, $next_link, + $file_name, $file_hash, $ftype); git_footer_html(); } sub git_log { - git_log_generic('log', \&git_log_body); + git_log_generic('log', \&git_log_body, + $hash, $hash_parent); } sub git_commit { @@ -5921,70 +5949,9 @@ sub git_patches { } sub git_history { - if (!defined $hash_base) { - $hash_base = git_get_head_hash($project); - } - if (!defined $page) { - $page = 0; - } - my $ftype; - 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) { - # 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); - } - if (!defined $ftype) { - die_error(500, "Unknown type of object"); - } - - my $paging_nav = ''; - if ($page > 0) { - $paging_nav .= - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name)}, - "first"); - $paging_nav .= " ⋅ " . - $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) { - $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"; - } - - git_header_html(); - git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); - git_print_page_path($file_name, $ftype, $hash_base); - - git_history_body(\@commitlist, 0, 99, - $refs, $hash_base, $ftype, $next_link); - - git_footer_html(); + git_log_generic('history', \&git_history_body, + $hash_base, $hash_parent_base, + $file_name, $hash); } sub git_search { @@ -6248,7 +6215,8 @@ EOT } sub git_shortlog { - git_log_generic('shortlog', \&git_shortlog_body); + git_log_generic('shortlog', \&git_shortlog_body, + $hash, $hash_parent); } ## ...................................................................... -- cgit v1.3 From e627e50a70677c057e984aea8bac4c27687e9614 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 26 Nov 2009 21:12:15 +0100 Subject: gitweb: Make linking to actions requiring JavaScript a feature Let gitweb turn some links (like 'blame' links) into linking to actions which require JavaScript (like 'blame_incremental' action) only if 'javascript-actions' feature is enabled. This means that links to such actions would be present only if both JavaScript is enabled and 'javascript-actions' feature is enabled. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 4a63646861..3368f2af7c 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -409,6 +409,13 @@ our %feature = ( 'timed' => { 'override' => 0, 'default' => [0]}, + + # Enable turning some links into links to actions which require + # JavaScript to run (like 'blame_incremental'). Not enabled by + # default. Project specific override is currently not supported. + 'javascript-actions' => { + 'override' => 0, + 'default' => [0]}, ); sub gitweb_get_feature { @@ -3250,7 +3257,7 @@ sub git_footer_html { qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!. qq! "!. href() .qq!");\n!. qq!\n!; - } else { + } elsif (gitweb_check_feature('javascript-actions')) { print qq!\n!; -- cgit v1.3 From 87e573f660dd0e871f3eb673d0b856488b6d8336 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Tue, 1 Dec 2009 17:54:26 +0100 Subject: gitweb: Add link to other blame implementation in blame views Add link to 'blame_incremental' action (which requires JavaScript) in 'blame' view, and add link to 'blame' action in 'blame_incremental' view. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/gitweb.perl | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 3368f2af7c..49402dc256 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -4883,6 +4883,17 @@ sub git_blame_common { my $formats_nav = $cgi->a({-href => href(action=>"blob", -replay=>1)}, "blob") . + " | "; + if ($format eq 'incremental') { + $formats_nav .= + $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)}, + "blame") . " (non-incremental)"; + } else { + $formats_nav .= + $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)}, + "blame") . " (incremental)"; + } + $formats_nav .= " | " . $cgi->a({-href => href(action=>"history", -replay=>1)}, "history") . -- cgit v1.3 From b62a1a98bc65f52d972a3f403c5b29d6a84642fd Mon Sep 17 00:00:00 2001 From: John 'Warthog9' Hawley Date: Sat, 30 Jan 2010 23:30:39 +0100 Subject: gitweb: Load checking This changes slightly the behavior of gitweb, so that it verifies that the box isn't inundated with before attempting to serve gitweb. If the box is overloaded, it basically returns a 503 Server Unavailable until the load falls below the defined threshold. This helps dramatically if you have a box that's I/O bound, reaches a certain load and you don't want gitweb, the I/O hog that it is, increasing the pain the server is already undergoing. This behavior is controlled by $maxload configuration variable. Default is a load of 300, which for most cases should never be hit. Unset it (set it to undefined value, i.e. undef) to turn off checking. Currently it requires that '/proc/loadavg' file exists, otherwise the load check is bypassed (load is taken to be 0). So platforms that do not implement '/proc/loadavg' currently cannot use this feature (provisions are included for additional checks to be added by others). There is simple test in t/t9501-gitweb-standalone-http-status.sh to check that it correctly returns "503 Service Unavailable" if load is too high, and also if there are any Perl warnings or errors. Signed-off-by: John 'Warthog9' Hawley Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/README | 7 ++++- gitweb/gitweb.perl | 48 ++++++++++++++++++++++++++++---- t/gitweb-lib.sh | 1 + t/t9501-gitweb-standalone-http-status.sh | 22 +++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) (limited to 'gitweb/gitweb.perl') diff --git a/gitweb/README b/gitweb/README index e34ee793ef..6c2c8e1259 100644 --- a/gitweb/README +++ b/gitweb/README @@ -174,7 +174,7 @@ not include variables usually directly set during build): Base URL for relative URLs in pages generated by gitweb, (e.g. $logo, $favicon, @stylesheets if they are relative URLs), needed and used only for URLs with nonempty PATH_INFO via - . Usually gitweb sets its value correctly, and there is no need to set this variable, e.g. to $my_uri or "/". * $home_link Target of the home link on top of all pages (the first part of view @@ -228,6 +228,11 @@ not include variables usually directly set during build): repositories from launching cross-site scripting (XSS) attacks. Set this to true if you don't trust the content of your repositories. The default is false. + * $maxload + Used to set the maximum load that we will still respond to gitweb queries. + If server load exceed this value then return "503 Service Unavaliable" error. + Server load is taken to be 0 if gitweb cannot determine its value. Set it to + undefined value to turn it off. The default is 300. Projects list file format diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 7e477af956..e2522cc64f 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -221,6 +221,12 @@ our %avatar_size = ( 'double' => 32 ); +# Used to set the maximum load that we will still respond to gitweb queries. +# If server load exceed this value then return "503 server busy" error. +# If gitweb cannot determined server load, it is taken to be 0. +# Leave it undefined (or set to 'undef') to turn off load checking. +our $maxload = 300; + # You define site-wide feature defaults here; override them with # $GITWEB_CONFIG as necessary. our %feature = ( @@ -551,12 +557,38 @@ if (-e $GITWEB_CONFIG) { do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM; } +# Get loadavg of system, to compare against $maxload. +# Currently it requires '/proc/loadavg' present to get loadavg; +# if it is not present it returns 0, which means no load checking. +sub get_loadavg { + if( -e '/proc/loadavg' ){ + open my $fd, '<', '/proc/loadavg' + or return 0; + my @load = split(/\s+/, scalar <$fd>); + close $fd; + + # The first three columns measure CPU and IO utilization of the last one, + # five, and 10 minute periods. The fourth column shows the number of + # currently running processes and the total number of processes in the m/n + # format. The last column displays the last process ID used. + return $load[0] || 0; + } + # additional checks for load average should go here for things that don't export + # /proc/loadavg + + return 0; +} + # version of the core git binary our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown"; $number_of_git_cmds++; $projects_list ||= $projectroot; +if (defined $maxload && get_loadavg() > $maxload) { + die_error(503, "The load average on the server is too high"); +} + # ====================================================================== # input validation and dispatch @@ -3328,7 +3360,8 @@ sub git_footer_html { } print qq!\n!; - if ($action eq 'blame_incremental') { + if (defined $action && + $action eq 'blame_incremental') { print qq!