gitweb: Create Gitweb::Format module
[git/gsoc2010-gitweb.git] / gitweb / lib / Gitweb / Format.pm
blobdc535bd9071aa2516bdb46843756507be8a39c7d
1 #!/usr/bin/perl
3 # Gitweb::Format -- gitweb's format_* subs package
5 # This program is licensed under the GPLv2
7 package Gitweb::Format;
9 use strict;
10 use warnings;
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 {
32 my $line = shift;
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);
38 }eg;
40 return $line;
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 {
51 my ($refs, $id) = @_;
52 my $markers = '';
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?/(.*)$!) {
63 $type = $1;
64 $name = $2;
65 } else {
66 $type = "ref";
67 $name = $ref;
70 my $class = $type;
71 $class .= " indirect" if $indirect;
73 my $dest_action = "shortlog";
75 if ($indirect) {
76 $dest_action = "tag" unless $action eq "tag";
77 } elsif ($action =~ /^(history|(short)?log)$/) {
78 $dest_action = $action;
81 my $dest = "";
82 $dest .= "refs/" unless $ref =~ m!^refs/!;
83 $dest .= $ref;
85 my $link = $cgi->a({
86 -href => href(
87 action=>$dest_action,
88 hash=>$dest
89 )}, $name);
91 $markers .= " <span class=\"$class\" title=\"$ref\">" .
92 $link . "</span>";
96 if ($markers) {
97 return ' <span class="refs">'. $markers . '</span>';
98 } else {
99 return "";
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;
113 } else {
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
129 sub picon_url {
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/" .
135 "$domain/$user/" .
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).
145 sub gravatar_url {
146 my $email = lc shift;
147 my $size = 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
155 # is enabled.
156 sub git_get_avatar {
157 my ($email, %opts) = @_;
158 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
159 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
160 $opts{-size} ||= 'default';
161 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
162 my $url = "";
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.
171 if ($url) {
172 return $pre_white .
173 "<img width=\"$size\" " .
174 "class=\"avatar\" " .
175 "src=\"$url\" " .
176 "alt=\"\" " .
177 "/>" . $post_white;
178 } else {
179 return "";
183 sub format_search_author {
184 my ($author, $searchtype, $displaytext) = @_;
185 my $have_search = gitweb_check_feature('search');
187 if ($have_search) {
188 my $performed = "";
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,
196 searchtext=>$author,
197 searchtype=>$searchtype), class=>"list",
198 title=>"Search for commits $performed by $author"},
199 $displaytext);
201 } else {
202 return $displaytext;
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 {
210 my $tag = shift;
211 my $co = shift;
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) .
216 $author) .
217 "</$tag>";
220 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
221 sub format_git_diff_header_line {
222 my $line = shift;
223 my $diffinfo = shift;
224 my ($from, $to) = @_;
226 if ($diffinfo->{'nparents'}) {
227 # combined diff
228 $line =~ s!^(diff (.*?) )"?.*$!$1!;
229 if ($to->{'href'}) {
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'});
235 } else {
236 # "ordinary" diff
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'});
244 $line .= ' ';
245 if ($to->{'href'}) {
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 {
258 my $line = shift;
259 my $diffinfo = shift;
260 my ($from, $to) = @_;
262 # match <path>
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"> (' .
274 file_type_long($1) .
275 ')</span>';
277 # match <hash>
278 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
279 # can match only for combined diff
280 $line = 'index ';
281 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
282 if ($from->{'href'}[$i]) {
283 $line .= $cgi->a({-href=>$from->{'href'}[$i],
284 -class=>"hash"},
285 substr($diffinfo->{'from_id'}[$i],0,7));
286 } else {
287 $line .= '0' x 7;
289 # separator
290 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
292 $line .= '..';
293 if ($to->{'href'}) {
294 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
295 substr($diffinfo->{'to_id'},0,7));
296 } else {
297 $line .= '0' x 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));
306 } else {
307 $from_link = '0' x 7;
309 if ($to->{'href'}) {
310 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
311 substr($diffinfo->{'to_id'},0,7));
312 } else {
313 $to_link = '0' x 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) = @_;
325 my $line;
326 my $result = '';
328 $line = $from_line;
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'}) {
335 $line = '--- a/' .
336 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
337 esc_path($from->{'file'}));
338 } else {
339 $line = '--- a/' .
340 esc_path($from->{'file'});
343 $result .= qq!<div class="diff from_file">$line</div>\n!;
345 } else {
346 # combined diff (merge commit)
347 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
348 if ($from->{'href'}[$i]) {
349 $line = '--- ' .
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'},
355 hash_base=>$hash,
356 file_name=>$to->{'file'}),
357 -class=>"path",
358 -title=>"diff" . ($i+1)},
359 $i+1) .
360 '/' .
361 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
362 esc_path($from->{'file'}[$i]));
363 } else {
364 $line = '--- /dev/null';
366 $result .= qq!<div class="diff from_file">$line</div>\n!;
370 $line = $to_line;
371 #assert($line =~ m/^\+\+\+/) if DEBUG;
372 # no extra formatting for "^+++ /dev/null"
373 if ($line =~ m!^\+\+\+ "?b/!) {
374 if ($to->{'href'}) {
375 $line = '+++ b/' .
376 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
377 esc_path($to->{'file'}));
378 } else {
379 $line = '+++ b/' .
380 esc_path($to->{'file'});
383 $result .= qq!<div class="diff to_file">$line</div>\n!;
385 return $result;
388 # create note for patch simplified by combined diff
389 sub format_diff_cc_simplified {
390 my ($diffinfo, @parents) = @_;
391 my $result = '';
393 $result .= "<div class=\"diff header\">" .
394 "diff --cc ";
395 if (!is_deleted($diffinfo)) {
396 $result .= $cgi->a({-href => href(action=>"blob",
397 hash_base=>$hash,
398 hash=>$diffinfo->{'to_id'},
399 file_name=>$diffinfo->{'to_file'}),
400 -class => "path"},
401 esc_path($diffinfo->{'to_file'}));
402 } else {
403 $result .= esc_path($diffinfo->{'to_file'});
405 $result .= "</div>\n" . # class="diff header"
406 "<div class=\"diff nodifferences\">" .
407 "Simple merge" .
408 "</div>\n"; # class="diff nodifferences"
410 return $result;
413 # format patch (diff) line (not to be used for diff headers)
414 sub format_diff_line {
415 my $line = shift;
416 my ($from, $to) = @_;
417 my $diff_class = "";
419 chomp $line;
421 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
422 # combined diff
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";
433 } else {
434 # assume ordinary diff
435 my $char = substr($line, 0, 1);
436 if ($char eq '+') {
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);
458 if ($to->{'href'}) {
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]);
484 } else {
485 $line .= $from_text[$i];
487 $line .= " ";
489 if ($to->{'href'}) {
490 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
491 -class=>"list"}, $to_text);
492 } else {
493 $line .= $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 {
505 my ($hash) = @_;
506 my $num_fmts = @snapshot_fmts;
507 if ($num_fmts > 1) {
508 # A parenthesized list of links bearing format names.
509 # e.g. "snapshot (_tar.gz_ _zip_)"
510 return "snapshot (" . join(' ', map
511 $cgi->a({
512 -href => href(
513 action=>"snapshot",
514 hash=>$hash,
515 snapshot_format=>$_
517 }, $known_snapshot_formats{$_}{'display'})
518 , @snapshot_fmts) . ")";
519 } elsif ($num_fmts == 1) {
520 # A single "snapshot" link whose tooltip bears the format name.
521 # i.e. "_snapshot_"
522 my ($fmt) = @snapshot_fmts;
523 return
524 $cgi->a({
525 -href => href(
526 action=>"snapshot",
527 hash=>$hash,
528 snapshot_format=>$fmt
530 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
531 }, "snapshot");
532 } else { # $num_fmts == 0
533 return undef;