3 # (C) 2005, Artem Khodush <greenkaa@gmail.com>
4 # (C) 2020, Kyle J. McKay <mackyle@gmail.com>
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
16 use CGI
qw(:standard :escapeHTML -nosticky);
17 use CGI
::Util
qw(unescape);
18 #use CGI::Carp qw(fatalsToBrowser);
20 binmode STDOUT
, ':utf8';
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
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).
65 my $p = cmd_pipe $git::inner::gitbin, "--git-dir=".$gitdir, @_;
66 defined($p) or return undef;
70 $e = ($e >= 256) ? ($e > 32768 ? 255 : $e >> 8) : ($e & 0x7f) + 128 if $e;
71 $e and $! = $e, return undef;
73 return open($p, '<', File::Spec->devnull) ? $p : undef;
76 my $fallback_encoding;
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
87 my $str = shift || '';
88 if (Encode::is_utf8($str) || utf8::decode($str)) {
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:
100 # 100644 - plain file
101 # 100755 - executable file
102 # 120000 - symbolic link
106 defined($m) or return "";
107 $m =~ /^[0-7]+$/os or return $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";
118 # always correct version of $cgi->a({-href = href("-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
140 hash_parent => '3hp',
141 hash_parent_base => '4hpb',
147 if (exists($opts{"-anchor"})) {
148 return '#'.esc_param($opts{"-anchor"});
151 foreach my $k (keys %opts) {
152 if (exists($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
169 return undef unless defined $str;
170 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
175 # replace invalid utf8 character with SUBSTITUTION sequence
180 return undef unless defined $str;
182 $str = to_utf8($str);
183 $str = $cgi->escapeHTML($str);
184 if ($opts{'-nbsp'}) {
185 $str =~ s/ / /g;
188 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
192 # quote control characters and escape filename to HTML
197 return undef unless defined $str;
199 $str = to_utf8($str);
200 $str = $cgi->escapeHTML($str);
201 if ($opts{'-nbsp'}) {
202 $str =~ s/ / /g;
205 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
209 # Make control characters "printable", using character escape codes (CEC)
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})
226 : sprintf('\x%02x', ord($cntrl)) );
227 if ($opts{-nohtml}) {
230 return "<span class=\"cntrl\">$chr</span>";
234 # git may return quoted and escaped filenames
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
258 # quoted ordinary character
262 if ($str =~ m/^"(.*)"$/) {
265 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
270 # escape tabs (convert tabs to spaces)
271 # improved faster version from Markdown.pl
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;
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;
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),
303 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
304 if ($begin - $pos > 0);
305 $out .= $cgi->span({-class => $css_class}, $escaped);
309 $out .= esc_html(substr($str, $pos), %opts)
310 if ($pos < length($str));
315 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
316 sub format_git_diff_header_line {
318 my $diffinfo = shift;
319 my ($from, $to) = @_;
321 if ($diffinfo->{'nparents'}) {
323 $line =~ s!^(diff (.*?) )"?.*$!$1!;
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'});
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'});
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 {
354 my $diffinfo = shift;
355 my ($from, $to) = @_;
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"> (' .
373 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
374 # can match only for combined diff
376 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
377 if ($from->{'href'}[$i]) {
378 $line .= $cgi->a({-href=>$from->{'href'}[$i],
380 substr($diffinfo->{'from_id'}[$i],0,7));
385 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
389 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
390 substr($diffinfo->{'to_id'},0,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));
402 $from_link = '0' x 7;
405 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
406 substr($diffinfo->{'to_id'},0,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) = @_;
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'}) {
431 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
432 esc_path($from->{'file'}));
435 esc_path($from->{'file'});
438 $result .= qq!<div class="diff from_file">$line</div>\n!;
441 # combined diff (merge commit)
442 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
443 if ($from->{'href'}[$i]) {
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'},
451 file_name=>$to->{'file'}),
453 -title=>"diff" . ($i+1)},
456 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
457 esc_path($from->{'file'}[$i]));
459 $line = '--- /dev/null';
461 $result .= qq!<div class="diff from_file">$line</div>\n!;
466 #assert($line =~ m/^\+\+\+/) if DEBUG;
467 # no extra formatting for "^+++ /dev/null"
468 if ($line =~ m!^\+\+\+ "?b/!) {
471 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
472 esc_path($to->{'file'}));
475 esc_path($to->{'file'});
478 $result .= qq!<div class="diff to_file">$line</div>\n!;
483 # create note for patch simplified by combined diff
484 sub format_diff_cc_simplified {
485 my ($diffinfo, @parents) = @_;
488 $result .= "<div class=\"diff header\">" .
490 if (!is_deleted($diffinfo)) {
491 $result .= $cgi->a({-href => href(action=>"blob",
493 hash=>$diffinfo->{'to_id'},
494 file_name=>$diffinfo->{'to_file'}),
496 esc_path($diffinfo->{'to_file'}));
498 $result .= esc_path($diffinfo->{'to_file'});
500 $result .= "</div>\n" . # class="diff header"
501 "<div class=\"diff nodifferences\">" .
503 "</div>\n"; # class="diff nodifferences"
508 sub diff_line_class {
509 my ($line, $from, $to) = @_;
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'});
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);
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>";
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]);
585 $line .= $from_text[$i];
590 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
591 -class=>"list"}, $to_text);
595 $line .= " $prefix</span>" .
596 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
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
604 sub format_diff_line {
605 my ($line, $diff_class, $from, $to) = @_;
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);
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";
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;
637 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
639 while (my $line = to_utf8(scalar <$fd>)) {
642 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
643 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
644 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
653 # parse line of git-diff-tree "raw" output
654 sub parse_difftree_raw_line {
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;
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);
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})$/) {
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)
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'}) {
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]);
726 $from->{'href'}[$i] = undef;
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'});
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'});
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]);
768 # is current raw difftree line of file deletion
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)
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");
796 print "<table class=\"" .
797 (@parents > 1 ? "combined " : "") .
800 # header only for combined diff in 'commitdiff' view
801 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
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];
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)},
816 print "</tr></thead>\n<tbody>\n";
821 foreach my $line (@{$difftree}) {
822 my $diff = parsed_difftree_line($line);
825 print "<tr class=\"dark\">\n";
827 print "<tr class=\"light\">\n";
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
839 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
840 file_name=>$diff->{'to_file'},
842 -class => "list"}, esc_path($diff->{'to_file'})) .
846 esc_path($diff->{'to_file'}) .
850 if ($action eq 'commitdiff') {
853 print "<td class=\"link\">" .
854 a_anchor({-href => href(-anchor=>"patch$patchno")},
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",
878 file_name=>$from_path)},
882 if ($diff->{'to_id'} eq $from_hash) {
883 print "<td class=\"link nochange\">";
885 print "<td class=\"link\">";
887 print $cgi->a({-href => href(action=>"blobdiff",
888 hash=>$diff->{'to_id'},
889 hash_parent=>$from_hash,
891 hash_parent_base=>$hash_parent,
892 file_name=>$diff->{'to_file'},
893 file_parent=>$from_path)},
899 print "<td class=\"link\">";
901 print $cgi->a({-href => href(action=>"blob",
902 hash=>$diff->{'to_id'},
903 file_name=>$diff->{'to_file'},
906 print " | " if ($has_history);
909 print $cgi->a({-href => href(action=>"history",
910 file_name=>$diff->{'to_file'},
917 next; # instead of 'else' clause, to avoid extra indent
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>";
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'}));
947 print "<td>$mode_chng</td>\n";
948 print "<td class=\"link\">";
949 if ($action eq 'commitdiff') {
952 print a_anchor({-href => href(-anchor=>"patch$patchno")},
956 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
957 hash_base=>$hash, file_name=>$diff->{'file'})},
961 } elsif ($diff->{'status'} eq "D") { # deleted
962 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
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'}));
968 print "<td>$mode_chng</td>\n";
969 print "<td class=\"link\">";
970 if ($action eq 'commitdiff') {
973 print a_anchor({-href => href(-anchor=>"patch$patchno")},
977 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
978 hash_base=>$parent, file_name=>$diff->{'file'})},
981 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$parent,
982 file_name=>$diff->{'file'}),
983 -class => "blamelink"},
986 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
987 file_name=>$diff->{'file'})},
991 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
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";
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'}));
1012 print "<td>$mode_chnge</td>\n";
1013 print "<td class=\"link\">";
1014 if ($action eq 'commitdiff') {
1017 print a_anchor({-href => href(-anchor=>"patch$patchno")},
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'})},
1029 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
1030 hash_base=>$hash, file_name=>$diff->{'file'})},
1033 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$hash,
1034 file_name=>$diff->{'file'}),
1035 -class => "blamelink"},
1038 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
1039 file_name=>$diff->{'file'})},
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'}};
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);
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') {
1064 print a_anchor({-href => href(-anchor=>"patch$patchno")},
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'})},
1076 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
1077 hash_base=>$parent, file_name=>$diff->{'to_file'})},
1080 print $cgi->a({-href => href(action=>$git::inner::blame_action, hash_base=>$hash,
1081 file_name=>$diff->{'to_file'}),
1082 -class => "blamelink"},
1085 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
1086 file_name=>$diff->{'to_file'})},
1090 } # we should not encounter Unmerged (U) or Unknown (X) status
1093 print "</tbody>" if $has_header;
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
1104 '<div class="chunk_block ctx">',
1105 '<div class="old">',
1108 '<div class="new">',
1117 '<div class="chunk_block rem">',
1118 '<div class="old">',
1125 '<div class="chunk_block add">',
1126 '<div class="new">',
1132 '<div class="chunk_block chg">',
1133 '<div class="old">',
1136 '<div class="new">',
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.
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/);
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/);
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);
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) {
1213 # Highlight lines in combined diff only if the chunk contains
1214 # diff between the same version, e.g.
1221 # Otherwise the highlightling would be confusing.
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) {
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;
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,
1262 if ($diff_style eq 'sidebyside' && !$is_combined) {
1263 print_sidebyside_diff_lines($ctx, $rem, $add);
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];
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);
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
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
1313 # rem, add or change
1314 if ($class eq 'rem') {
1316 } elsif ($class eq 'add') {
1320 if ($class eq 'ctx') {
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);
1334 my $patch_number = 0;
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>)) {
1347 last if ($patch_line =~ m/^diff /);
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);
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";
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;
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"
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";
1402 #assert($patch_line =~ m/^diff /) if DEBUG;
1403 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
1405 # print "git diff" header
1406 print format_git_diff_header_line($patch_line, $diffinfo,
1409 # print extended diff header
1410 print "<div class=\"diff extended_header\">\n";
1412 while ($patch_line = to_utf8(scalar<$fd>)) {
1415 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
1417 print format_extended_diff_header_line($patch_line, $diffinfo,
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"
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>);
1433 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
1435 print format_diff_from_to_header($last_patch_line, $patch_line,
1436 $diffinfo, \%from, \%to,
1441 while ($patch_line = to_utf8(scalar <$fd>)) {
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);
1453 push @chunk, [ $class, $patch_line ];
1458 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @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;
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"
1480 if ($patch_number == 0) {
1481 if (@hash_parents > 1) {
1482 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
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);
1502 <html xmlns="http://www.w3.org/1999/xhtml">
1504 <meta charset="utf-8" />
1505 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
1506 <title>git diff</title>
1512 sub git_footer_html
{
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" .
1524 "$status - $error\n" .
1531 sub git_commitdiff
{
1533 local $hash_parent = $id1;
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");
1539 while (my $line = to_utf8
(scalar <$fd>)) {
1541 # empty line ends raw part of diff-tree output
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,}$/) {
1551 git_header_html
(undef, $expires);
1552 print "<div class=\"page_body\">\n";
1553 git_difftree_body
(\
@difftree, $hash, $hash_parent);
1555 git_patchset_body
($fd, 'inline', \
@difftree, $hash, $hash_parent);
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);
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));
1589 if( $section eq "repos" ) {
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;
1602 if( m/^gitbin:\s*/ ) {
1603 $git::inner
::gitbin
=$';
1604 }elsif( m/^gitweb:\s*/ ) {
1605 $git::inner::gitweb=$';
1606 }elsif( m/^path:\s*/ ) {
1608 }elsif( m/^http_expires:\s*/ ) {
1609 $inner::http_expires=$';
1610 }elsif( m/^warehouse:\s*/ ) {
1612 file_name_is_absolute($path) or
1613 $path = catdir($confdir, $path);
1614 $inner::warehouse=$path;
1615 }elsif( m/^doconfig:\s*/ ) {
1617 }elsif( m/^repos:\s*/ ) {
1622 if ($configfile && -e
$configfile) {
1626 $git::inner
::gitweb
.= "/" unless substr($git::inner
::gitweb
, -1, 1) eq "/";
1631 use File
::Spec
::Functions
qw(catdir);
1636 my $path = $inner::known_repos
{$name};
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);
1645 sub validate_input
{
1648 if ($input =~ m/^[0-9a-fA-F]{40,}$/) {
1651 if ($input =~ m/(?:^|\/)(?
:|\
.|\
.\
.)(?
:$|\
/)/) {
1654 if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\
-\
+\
*\
~\
%\
,\x21-\x7e]/) {
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;
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 );