tests/run-tests.html: move noscript into body
[git-browser.git] / git-diff.cgi
blob845326c08e11744e42a5740bf3d82ec86f10e1c0
1 #!/usr/bin/env perl
3 # (C) 2005, Artem Khodush <greenkaa@gmail.com>
4 # (C) 2020, Kyle J. McKay <mackyle@gmail.com>
5 # All rights reserved.
7 # Parts originally from gitweb.cgi
8 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
9 # (C) 2005, Christian Gierke <ch@gierke.de>
11 # This program is licensed under the GPL v2
13 use 5.008;
14 use strict;
15 no warnings;
16 use CGI qw(:standard :escapeHTML -nosticky);
17 use CGI::Util qw(unescape);
18 #use CGI::Carp qw(fatalsToBrowser);
20 binmode STDOUT, ':utf8';
22 my $cgi=CGI::new();
24 package git::inner;
26 use Encode;
27 use File::Spec;
28 use vars qw($gitdir $cgi $urlbase $action $hash $hash_parent);
30 # location of the git-core binaries
31 $git::inner::gitbin="git";
32 # location of the gitweb URL
33 $git::inner::gitweb="/gitweb.cgi";
34 # include blame links?
35 $git::inner::blame_links=1;
36 $git::inner::blame_action='blame_incremental';
38 # rename detection options for git-diff-tree
39 # - default is '-M', with the cost proportional to
40 # (number of removed files) * (number of new files).
41 # - more costly is '-C' (which implies '-M'), with the cost proportional to
42 # (number of changed files + number of removed files) * (number of new files)
43 # - even more costly is '-C', '--find-copies-harder' with cost
44 # (number of files in the original tree) * (number of new files)
45 # - one might want to include '-B' option, e.g. '-B', '-M'
46 @git::inner::diff_opts = ('-B', '-C');
48 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
49 sub cmd_pipe {
50 open(NULL, '>', File::Spec->devnull) or die "Cannot open devnull: $!\n";
51 open(SAVEERR, ">&STDERR") || die "couldn't dup STDERR: $!\n";
52 open(STDERR, ">&NULL") || die "couldn't dup NULL to STDERR: $!\n";
53 my $result = open(my $fd, "-|", @_);
54 open(STDERR, ">&SAVEERR") || die "couldn't dup SAVERR to STDERR: $!\n";
55 close(SAVEERR) or die "couldn't close SAVEERR: $!\n";
56 close(NULL) or die "couldn't close NULL: $!\n";
57 return $result ? $fd : undef;
60 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
61 # returns undef and sets a non-zero $! if the pipe is empty and
62 # the command failed (otherwise, if the command succeeded and the
63 # pipe is empty, a read-only handle on devnull is returned).
64 sub git_cmd_pipe {
65 my $p = cmd_pipe $git::inner::gitbin, "--git-dir=".$gitdir, @_;
66 defined($p) or return undef;
67 eof($p) or return $p;
68 close($p);
69 my $e = $?;
70 $e = ($e >= 256) ? ($e > 32768 ? 255 : $e >> 8) : ($e & 0x7f) + 128 if $e;
71 $e and $! = $e, return undef;
72 undef $p;
73 return open($p, '<', File::Spec->devnull) ? $p : undef;
76 my $fallback_encoding;
77 BEGIN {
78 $fallback_encoding = Encode::find_encoding('Windows-1252');
79 $fallback_encoding = Encode::find_encoding('ISO-8859-1')
80 unless $fallback_encoding;
83 # decode sequences of octets in utf8 into Perl's internal form,
84 # which is utf-8 with utf8 flag set if needed. git-diff writes out
85 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
86 sub to_utf8 {
87 my $str = shift || '';
88 if (Encode::is_utf8($str) || utf8::decode($str)) {
89 return $str;
90 } else {
91 return $fallback_encoding->decode($str, Encode::FB_DEFAULT);
95 # much simpler than including all of Fcntl :mode since Git's modes are fixed
96 sub S_ISREG {($_[0] & 0170000) == 0100000}
98 # Git only knows about these modes:
99 # 040000 - directory
100 # 100644 - plain file
101 # 100755 - executable file
102 # 120000 - symbolic link
103 # 160000 - submodule
104 sub file_type {
105 my $m = shift;
106 defined($m) or return "";
107 $m =~ /^[0-7]+$/os or return $m;
108 my $mx = oct($m);
109 my $ft = ($mx & 0170000) >> 12;
110 # in rough order of expected decreasing probability
111 $ft == 010 and return ($mx & 0111) ? "executable" : "file";
112 $ft == 004 and return "directory";
113 $ft == 012 and return "symlink";
114 $ft == 016 and return "submodule";
115 return "unknown";
118 # always correct version of $cgi->a({-href = href("-anchor", "...")}, "...")
119 sub a_anchor {
120 my ($opts, $text) = @_;
121 my $href = CGI::escapeHTML($opts->{"-href"});
122 return "<a href=\"$href\">".CGI::escapeHTML($text)."</a>";
125 # a much simpler version than the one from gitweb
126 # -anchor makes a fragment link, everything else is ignored
127 # otherwise any combination of the following are allowed:
128 # action -> 'a' parameter
129 # hash -> 'h' parameter
130 # hash_base -> 'hb' parameter
131 # hash_parent -> 'hp' parameter
132 # hash_parent_base -> 'hpb' parameter
133 # file_name -> 'f' parameter
134 # file_parent -> 'fp' parameter
135 my %pmap;
136 BEGIN { %pmap = (
137 action => '0a',
138 hash => '1h',
139 hash_base => '2hb',
140 hash_parent => '3hp',
141 hash_parent_base => '4hpb',
142 file_name => '5f',
143 file_parent => '6fp'
145 sub href {
146 my %opts = @_;
147 if (exists($opts{"-anchor"})) {
148 return '#'.esc_param($opts{"-anchor"});
150 my @params = ();
151 foreach my $k (keys %opts) {
152 if (exists($pmap{$k})) {
153 my $p = $pmap{$k};
154 push(@params, $p.'='.esc_param($opts{$k}));
157 @params = map(substr($_,1), sort @params);
158 return @params ? $urlbase.'?'.join('&',@params) : "";
162 ## BEGIN gitweb.cgi source
165 # quote unsafe chars, but keep the slash, even when it's not
166 # correct, but quoted slashes look too horrible in bookmarks
167 sub esc_param {
168 my $str = shift;
169 return undef unless defined $str;
170 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
171 $str =~ s/ /\+/g;
172 return $str;
175 # replace invalid utf8 character with SUBSTITUTION sequence
176 sub esc_html {
177 my $str = shift;
178 my %opts = @_;
180 return undef unless defined $str;
182 $str = to_utf8($str);
183 $str = $cgi->escapeHTML($str);
184 if ($opts{'-nbsp'}) {
185 $str =~ s/ /&#160;/g;
187 use bytes;
188 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
189 return $str;
192 # quote control characters and escape filename to HTML
193 sub esc_path {
194 my $str = shift;
195 my %opts = @_;
197 return undef unless defined $str;
199 $str = to_utf8($str);
200 $str = $cgi->escapeHTML($str);
201 if ($opts{'-nbsp'}) {
202 $str =~ s/ /&#160;/g;
204 use bytes;
205 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
206 return $str;
209 # Make control characters "printable", using character escape codes (CEC)
210 sub quot_cec {
211 my $cntrl = shift;
212 my %opts = @_;
213 my %es = ( # character escape codes, aka escape sequences
214 "\t" => '\t', # tab (HT)
215 "\n" => '\n', # line feed (LF)
216 "\r" => '\r', # carrige return (CR)
217 "\f" => '\f', # form feed (FF)
218 "\b" => '\b', # backspace (BS)
219 "\a" => '\a', # alarm (bell) (BEL)
220 "\e" => '\e', # escape (ESC)
221 "\013" => '\v', # vertical tab (VT)
222 "\000" => '\0', # nul character (NUL)
224 my $chr = ( (exists $es{$cntrl})
225 ? $es{$cntrl}
226 : sprintf('\x%02x', ord($cntrl)) );
227 if ($opts{-nohtml}) {
228 return $chr;
229 } else {
230 return "<span class=\"cntrl\">$chr</span>";
234 # git may return quoted and escaped filenames
235 sub unquote {
236 my $str = shift;
238 sub unq {
239 my $seq = shift;
240 my %es = ( # character escape codes, aka escape sequences
241 't' => "\t", # tab (HT, TAB)
242 'n' => "\n", # newline (NL)
243 'r' => "\r", # return (CR)
244 'f' => "\f", # form feed (FF)
245 'b' => "\b", # backspace (BS)
246 'a' => "\a", # alarm (bell) (BEL)
247 'e' => "\e", # escape (ESC)
248 'v' => "\013", # vertical tab (VT)
251 if ($seq =~ m/^[0-7]{1,3}$/) {
252 # octal char sequence
253 return chr(oct($seq));
254 } elsif (exists $es{$seq}) {
255 # C escape sequence, aka character escape code
256 return $es{$seq};
258 # quoted ordinary character
259 return $seq;
262 if ($str =~ m/^"(.*)"$/) {
263 # needs unquoting
264 $str = $1;
265 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
267 return $str;
270 # escape tabs (convert tabs to spaces)
271 # improved faster version from Markdown.pl
272 sub untabify {
273 my $line = shift;
274 # From the Perl camel book section "Fluent Perl" but modified a bit
275 $line =~ s/(.*?)(\t+)/$1 . ' ' x (length($2) * 8 - length($1) % 8)/ges;
276 return $line;
279 # Highlight selected fragments of string, using given CSS class,
280 # and escape HTML. It is assumed that fragments do not overlap.
281 # Regions are passed as list of pairs (array references).
283 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
284 # '<span class="mark">foo</span>bar'
285 sub esc_html_hl_regions {
286 my ($str, $css_class, @sel) = @_;
287 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
288 @sel = grep { ref($_) eq 'ARRAY' } @sel;
289 return esc_html($str, %opts) unless @sel;
291 my $out = '';
292 my $pos = 0;
294 for my $s (@sel) {
295 my ($begin, $end) = @$s;
297 # Don't create empty <span> elements.
298 next if $end <= $begin;
300 my $escaped = esc_html(substr($str, $begin, $end - $begin),
301 %opts);
303 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
304 if ($begin - $pos > 0);
305 $out .= $cgi->span({-class => $css_class}, $escaped);
307 $pos = $end;
309 $out .= esc_html(substr($str, $pos), %opts)
310 if ($pos < length($str));
312 return $out;
315 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
316 sub format_git_diff_header_line {
317 my $line = shift;
318 my $diffinfo = shift;
319 my ($from, $to) = @_;
321 if ($diffinfo->{'nparents'}) {
322 # combined diff
323 $line =~ s!^(diff (.*?) )"?.*$!$1!;
324 if ($to->{'href'}) {
325 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
326 esc_path($to->{'file'}));
327 } else { # file was deleted (no href)
328 $line .= esc_path($to->{'file'});
330 } else {
331 # "ordinary" diff
332 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
333 if ($from->{'href'}) {
334 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
335 'a/' . esc_path($from->{'file'}));
336 } else { # file was added (no href)
337 $line .= 'a/' . esc_path($from->{'file'});
339 $line .= ' ';
340 if ($to->{'href'}) {
341 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
342 'b/' . esc_path($to->{'file'}));
343 } else { # file was deleted
344 $line .= 'b/' . esc_path($to->{'file'});
348 return "<div class=\"diff header\">$line</div>\n";
351 # format extended diff header line, before patch itself
352 sub format_extended_diff_header_line {
353 my $line = shift;
354 my $diffinfo = shift;
355 my ($from, $to) = @_;
357 # match <path>
358 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
359 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
360 esc_path($from->{'file'}));
362 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
363 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
364 esc_path($to->{'file'}));
366 # match single <mode>
367 if ($line =~ m/\s(\d{6})$/) {
368 $line .= '<span class="info"> (' .
369 file_type($1) .
370 ')</span>';
372 # match <hash>
373 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
374 # can match only for combined diff
375 $line = 'index ';
376 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
377 if ($from->{'href'}[$i]) {
378 $line .= $cgi->a({-href=>$from->{'href'}[$i],
379 -class=>"hash"},
380 substr($diffinfo->{'from_id'}[$i],0,7));
381 } else {
382 $line .= '0' x 7;
384 # separator
385 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
387 $line .= '..';
388 if ($to->{'href'}) {
389 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
390 substr($diffinfo->{'to_id'},0,7));
391 } else {
392 $line .= '0' x 7;
395 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
396 # can match only for ordinary diff
397 my ($from_link, $to_link);
398 if ($from->{'href'}) {
399 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
400 substr($diffinfo->{'from_id'},0,7));
401 } else {
402 $from_link = '0' x 7;
404 if ($to->{'href'}) {
405 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
406 substr($diffinfo->{'to_id'},0,7));
407 } else {
408 $to_link = '0' x 7;
410 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
411 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
414 return $line . "<br/>\n";
417 # format from-file/to-file diff header
418 sub format_diff_from_to_header {
419 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
420 my $line;
421 my $result = '';
423 $line = $from_line;
424 #assert($line =~ m/^---/) if DEBUG;
425 # no extra formatting for "^--- /dev/null"
426 if (! $diffinfo->{'nparents'}) {
427 # ordinary (single parent) diff
428 if ($line =~ m!^--- "?a/!) {
429 if ($from->{'href'}) {
430 $line = '--- a/' .
431 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
432 esc_path($from->{'file'}));
433 } else {
434 $line = '--- a/' .
435 esc_path($from->{'file'});
438 $result .= qq!<div class="diff from_file">$line</div>\n!;
440 } else {
441 # combined diff (merge commit)
442 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
443 if ($from->{'href'}[$i]) {
444 $line = '--- ' .
445 $cgi->a({-href=>href(action=>"blobdiff",
446 hash_parent=>$diffinfo->{'from_id'}[$i],
447 hash_parent_base=>$parents[$i],
448 file_parent=>$from->{'file'}[$i],
449 hash=>$diffinfo->{'to_id'},
450 hash_base=>$hash,
451 file_name=>$to->{'file'}),
452 -class=>"path",
453 -title=>"diff" . ($i+1)},
454 $i+1) .
455 '/' .
456 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
457 esc_path($from->{'file'}[$i]));
458 } else {
459 $line = '--- /dev/null';
461 $result .= qq!<div class="diff from_file">$line</div>\n!;
465 $line = $to_line;
466 #assert($line =~ m/^\+\+\+/) if DEBUG;
467 # no extra formatting for "^+++ /dev/null"
468 if ($line =~ m!^\+\+\+ "?b/!) {
469 if ($to->{'href'}) {
470 $line = '+++ b/' .
471 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
472 esc_path($to->{'file'}));
473 } else {
474 $line = '+++ b/' .
475 esc_path($to->{'file'});
478 $result .= qq!<div class="diff to_file">$line</div>\n!;
480 return $result;
483 # create note for patch simplified by combined diff
484 sub format_diff_cc_simplified {
485 my ($diffinfo, @parents) = @_;
486 my $result = '';
488 $result .= "<div class=\"diff header\">" .
489 "diff --cc ";
490 if (!is_deleted($diffinfo)) {
491 $result .= $cgi->a({-href => href(action=>"blob",
492 hash_base=>$hash,
493 hash=>$diffinfo->{'to_id'},
494 file_name=>$diffinfo->{'to_file'}),
495 -class => "path"},
496 esc_path($diffinfo->{'to_file'}));
497 } else {
498 $result .= esc_path($diffinfo->{'to_file'});
500 $result .= "</div>\n" . # class="diff header"
501 "<div class=\"diff nodifferences\">" .
502 "Simple merge" .
503 "</div>\n"; # class="diff nodifferences"
505 return $result;
508 sub diff_line_class {
509 my ($line, $from, $to) = @_;
511 # ordinary diff
512 my $num_sign = 1;
513 # combined diff
514 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
515 $num_sign = scalar @{$from->{'href'}};
518 my @diff_line_classifier = (
519 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
520 { regexp => qr/^\\/, class => "incomplete" },
521 { regexp => qr/^ {$num_sign}/, class => "ctx" },
522 # classifier for context must come before classifier add/rem,
523 # or we would have to use more complicated regexp, for example
524 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
525 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
526 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
528 for my $clsfy (@diff_line_classifier) {
529 return $clsfy->{'class'}
530 if ($line =~ $clsfy->{'regexp'});
533 # fallback
534 return "";
537 # assumes that $from and $to are defined and correctly filled,
538 # and that $line holds a line of chunk header for unified diff
539 sub format_unidiff_chunk_header {
540 my ($line, $from, $to) = @_;
542 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
543 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
545 $from_lines = 0 unless defined $from_lines;
546 $to_lines = 0 unless defined $to_lines;
548 if ($from->{'href'}) {
549 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
550 -class=>"list"}, $from_text);
552 if ($to->{'href'}) {
553 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
554 -class=>"list"}, $to_text);
556 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
557 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
558 return $line;
561 # assumes that $from and $to are defined and correctly filled,
562 # and that $line holds a line of chunk header for combined diff
563 sub format_cc_diff_chunk_header {
564 my ($line, $from, $to) = @_;
566 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
567 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
569 @from_text = split(' ', $ranges);
570 for (my $i = 0; $i < @from_text; ++$i) {
571 ($from_start[$i], $from_nlines[$i]) =
572 (split(',', substr($from_text[$i], 1)), 0);
575 $to_text = pop @from_text;
576 $to_start = pop @from_start;
577 $to_nlines = pop @from_nlines;
579 $line = "<span class=\"chunk_info\">$prefix ";
580 for (my $i = 0; $i < @from_text; ++$i) {
581 if ($from->{'href'}[$i]) {
582 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
583 -class=>"list"}, $from_text[$i]);
584 } else {
585 $line .= $from_text[$i];
587 $line .= " ";
589 if ($to->{'href'}) {
590 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
591 -class=>"list"}, $to_text);
592 } else {
593 $line .= $to_text;
595 $line .= " $prefix</span>" .
596 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
597 return $line;
600 # process patch (diff) line (not to be used for diff headers),
601 # returning HTML-formatted (but not wrapped) line.
602 # If the line is passed as a reference, it is treated as HTML and not
603 # esc_html()'ed.
604 sub format_diff_line {
605 my ($line, $diff_class, $from, $to) = @_;
607 if (ref($line)) {
608 $line = $$line;
609 } else {
610 chomp $line;
611 $line = untabify($line);
613 if ($from && $to && $line =~ m/^\@{2} /) {
614 $line = format_unidiff_chunk_header($line, $from, $to);
615 } elsif ($from && $to && $line =~ m/^\@{3}/) {
616 $line = format_cc_diff_chunk_header($line, $from, $to);
617 } else {
618 $line = esc_html($line, -nbsp=>1);
622 my $diff_classes = "diff diff_body";
623 $diff_classes .= " $diff_class" if ($diff_class);
624 $line = "<div class=\"$diff_classes\">$line</div>\n";
626 return $line;
629 # get path of entry with given hash at given tree-ish (ref)
630 # used to get 'from' filename for combined diff (merge commit) for renames
631 sub git_get_path_by_hash {
632 my $base = shift || return;
633 my $hash = shift || return;
635 local $/ = "\0";
637 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
638 or return undef;
639 while (my $line = to_utf8(scalar <$fd>)) {
640 chomp $line;
642 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
643 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
644 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
645 close $fd;
646 return $1;
649 close $fd;
650 return undef;
653 # parse line of git-diff-tree "raw" output
654 sub parse_difftree_raw_line {
655 my $line = shift;
656 my %res;
658 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
659 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
660 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
661 $res{'from_mode'} = $1;
662 $res{'to_mode'} = $2;
663 $res{'from_id'} = $3;
664 $res{'to_id'} = $4;
665 $res{'status'} = $5;
666 $res{'similarity'} = $6;
667 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
668 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
669 } else {
670 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
673 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
674 # combined diff (for merge commit)
675 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
676 $res{'nparents'} = length($1);
677 $res{'from_mode'} = [ split(' ', $2) ];
678 $res{'to_mode'} = pop @{$res{'from_mode'}};
679 $res{'from_id'} = [ split(' ', $3) ];
680 $res{'to_id'} = pop @{$res{'from_id'}};
681 $res{'status'} = [ split('', $4) ];
682 $res{'to_file'} = unquote($5);
684 # 'c512b523472485aef4fff9e57b229d9d243c967f'
685 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
686 $res{'commit'} = $1;
689 return wantarray ? %res : \%res;
692 # wrapper: return parsed line of git-diff-tree "raw" output
693 # (the argument might be raw line, or parsed info)
694 sub parsed_difftree_line {
695 my $line_or_ref = shift;
697 if (ref($line_or_ref) eq "HASH") {
698 # pre-parsed (or generated by hand)
699 return $line_or_ref;
700 } else {
701 return parse_difftree_raw_line($line_or_ref);
705 # generates _two_ hashes, references to which are passed as 2 and 3 argument
706 sub parse_from_to_diffinfo {
707 my ($diffinfo, $from, $to, @parents) = @_;
709 if ($diffinfo->{'nparents'}) {
710 # combined diff
711 $from->{'file'} = [];
712 $from->{'href'} = [];
713 fill_from_file_info($diffinfo, @parents)
714 unless exists $diffinfo->{'from_file'};
715 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
716 $from->{'file'}[$i] =
717 defined $diffinfo->{'from_file'}[$i] ?
718 $diffinfo->{'from_file'}[$i] :
719 $diffinfo->{'to_file'};
720 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
721 $from->{'href'}[$i] = href(action=>"blob",
722 hash_base=>$parents[$i],
723 hash=>$diffinfo->{'from_id'}[$i],
724 file_name=>$from->{'file'}[$i]);
725 } else {
726 $from->{'href'}[$i] = undef;
729 } else {
730 # ordinary (not combined) diff
731 $from->{'file'} = $diffinfo->{'from_file'};
732 if ($diffinfo->{'status'} ne "A") { # not new (added) file
733 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
734 hash=>$diffinfo->{'from_id'},
735 file_name=>$from->{'file'});
736 } else {
737 delete $from->{'href'};
741 $to->{'file'} = $diffinfo->{'to_file'};
742 if (!is_deleted($diffinfo)) { # file exists in result
743 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
744 hash=>$diffinfo->{'to_id'},
745 file_name=>$to->{'file'});
746 } else {
747 delete $to->{'href'};
751 # get pre-image filenames for merge (combined) diff
752 sub fill_from_file_info {
753 my ($diff, @parents) = @_;
755 $diff->{'from_file'} = [ ];
756 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
757 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
758 if ($diff->{'status'}[$i] eq 'R' ||
759 $diff->{'status'}[$i] eq 'C') {
760 $diff->{'from_file'}[$i] =
761 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
765 return $diff;
768 # is current raw difftree line of file deletion
769 sub is_deleted {
770 my $diffinfo = shift;
772 return $diffinfo->{'to_id'} =~ /^0{40,}$/os;
775 # does patch correspond to [previous] difftree raw line
776 # $diffinfo - hashref of parsed raw diff format
777 # $patchinfo - hashref of parsed patch diff format
778 # (the same keys as in $diffinfo)
779 sub is_patch_split {
780 my ($diffinfo, $patchinfo) = @_;
782 return defined $diffinfo && defined $patchinfo
783 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
786 sub git_difftree_body {
787 my ($difftree, $hash, @parents) = @_;
788 my ($parent) = $parents[0];
789 my $have_blame = $git::inner::blame_links;
790 if ($#{$difftree} > 10) {
791 print "<div class=\"list_head\">\n";
792 print(($#{$difftree} + 1) . " files changed:\n");
793 print "</div>\n";
796 print "<table class=\"" .
797 (@parents > 1 ? "combined " : "") .
798 "diff_tree\">\n";
800 # header only for combined diff in 'commitdiff' view
801 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
802 if ($has_header) {
803 # table header
804 print "<thead><tr>\n" .
805 "<th></th><th></th>\n"; # filename, patchN link
806 for (my $i = 0; $i < @parents; $i++) {
807 my $par = $parents[$i];
808 print "<th>" .
809 $cgi->a({-href => href(action=>"commitdiff",
810 hash=>$hash, hash_parent=>$par),
811 -title => 'commitdiff to parent number ' .
812 ($i+1) . ': ' . substr($par,0,7)},
813 $i+1) .
814 "&#160;</th>\n";
816 print "</tr></thead>\n<tbody>\n";
819 my $alternate = 1;
820 my $patchno = 0;
821 foreach my $line (@{$difftree}) {
822 my $diff = parsed_difftree_line($line);
824 if ($alternate) {
825 print "<tr class=\"dark\">\n";
826 } else {
827 print "<tr class=\"light\">\n";
829 $alternate ^= 1;
831 if (exists $diff->{'nparents'}) { # combined diff
833 fill_from_file_info($diff, @parents)
834 unless exists $diff->{'from_file'};
836 if (!is_deleted($diff)) {
837 # file exists in the result (child) commit
838 print "<td>" .
839 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
840 file_name=>$diff->{'to_file'},
841 hash_base=>$hash),
842 -class => "list"}, esc_path($diff->{'to_file'})) .
843 "</td>\n";
844 } else {
845 print "<td>" .
846 esc_path($diff->{'to_file'}) .
847 "</td>\n";
850 if ($action eq 'commitdiff') {
851 # link to patch
852 $patchno++;
853 print "<td class=\"link\">" .
854 a_anchor({-href => href(-anchor=>"patch$patchno")},
855 "patch") .
856 " | " .
857 "</td>\n";
860 my $has_history = 0;
861 my $not_deleted = 0;
862 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
863 my $hash_parent = $parents[$i];
864 my $from_hash = $diff->{'from_id'}[$i];
865 my $from_path = $diff->{'from_file'}[$i];
866 my $status = $diff->{'status'}[$i];
868 $has_history ||= ($status ne 'A');
869 $not_deleted ||= ($status ne 'D');
871 if ($status eq 'A') {
872 print "<td class=\"link\" align=\"right\"> | </td>\n";
873 } elsif ($status eq 'D') {
874 print "<td class=\"link\">" .
875 $cgi->a({-href => href(action=>"blob",
876 hash_base=>$hash,
877 hash=>$from_hash,
878 file_name=>$from_path)},
879 "blob" . ($i+1)) .
880 " | </td>\n";
881 } else {
882 if ($diff->{'to_id'} eq $from_hash) {
883 print "<td class=\"link nochange\">";
884 } else {
885 print "<td class=\"link\">";
887 print $cgi->a({-href => href(action=>"blobdiff",
888 hash=>$diff->{'to_id'},
889 hash_parent=>$from_hash,
890 hash_base=>$hash,
891 hash_parent_base=>$hash_parent,
892 file_name=>$diff->{'to_file'},
893 file_parent=>$from_path)},
894 "diff" . ($i+1)) .
895 " | </td>\n";
899 print "<td class=\"link\">";
900 if ($not_deleted) {
901 print $cgi->a({-href => href(action=>"blob",
902 hash=>$diff->{'to_id'},
903 file_name=>$diff->{'to_file'},
904 hash_base=>$hash)},
905 "blob");
906 print " | " if ($has_history);
908 if ($has_history) {
909 print $cgi->a({-href => href(action=>"history",
910 file_name=>$diff->{'to_file'},
911 hash_base=>$hash)},
912 "history");
914 print "</td>\n";
916 print "</tr>\n";
917 next; # instead of 'else' clause, to avoid extra indent
919 # else ordinary diff
921 my ($to_mode_oct, $to_mode_str, $to_file_type);
922 my ($from_mode_oct, $from_mode_str, $from_file_type);
923 if ($diff->{'to_mode'} ne ('0' x 6)) {
924 $to_mode_oct = oct $diff->{'to_mode'};
925 if (S_ISREG($to_mode_oct)) { # only for regular file
926 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
928 $to_file_type = file_type($diff->{'to_mode'});
930 if ($diff->{'from_mode'} ne ('0' x 6)) {
931 $from_mode_oct = oct $diff->{'from_mode'};
932 if (S_ISREG($from_mode_oct)) { # only for regular file
933 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
935 $from_file_type = file_type($diff->{'from_mode'});
938 if ($diff->{'status'} eq "A") { # created
939 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
940 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
941 $mode_chng .= "]</span>";
942 print "<td>";
943 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
944 hash_base=>$hash, file_name=>$diff->{'file'}),
945 -class => "list"}, esc_path($diff->{'file'}));
946 print "</td>\n";
947 print "<td>$mode_chng</td>\n";
948 print "<td class=\"link\">";
949 if ($action eq 'commitdiff') {
950 # link to patch
951 $patchno++;
952 print a_anchor({-href => href(-anchor=>"patch$patchno")},
953 "patch") .
954 " | ";
956 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
957 hash_base=>$hash, file_name=>$diff->{'file'})},
958 "blob");
959 print "</td>\n";
961 } elsif ($diff->{'status'} eq "D") { # deleted
962 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
963 print "<td>";
964 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
965 hash_base=>$parent, file_name=>$diff->{'file'}),
966 -class => "list"}, esc_path($diff->{'file'}));
967 print "</td>\n";
968 print "<td>$mode_chng</td>\n";
969 print "<td class=\"link\">";
970 if ($action eq 'commitdiff') {
971 # link to patch
972 $patchno++;
973 print a_anchor({-href => href(-anchor=>"patch$patchno")},
974 "patch") .
975 " | ";
977 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
978 hash_base=>$parent, file_name=>$diff->{'file'})},
979 "blob") . " | ";
980 if ($have_blame) {
981 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$parent,
982 file_name=>$diff->{'file'}),
983 -class => "blamelink"},
984 "blame") . " | ";
986 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
987 file_name=>$diff->{'file'})},
988 "history");
989 print "</td>\n";
991 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
992 my $mode_chnge = "";
993 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
994 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
995 if ($from_file_type ne $to_file_type) {
996 $mode_chnge .= " from $from_file_type to $to_file_type";
998 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
999 if ($from_mode_str && $to_mode_str) {
1000 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
1001 } elsif ($to_mode_str) {
1002 $mode_chnge .= " mode: $to_mode_str";
1005 $mode_chnge .= "]</span>\n";
1007 print "<td>";
1008 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
1009 hash_base=>$hash, file_name=>$diff->{'file'}),
1010 -class => "list"}, esc_path($diff->{'file'}));
1011 print "</td>\n";
1012 print "<td>$mode_chnge</td>\n";
1013 print "<td class=\"link\">";
1014 if ($action eq 'commitdiff') {
1015 # link to patch
1016 $patchno++;
1017 print a_anchor({-href => href(-anchor=>"patch$patchno")},
1018 "patch") .
1019 " | ";
1020 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
1021 # "commit" view and modified file (not onlu mode changed)
1022 print $cgi->a({-href => href(action=>"blobdiff",
1023 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
1024 hash_base=>$hash, hash_parent_base=>$parent,
1025 file_name=>$diff->{'file'})},
1026 "diff") .
1027 " | ";
1029 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
1030 hash_base=>$hash, file_name=>$diff->{'file'})},
1031 "blob") . " | ";
1032 if ($have_blame) {
1033 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$hash,
1034 file_name=>$diff->{'file'}),
1035 -class => "blamelink"},
1036 "blame") . " | ";
1038 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
1039 file_name=>$diff->{'file'})},
1040 "history");
1041 print "</td>\n";
1043 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
1044 my %status_name = ('R' => 'moved', 'C' => 'copied');
1045 my $nstatus = $status_name{$diff->{'status'}};
1046 my $mode_chng = "";
1047 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
1048 # mode also for directories, so we cannot use $to_mode_str
1049 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
1051 print "<td>" .
1052 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1053 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
1054 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
1055 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
1056 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
1057 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
1058 -class => "list"}, esc_path($diff->{'from_file'})) .
1059 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
1060 "<td class=\"link\">";
1061 if ($action eq 'commitdiff') {
1062 # link to patch
1063 $patchno++;
1064 print a_anchor({-href => href(-anchor=>"patch$patchno")},
1065 "patch") .
1066 " | ";
1067 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
1068 # "commit" view and modified file (not only pure rename or copy)
1069 print $cgi->a({-href => href(action=>"blobdiff",
1070 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
1071 hash_base=>$hash, hash_parent_base=>$parent,
1072 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
1073 "diff") .
1074 " | ";
1076 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
1077 hash_base=>$parent, file_name=>$diff->{'to_file'})},
1078 "blob") . " | ";
1079 if ($have_blame) {
1080 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$hash,
1081 file_name=>$diff->{'to_file'}),
1082 -class => "blamelink"},
1083 "blame") . " | ";
1085 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
1086 file_name=>$diff->{'to_file'})},
1087 "history");
1088 print "</td>\n";
1090 } # we should not encounter Unmerged (U) or Unknown (X) status
1091 print "</tr>\n";
1093 print "</tbody>" if $has_header;
1094 print "</table>\n";
1097 # Print context lines and then rem/add lines in a side-by-side manner.
1098 sub print_sidebyside_diff_lines {
1099 my ($ctx, $rem, $add) = @_;
1101 # print context block before add/rem block
1102 if (@$ctx) {
1103 print join '',
1104 '<div class="chunk_block ctx">',
1105 '<div class="old">',
1106 @$ctx,
1107 '</div>',
1108 '<div class="new">',
1109 @$ctx,
1110 '</div>',
1111 '</div>';
1114 if (!@$add) {
1115 # pure removal
1116 print join '',
1117 '<div class="chunk_block rem">',
1118 '<div class="old">',
1119 @$rem,
1120 '</div>',
1121 '</div>';
1122 } elsif (!@$rem) {
1123 # pure addition
1124 print join '',
1125 '<div class="chunk_block add">',
1126 '<div class="new">',
1127 @$add,
1128 '</div>',
1129 '</div>';
1130 } else {
1131 print join '',
1132 '<div class="chunk_block chg">',
1133 '<div class="old">',
1134 @$rem,
1135 '</div>',
1136 '<div class="new">',
1137 @$add,
1138 '</div>',
1139 '</div>';
1143 # Print context lines and then rem/add lines in inline manner.
1144 sub print_inline_diff_lines {
1145 my ($ctx, $rem, $add) = @_;
1147 print @$ctx, @$rem, @$add;
1150 # Format removed and added line, mark changed part and HTML-format them.
1151 # Implementation is based on contrib/diff-highlight
1152 sub format_rem_add_lines_pair {
1153 my ($rem, $add, $num_parents) = @_;
1155 # We need to untabify lines before split()'ing them;
1156 # otherwise offsets would be invalid.
1157 chomp $rem;
1158 chomp $add;
1159 $rem = untabify($rem);
1160 $add = untabify($add);
1162 my @rem = split(//, $rem);
1163 my @add = split(//, $add);
1164 my ($esc_rem, $esc_add);
1165 # Ignore leading +/- characters for each parent.
1166 my ($prefix_len, $suffix_len) = ($num_parents, 0);
1167 my ($prefix_has_nonspace, $suffix_has_nonspace);
1169 my $shorter = (@rem < @add) ? @rem : @add;
1170 while ($prefix_len < $shorter) {
1171 last if ($rem[$prefix_len] ne $add[$prefix_len]);
1173 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
1174 $prefix_len++;
1177 while ($prefix_len + $suffix_len < $shorter) {
1178 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
1180 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
1181 $suffix_len++;
1184 # Mark lines that are different from each other, but have some common
1185 # part that isn't whitespace. If lines are completely different, don't
1186 # mark them because that would make output unreadable, especially if
1187 # diff consists of multiple lines.
1188 if ($prefix_has_nonspace || $suffix_has_nonspace) {
1189 $esc_rem = esc_html_hl_regions($rem, 'marked',
1190 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
1191 $esc_add = esc_html_hl_regions($add, 'marked',
1192 [$prefix_len, @add - $suffix_len], -nbsp=>1);
1193 } else {
1194 $esc_rem = esc_html($rem, -nbsp=>1);
1195 $esc_add = esc_html($add, -nbsp=>1);
1198 return format_diff_line(\$esc_rem, 'rem'),
1199 format_diff_line(\$esc_add, 'add');
1202 # HTML-format diff context, removed and added lines.
1203 sub format_ctx_rem_add_lines {
1204 my ($ctx, $rem, $add, $num_parents) = @_;
1205 my (@new_ctx, @new_rem, @new_add);
1206 my $can_highlight = 0;
1207 my $is_combined = ($num_parents > 1);
1209 # Highlight if every removed line has a corresponding added line.
1210 if (@$add > 0 && @$add == @$rem) {
1211 $can_highlight = 1;
1213 # Highlight lines in combined diff only if the chunk contains
1214 # diff between the same version, e.g.
1216 # - a
1217 # - b
1218 # + c
1219 # + d
1221 # Otherwise the highlightling would be confusing.
1222 if ($is_combined) {
1223 for (my $i = 0; $i < @$add; $i++) {
1224 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
1225 my $prefix_add = substr($add->[$i], 0, $num_parents);
1227 $prefix_rem =~ s/-/+/g;
1229 if ($prefix_rem ne $prefix_add) {
1230 $can_highlight = 0;
1231 last;
1237 if ($can_highlight) {
1238 for (my $i = 0; $i < @$add; $i++) {
1239 my ($line_rem, $line_add) = format_rem_add_lines_pair(
1240 $rem->[$i], $add->[$i], $num_parents);
1241 push @new_rem, $line_rem;
1242 push @new_add, $line_add;
1244 } else {
1245 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
1246 @new_add = map { format_diff_line($_, 'add') } @$add;
1249 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
1251 return (\@new_ctx, \@new_rem, \@new_add);
1254 # Print context lines and then rem/add lines.
1255 sub print_diff_lines {
1256 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
1257 my $is_combined = $num_parents > 1;
1259 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
1260 $num_parents);
1262 if ($diff_style eq 'sidebyside' && !$is_combined) {
1263 print_sidebyside_diff_lines($ctx, $rem, $add);
1264 } else {
1265 # default 'inline' style and unknown styles
1266 print_inline_diff_lines($ctx, $rem, $add);
1270 sub print_diff_chunk {
1271 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
1272 my (@ctx, @rem, @add);
1274 # The class of the previous line.
1275 my $prev_class = '';
1277 return unless @chunk;
1279 # incomplete last line might be among removed or added lines,
1280 # or both, or among context lines: find which
1281 for (my $i = 1; $i < @chunk; $i++) {
1282 if ($chunk[$i][0] eq 'incomplete') {
1283 $chunk[$i][0] = $chunk[$i-1][0];
1287 # guardian
1288 push @chunk, ["", ""];
1290 foreach my $line_info (@chunk) {
1291 my ($class, $line) = @$line_info;
1293 # print chunk headers
1294 if ($class && $class eq 'chunk_header') {
1295 print format_diff_line($line, $class, $from, $to);
1296 next;
1299 ## print from accumulator when have some add/rem lines or end
1300 # of chunk (flush context lines), or when have add and rem
1301 # lines and new block is reached (otherwise add/rem lines could
1302 # be reordered)
1303 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
1304 (@rem && @add && $class ne $prev_class)) {
1305 print_diff_lines(\@ctx, \@rem, \@add,
1306 $diff_style, $num_parents);
1307 @ctx = @rem = @add = ();
1310 ## adding lines to accumulator
1311 # guardian value
1312 last unless $line;
1313 # rem, add or change
1314 if ($class eq 'rem') {
1315 push @rem, $line;
1316 } elsif ($class eq 'add') {
1317 push @add, $line;
1319 # context line
1320 if ($class eq 'ctx') {
1321 push @ctx, $line;
1324 $prev_class = $class;
1328 sub git_patchset_body {
1329 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
1330 my ($hash_parent) = $hash_parents[0];
1332 my $is_combined = (@hash_parents > 1);
1333 my $patch_idx = 0;
1334 my $patch_number = 0;
1335 my $patch_line;
1336 my $diffinfo;
1337 my $to_name;
1338 my (%from, %to);
1339 my @chunk; # for side-by-side diff
1341 print "<div class=\"patchset\">\n";
1343 # skip to first patch
1344 while ($patch_line = to_utf8(scalar <$fd>)) {
1345 chomp $patch_line;
1347 last if ($patch_line =~ m/^diff /);
1350 PATCH:
1351 while ($patch_line) {
1353 # parse "git diff" header line
1354 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
1355 # $1 is from_name, which we do not use
1356 $to_name = unquote($2);
1357 $to_name =~ s!^b/!!;
1358 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
1359 # $1 is 'cc' or 'combined', which we do not use
1360 $to_name = unquote($2);
1361 } else {
1362 $to_name = undef;
1365 # check if current patch belong to current raw line
1366 # and parse raw git-diff line if needed
1367 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
1368 # this is continuation of a split patch
1369 print "<div class=\"patch cont\">\n";
1370 } else {
1371 # advance raw git-diff output if needed
1372 $patch_idx++ if defined $diffinfo;
1374 # read and prepare patch information
1375 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1377 # compact combined diff output can have some patches skipped
1378 # find which patch (using pathname of result) we are at now;
1379 if ($is_combined) {
1380 while ($to_name ne $diffinfo->{'to_file'}) {
1381 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1382 format_diff_cc_simplified($diffinfo, @hash_parents) .
1383 "</div>\n"; # class="patch"
1385 $patch_idx++;
1386 $patch_number++;
1388 last if $patch_idx > $#$difftree;
1389 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1393 # modifies %from, %to hashes
1394 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
1396 # this is first patch for raw difftree line with $patch_idx index
1397 # we index @$difftree array from 0, but number patches from 1
1398 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1401 # git diff header
1402 #assert($patch_line =~ m/^diff /) if DEBUG;
1403 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
1404 $patch_number++;
1405 # print "git diff" header
1406 print format_git_diff_header_line($patch_line, $diffinfo,
1407 \%from, \%to);
1409 # print extended diff header
1410 print "<div class=\"diff extended_header\">\n";
1411 EXTENDED_HEADER:
1412 while ($patch_line = to_utf8(scalar<$fd>)) {
1413 chomp $patch_line;
1415 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
1417 print format_extended_diff_header_line($patch_line, $diffinfo,
1418 \%from, \%to);
1420 print "</div>\n"; # class="diff extended_header"
1422 # from-file/to-file diff header
1423 if (! $patch_line) {
1424 print "</div>\n"; # class="patch"
1425 last PATCH;
1427 next PATCH if ($patch_line =~ m/^diff /);
1428 #assert($patch_line =~ m/^---/) if DEBUG;
1430 my $last_patch_line = $patch_line;
1431 $patch_line = to_utf8(scalar <$fd>);
1432 chomp $patch_line;
1433 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
1435 print format_diff_from_to_header($last_patch_line, $patch_line,
1436 $diffinfo, \%from, \%to,
1437 @hash_parents);
1439 # the patch itself
1440 LINE:
1441 while ($patch_line = to_utf8(scalar <$fd>)) {
1442 chomp $patch_line;
1444 next PATCH if ($patch_line =~ m/^diff /);
1446 my $class = diff_line_class($patch_line, \%from, \%to);
1448 if ($class eq 'chunk_header') {
1449 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
1450 @chunk = ();
1453 push @chunk, [ $class, $patch_line ];
1456 } continue {
1457 if (@chunk) {
1458 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
1459 @chunk = ();
1461 print "</div>\n"; # class="patch"
1464 # for compact combined (--cc) format, with chunk and patch simplification
1465 # the patchset might be empty, but there might be unprocessed raw lines
1466 for (++$patch_idx if $patch_number > 0;
1467 $patch_idx < @$difftree;
1468 ++$patch_idx) {
1469 # read and prepare patch information
1470 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1472 # generate anchor for "patch" links in difftree / whatchanged part
1473 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1474 format_diff_cc_simplified($diffinfo, @hash_parents) .
1475 "</div>\n"; # class="patch"
1477 $patch_number++;
1480 if ($patch_number == 0) {
1481 if (@hash_parents > 1) {
1482 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
1483 } else {
1484 print "<div class=\"diff nodifferences\">No differences found</div>\n";
1488 print "</div>\n"; # class="patchset"
1492 ## END gitweb.cgi source
1495 sub git_header_html {
1496 my $status = shift || "200 OK";
1497 my $expires = shift;
1499 print $cgi->header(-type=>'text/html', -charset => 'utf-8', -status=> $status, -expires => $expires);
1500 print <<EOF;
1501 <!DOCTYPE html>
1502 <html xmlns="http://www.w3.org/1999/xhtml">
1503 <head>
1504 <meta charset="utf-8" />
1505 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
1506 <title>git diff</title>
1507 </head>
1508 <body>
1512 sub git_footer_html {
1513 print "</body>\n" .
1514 "</html>";
1517 sub die_error {
1518 my $status = shift || "403 Forbidden";
1519 my $error = shift || "Malformed query, file missing or permission denied";
1521 git_header_html($status);
1522 print "<div class=\"page_body\">\n" .
1523 "<br /><br />\n" .
1524 "$status - $error\n" .
1525 "<br />\n" .
1526 "</div>\n";
1527 git_footer_html();
1528 exit;
1531 sub git_commitdiff {
1532 my ($id1, $id2)=@_;
1533 local $hash_parent = $id1;
1534 local $hash = $id2;
1535 defined(my $fd = git_cmd_pipe 'diff-tree', '-r', @git::inner::diff_opts,
1536 '--no-commit-id', '--patch-with-raw', '--full-index', $id1, $id2, '--') or
1537 die_error(undef, "Open git-diff-tree failed: @{[0+$!]}\n");
1538 my @difftree;
1539 while (my $line = to_utf8(scalar <$fd>)) {
1540 chomp $line;
1541 # empty line ends raw part of diff-tree output
1542 last unless $line;
1543 push @difftree, scalar parse_difftree_raw_line($line);
1545 my $expires = $inner::http_expires;
1546 if ((!defined($expires) || $expires eq "") &&
1547 $id1 =~ m/^[0-9a-fA-F]{40,}$/ && $id2 =~ m/^[0-9a-fA-F]{40,}$/) {
1548 $expires = "+1d";
1551 git_header_html(undef, $expires);
1552 print "<div class=\"page_body\">\n";
1553 git_difftree_body(\@difftree, $hash, $hash_parent);
1554 #print "<br />\n";
1555 git_patchset_body($fd, 'inline', \@difftree, $hash, $hash_parent);
1556 print "</div>";
1557 git_footer_html();
1560 package inner;
1562 # Set the global doconfig setting in the GITBROWSER_CONFIG file to the full
1563 # path to a perl source file to run to alter these settings
1565 # If $check_path is set to a subroutine reference, it will be called
1566 # by get_repo_path with two arguments, the name of the repo and its
1567 # path which will be undef if it's not a known repo. If the function
1568 # returns false, access to the repo will be denied.
1569 # $check_path = sub { my ($name, $path) = @_; $name ~! /restricted/i; };
1570 use vars qw($check_path);
1572 use Cwd qw(abs_path);
1573 use File::Basename qw(dirname);
1574 use File::Spec::Functions qw(file_name_is_absolute catdir);
1576 sub read_config
1578 my $f;
1579 my $GITBROWSER_CONFIG = $ENV{'GITBROWSER_CONFIG'} || "git-browser.conf";
1580 -e $GITBROWSER_CONFIG or $GITBROWSER_CONFIG = "/etc/git-browser.conf";
1582 open $f, '<', $GITBROWSER_CONFIG or return;
1583 my $confdir = dirname(abs_path($GITBROWSER_CONFIG));
1584 my $section="";
1585 my $configfile="";
1586 while( <$f> ) {
1587 chomp;
1588 $_=~ s/\r$//;
1589 if( $section eq "repos" ) {
1590 if( m/^\w+:\s*/ ) {
1591 $section="";
1592 redo;
1593 }else {
1594 my ($name,$path)=split;
1595 if( $name && $path ) {
1596 file_name_is_absolute($path) or
1597 $path = catdir($confdir, $path);
1598 $inner::known_repos{$name}=$path;
1601 }else {
1602 if( m/^gitbin:\s*/ ) {
1603 $git::inner::gitbin=$';
1604 }elsif( m/^gitweb:\s*/ ) {
1605 $git::inner::gitweb=$';
1606 }elsif( m/^path:\s*/ ) {
1607 $ENV{PATH}=$';
1608 }elsif( m/^http_expires:\s*/ ) {
1609 $inner::http_expires=$';
1610 }elsif( m/^warehouse:\s*/ ) {
1611 my $path = $';
1612 file_name_is_absolute($path) or
1613 $path = catdir($confdir, $path);
1614 $inner::warehouse=$path;
1615 }elsif( m/^doconfig:\s*/ ) {
1616 $configfile=$';
1617 }elsif( m/^repos:\s*/ ) {
1618 $section="repos";
1622 if ($configfile && -e $configfile) {
1623 do $configfile;
1624 die $@ if $@;
1626 $git::inner::gitweb .= "/" unless substr($git::inner::gitweb, -1, 1) eq "/";
1629 package main;
1631 use File::Spec::Functions qw(catdir);
1633 sub get_repo_path
1635 my ($name) = @_;
1636 my $path = $inner::known_repos{$name};
1637 return undef
1638 if ref($inner::check_path) eq 'CODE' && !&{$inner::check_path}($name, $path);
1639 if (not defined $path and $inner::warehouse and -d catdir($inner::warehouse, $name)) {
1640 $path = catdir($inner::warehouse, $name);
1642 return $path;
1645 sub validate_input {
1646 my $input = shift;
1648 if ($input =~ m/^[0-9a-fA-F]{40,}$/) {
1649 return $input;
1651 if ($input =~ m/(?:^|\/)(?:|\.|\.\.)(?:$|\/)/) {
1652 return undef;
1654 if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\*\~\%\,\x21-\x7e]/) {
1655 return undef;
1657 return $input;
1660 inner::read_config();
1662 my $repo=$cgi->param( "repo" );
1663 my $id1=$cgi->param( "id1" );
1664 my $id2=$cgi->param( "id2" );
1666 git::inner::die_error( "403 Forbidden", "malformed value for repo param" )
1667 unless defined( validate_input( $repo ) ) && get_repo_path( $repo );
1668 git::inner::die_error( "403 Forbidden", "malformed value for id1 param" ) unless defined validate_input( $id1 );
1669 git::inner::die_error( "403 Forbidden", "malformed value for id2 param" ) unless defined validate_input( $id2 );
1672 my $encrepo = $repo;
1674 use bytes;
1675 $encrepo =~ s/([\x00-\x1F\x7F-\xFF <>"#%{}|\\^`?])/sprintf("%%%02X",ord($1))/gse;
1677 local $git::inner::gitdir = get_repo_path($repo);
1678 local $git::inner::cgi = $cgi;
1679 local $git::inner::urlbase = $git::inner::gitweb . $encrepo;
1680 local $git::inner::action = 'commitdiff';
1682 git::inner::git_commitdiff( $id1, $id2 );