3 # Gitweb::Format -- gitweb's format_* subs package
5 # This program is licensed under the GPLv2
7 package Gitweb
::Format
;
11 use Exporter
qw(import);
13 our @EXPORT = qw(format_log_line_html format_ref_marker format_subject_html
14 git_get_avatar format_search_author format_author_html
15 format_git_diff_header_line format_extended_diff_header_line
16 format_diff_from_to_header format_diff_cc_simplified
17 format_diff_line format_snapshot_links);
19 use Gitweb
::Config
qw($git_avatar gitweb_check_feature @snapshot_fmts
20 %known_snapshot_formats %avatar_size);
21 use Gitweb::Request qw($cgi $action $hash);
22 use Gitweb::Escape qw(to_utf8 esc_html esc_path untabify);
23 use Gitweb
::View
qw(href chop_and_escape_str file_type_long);
24 use Gitweb
::Util
qw(is_deleted);
26 ## ----------------------------------------------------------------------
27 ## functions returning short HTML fragments, or transforming HTML fragments
28 ## which don't belong to other sections
30 # format line of commit message.
31 sub format_log_line_html
{
34 $line = esc_html
($line, -nbsp
=>1);
35 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
36 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
37 -class => "text"}, $1);
43 # format marker of refs pointing to given object
45 # the destination action is chosen based on object type and current context:
46 # - for annotated tags, we choose the tag view unless it's the current view
47 # already, in which case we go to shortlog view
48 # - for other refs, we keep the current view if we're in history, shortlog or
49 # log view, and select shortlog otherwise
50 sub format_ref_marker
{
54 if (defined $refs->{$id}) {
55 foreach my $ref (@
{$refs->{$id}}) {
56 # this code exploits the fact that non-lightweight tags are the
57 # only indirect objects, and that they are the only objects for which
58 # we want to use tag instead of shortlog as action
59 my ($type, $name) = qw();
60 my $indirect = ($ref =~ s/\^\{\}$//);
61 # e.g. tags/v2.6.11 or heads/next
62 if ($ref =~ m!^(.*?)s?/(.*)$!) {
71 $class .= " indirect" if $indirect;
73 my $dest_action = "shortlog";
76 $dest_action = "tag" unless $action eq "tag";
77 } elsif ($action =~ /^(history|(short)?log)$/) {
78 $dest_action = $action;
82 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
91 $markers .= " <span class=\"$class\" title=\"$ref\">" .
97 return ' <span class="refs">'. $markers . '</span>';
103 # format, perhaps shortened and with markers, title line
104 sub format_subject_html
{
105 my ($long, $short, $href, $extra) = @_;
106 $extra = '' unless defined($extra);
108 if (length($short) < length($long)) {
109 $long =~ s/[[:cntrl:]]/?/g;
110 return $cgi->a({-href
=> $href, -class => "list subject",
111 -title
=> to_utf8
($long)},
112 esc_html
($short)) . $extra;
114 return $cgi->a({-href
=> $href, -class => "list subject"},
115 esc_html
($long)) . $extra;
119 # Rather than recomputing the url for an email multiple times, we cache it
120 # after the first hit. This gives a visible benefit in views where the avatar
121 # for the same email is used repeatedly (e.g. shortlog).
122 # The cache is shared by all avatar engines (currently gravatar only), which
123 # are free to use it as preferred. Since only one avatar engine is used for any
124 # given page, there's no risk for cache conflicts.
125 our %avatar_cache = ();
127 # Compute the picon url for a given email, by using the picon search service over at
128 # http://www.cs.indiana.edu/picons/search.html
130 my $email = lc shift;
131 if (!$avatar_cache{$email}) {
132 my ($user, $domain) = split('@', $email);
133 $avatar_cache{$email} =
134 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
136 "users+domains+unknown/up/single";
138 return $avatar_cache{$email};
141 # Compute the gravatar url for a given email, if it's not in the cache already.
142 # Gravatar stores only the part of the URL before the size, since that's the
143 # one computationally more expensive. This also allows reuse of the cache for
144 # different sizes (for this particular engine).
146 my $email = lc shift;
148 $avatar_cache{$email} ||=
149 "http://www.gravatar.com/avatar/" .
150 Digest
::MD5
::md5_hex
($email) . "?s=";
151 return $avatar_cache{$email} . $size;
154 # Insert an avatar for the given $email at the given $size if the feature
157 my ($email, %opts) = @_;
158 my $pre_white = ($opts{-pad_before
} ?
" " : "");
159 my $post_white = ($opts{-pad_after
} ?
" " : "");
160 $opts{-size
} ||= 'default';
161 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
163 if ($git_avatar eq 'gravatar') {
164 $url = gravatar_url
($email, $size);
165 } elsif ($git_avatar eq 'picon') {
166 $url = picon_url
($email);
168 # Other providers can be added by extending the if chain, defining $url
169 # as needed. If no variant puts something in $url, we assume avatars
170 # are completely disabled/unavailable.
173 "<img width=\"$size\" " .
174 "class=\"avatar\" " .
183 sub format_search_author
{
184 my ($author, $searchtype, $displaytext) = @_;
185 my $have_search = gitweb_check_feature
('search');
189 if ($searchtype eq 'author') {
190 $performed = "authored";
191 } elsif ($searchtype eq 'committer') {
192 $performed = "committed";
195 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
197 searchtype
=>$searchtype), class=>"list",
198 title
=>"Search for commits $performed by $author"},
206 # format the author name of the given commit with the given tag
207 # the author name is chopped and escaped according to the other
208 # optional parameters (see chop_str).
209 sub format_author_html
{
212 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
213 return "<$tag class=\"author\">" .
214 format_search_author
($co->{'author_name'}, "author",
215 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
220 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
221 sub format_git_diff_header_line
{
223 my $diffinfo = shift;
224 my ($from, $to) = @_;
226 if ($diffinfo->{'nparents'}) {
228 $line =~ s!^(diff (.*?) )"?.*$!$1!;
230 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
231 esc_path
($to->{'file'}));
232 } else { # file was deleted (no href)
233 $line .= esc_path
($to->{'file'});
237 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
238 if ($from->{'href'}) {
239 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
240 'a/' . esc_path
($from->{'file'}));
241 } else { # file was added (no href)
242 $line .= 'a/' . esc_path
($from->{'file'});
246 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
247 'b/' . esc_path
($to->{'file'}));
248 } else { # file was deleted
249 $line .= 'b/' . esc_path
($to->{'file'});
253 return "<div class=\"diff header\">$line</div>\n";
256 # format extended diff header line, before patch itself
257 sub format_extended_diff_header_line
{
259 my $diffinfo = shift;
260 my ($from, $to) = @_;
263 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
264 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
265 esc_path
($from->{'file'}));
267 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
268 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
269 esc_path
($to->{'file'}));
271 # match single <mode>
272 if ($line =~ m/\s(\d{6})$/) {
273 $line .= '<span class="info"> (' .
278 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
279 # can match only for combined diff
281 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
282 if ($from->{'href'}[$i]) {
283 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
285 substr($diffinfo->{'from_id'}[$i],0,7));
290 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
294 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
295 substr($diffinfo->{'to_id'},0,7));
300 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
301 # can match only for ordinary diff
302 my ($from_link, $to_link);
303 if ($from->{'href'}) {
304 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
305 substr($diffinfo->{'from_id'},0,7));
307 $from_link = '0' x
7;
310 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
311 substr($diffinfo->{'to_id'},0,7));
315 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
316 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
319 return $line . "<br/>\n";
322 # format from-file/to-file diff header
323 sub format_diff_from_to_header
{
324 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
329 #assert($line =~ m/^---/) if DEBUG;
330 # no extra formatting for "^--- /dev/null"
331 if (! $diffinfo->{'nparents'}) {
332 # ordinary (single parent) diff
333 if ($line =~ m!^--- "?a/!) {
334 if ($from->{'href'}) {
336 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
337 esc_path
($from->{'file'}));
340 esc_path
($from->{'file'});
343 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
346 # combined diff (merge commit)
347 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
348 if ($from->{'href'}[$i]) {
350 $cgi->a({-href
=>href
(action
=>"blobdiff",
351 hash_parent
=>$diffinfo->{'from_id'}[$i],
352 hash_parent_base
=>$parents[$i],
353 file_parent
=>$from->{'file'}[$i],
354 hash
=>$diffinfo->{'to_id'},
356 file_name
=>$to->{'file'}),
358 -title
=>"diff" . ($i+1)},
361 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
362 esc_path
($from->{'file'}[$i]));
364 $line = '--- /dev/null';
366 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
371 #assert($line =~ m/^\+\+\+/) if DEBUG;
372 # no extra formatting for "^+++ /dev/null"
373 if ($line =~ m!^\+\+\+ "?b/!) {
376 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
377 esc_path
($to->{'file'}));
380 esc_path
($to->{'file'});
383 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
388 # create note for patch simplified by combined diff
389 sub format_diff_cc_simplified
{
390 my ($diffinfo, @parents) = @_;
393 $result .= "<div class=\"diff header\">" .
395 if (!is_deleted
($diffinfo)) {
396 $result .= $cgi->a({-href
=> href
(action
=>"blob",
398 hash
=>$diffinfo->{'to_id'},
399 file_name
=>$diffinfo->{'to_file'}),
401 esc_path
($diffinfo->{'to_file'}));
403 $result .= esc_path
($diffinfo->{'to_file'});
405 $result .= "</div>\n" . # class="diff header"
406 "<div class=\"diff nodifferences\">" .
408 "</div>\n"; # class="diff nodifferences"
413 # format patch (diff) line (not to be used for diff headers)
414 sub format_diff_line
{
416 my ($from, $to) = @_;
421 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
423 my $prefix = substr($line, 0, scalar @
{$from->{'href'}});
424 if ($line =~ m/^\@{3}/) {
425 $diff_class = " chunk_header";
426 } elsif ($line =~ m/^\\/) {
427 $diff_class = " incomplete";
428 } elsif ($prefix =~ tr/+/+/) {
429 $diff_class = " add";
430 } elsif ($prefix =~ tr/-/-/) {
431 $diff_class = " rem";
434 # assume ordinary diff
435 my $char = substr($line, 0, 1);
437 $diff_class = " add";
438 } elsif ($char eq '-') {
439 $diff_class = " rem";
440 } elsif ($char eq '@') {
441 $diff_class = " chunk_header";
442 } elsif ($char eq "\\") {
443 $diff_class = " incomplete";
446 $line = untabify
($line);
447 if ($from && $to && $line =~ m/^\@{2} /) {
448 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
449 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
451 $from_lines = 0 unless defined $from_lines;
452 $to_lines = 0 unless defined $to_lines;
454 if ($from->{'href'}) {
455 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
456 -class=>"list"}, $from_text);
459 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
460 -class=>"list"}, $to_text);
462 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
463 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
464 return "<div class=\"diff$diff_class\">$line</div>\n";
465 } elsif ($from && $to && $line =~ m/^\@{3}/) {
466 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
467 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
469 @from_text = split(' ', $ranges);
470 for (my $i = 0; $i < @from_text; ++$i) {
471 ($from_start[$i], $from_nlines[$i]) =
472 (split(',', substr($from_text[$i], 1)), 0);
475 $to_text = pop @from_text;
476 $to_start = pop @from_start;
477 $to_nlines = pop @from_nlines;
479 $line = "<span class=\"chunk_info\">$prefix ";
480 for (my $i = 0; $i < @from_text; ++$i) {
481 if ($from->{'href'}[$i]) {
482 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
483 -class=>"list"}, $from_text[$i]);
485 $line .= $from_text[$i];
490 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
491 -class=>"list"}, $to_text);
495 $line .= " $prefix</span>" .
496 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
497 return "<div class=\"diff$diff_class\">$line</div>\n";
499 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
502 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
503 # linked. Pass the hash of the tree/commit to snapshot.
504 sub format_snapshot_links
{
506 my $num_fmts = @snapshot_fmts;
508 # A parenthesized list of links bearing format names.
509 # e.g. "snapshot (_tar.gz_ _zip_)"
510 return "snapshot (" . join(' ', map
517 }, $known_snapshot_formats{$_}{'display'})
518 , @snapshot_fmts) . ")";
519 } elsif ($num_fmts == 1) {
520 # A single "snapshot" link whose tooltip bears the format name.
522 my ($fmt) = @snapshot_fmts;
528 snapshot_format
=>$fmt
530 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
532 } else { # $num_fmts == 0