git-browser.cgi: fix handling of latin-1 characters
[git-browser-mirror.git] / git-diff.cgi
blobfb507f9d95d57044043f35e9eff1fbf59444d83a
1 #! /usr/bin/perl
3 # Originally from gitweb.cgi,
4 # (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
5 # (C) 2005, Christian Gierke <ch@gierke.de>
7 # This program is licensed under the GPL v2, or a later version
9 use strict;
10 use warnings;
11 use CGI qw(:standard :escapeHTML -nosticky);
12 use CGI::Util qw(unescape);
13 use CGI::Carp qw(fatalsToBrowser);
14 BEGIN {
15 if( $^V ge v5.8.0 ) {
16 require Encode; import Encode;
17 require Fcntl; import Fcntl ':mode';
18 }else {
19 no strict "refs";
20 *{"Encode::FB_DEFAULT"}=sub { 1; };
21 *{"Encode::decode"}=sub { my ($a,$s,$b)=@_; return $s; };
25 if( $^V ge v5.8.0 ) {
26 binmode STDOUT, ':utf8';
29 my $request=CGI::new();
31 package git::inner;
33 # location of the git-core binaries
34 $git::inner::gitbin="git";
35 $git::inner::git_temp="/tmp/gitweb";
37 sub git_commitdiff {
38 my ($id1, $id2)=@_;
39 mkdir($git::inner::git_temp, 0700);
40 open my $fd, "-|", "${git::inner::gitbin}", "diff-tree", '-r', $id1, $id2 or die_error(undef, "Open failed.");
41 my (@difftree) = map { chomp; $_ } <$fd>;
42 close $fd or die_error(undef, "Reading diff-tree failed.");
44 git_header_html(undef, $inner::http_expires);
46 print "<div class=\"page_body\">\n";
47 foreach my $line (@difftree) {
48 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
49 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
50 $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
51 my $from_mode = $1;
52 my $to_mode = $2;
53 my $from_id = $3;
54 my $to_id = $4;
55 my $status = $5;
56 my $file = validate_input(unquote($6));
57 if ($status eq "A") {
58 # print "<div class=\"diff_info\">" . file_type($to_mode) . ":" .
59 # $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" .
60 # "</div>\n";
61 git_diff_print(undef, "/dev/null", $to_id, "b/$file");
62 } elsif ($status eq "D") {
63 # print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
64 # $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) . "(deleted)" .
65 # "</div>\n";
66 git_diff_print($from_id, "a/$file", undef, "/dev/null");
67 } elsif ($status eq "M") {
68 if ($from_id ne $to_id) {
69 # print "<div class=\"diff_info\">" .
70 # file_type($from_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) .
71 # " -> " .
72 # file_type($to_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id);
73 # print "</div>\n";
74 git_diff_print($from_id, "a/$file", $to_id, "b/$file");
78 print "</div>";
79 git_footer_html();
82 sub git_diff_print {
83 my $from = shift;
84 my $from_name = shift;
85 my $to = shift;
86 my $to_name = shift;
87 my $format = shift || "html";
89 my $from_tmp = "/dev/null";
90 my $to_tmp = "/dev/null";
91 my $pid = $$;
93 # create tmp from-file
94 if (defined $from) {
95 $from_tmp = "${git::inner::git_temp}/gitweb_" . $$ . "_from";
96 open my $fd2, "> $from_tmp" or die "error creating $from_tmp: $! $@";
97 open my $fd, "-|", "${git::inner::gitbin}", "cat-file", 'blob', $from;
98 my @file = <$fd>;
99 print $fd2 @file;
100 close $fd2;
101 close $fd;
104 # create tmp to-file
105 if (defined $to) {
106 $to_tmp = "${git::inner::git_temp}/gitweb_" . $$ . "_to";
107 open my $fd2, "> $to_tmp" or die "error creating $from_tmp: $! $@";
108 open my $fd, "-|", "${git::inner::gitbin}", "cat-file", 'blob', $to;
109 my @file = <$fd>;
110 print $fd2 @file;
111 close $fd2;
112 close $fd;
115 open my $fd, "-|", "/usr/bin/diff", '-u', '-p', '-L', $from_name, '-L', $to_name, $from_tmp, $to_tmp;
116 if ($format eq "plain") {
117 undef $/;
118 print <$fd>;
119 $/ = "\n";
120 } else {
121 while (my $line = <$fd>) {
122 chomp($line);
123 my $char = substr($line, 0, 1);
124 my $color = "";
125 if ($char eq '+') {
126 $color = " style=\"color:#008800;\"";
127 } elsif ($char eq "-") {
128 $color = " style=\"color:#cc0000;\"";
129 } elsif ($char eq "@") {
130 $color = " style=\"color:#990099;\"";
131 } elsif ($char eq "\\") {
132 # skip errors
133 next;
135 while ((my $pos = index($line, "\t")) != -1) {
136 if (my $count = (8 - (($pos-1) % 8))) {
137 my $spaces = ' ' x $count;
138 $line =~ s/\t/$spaces/;
141 print "<div class=\"pre\"$color>" . esc_html($line) . "</div>\n";
144 close $fd;
146 if (defined $from) {
147 unlink($from_tmp);
149 if (defined $to) {
150 unlink($to_tmp);
154 sub validate_input {
155 my $input = shift;
157 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
158 return $input;
160 if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
161 return undef;
163 if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) {
164 return undef;
166 return $input;
170 # replace invalid utf8 character with SUBSTITUTION sequence
171 sub esc_html {
172 my $str = shift;
173 if( $^V ge v5.8.0 ) {
174 $str = Encode::decode("utf8", $str, Encode::FB_DEFAULT);
176 $str = CGI::escapeHTML($str);
177 return $str;
180 # git may return quoted and escaped filenames
181 sub unquote {
182 my $str = shift;
183 if ($str =~ m/^"(.*)"$/) {
184 $str = $1;
185 $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
187 return $str;
190 sub git_header_html {
191 my $status = shift || "200 OK";
192 my $expires = shift;
194 print $request->header(-type=>'text/html', -charset => 'utf-8', -status=> $status, -expires => $expires);
195 print <<EOF;
196 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
197 <head>
198 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
199 <title>git diff</title>
200 </head>
201 <body>
205 sub git_footer_html {
206 print "</body>\n" .
207 "</html>";
210 sub die_error {
211 my $status = shift || "403 Forbidden";
212 my $error = shift || "Malformed query, file missing or permission denied";
214 git_header_html($status);
215 print "<div class=\"page_body\">\n" .
216 "<br/><br/>\n" .
217 "$status - $error\n" .
218 "<br/>\n" .
219 "</div>\n";
220 git_footer_html();
221 exit;
224 package inner;
226 sub read_config
228 open my $f, "< git-browser.conf" or return;
229 my $section="";
230 while( <$f> ) {
231 chomp;
232 $_=~ s/\r$//;
233 if( $section eq "repos" ) {
234 if( m/^\w+:\s*/ ) {
235 $section="";
236 redo;
237 }else {
238 my ($name,$path)=split;
239 if( $name && $path ) {
240 $inner::known_repos{$name}=$path;
243 }else {
244 if( m/^gitbin:\s*/ ) {
245 $git::inner::gitbin=$';
246 $ENV{GIT_EXEC_PATH}=$';
247 }elsif( m/^path:\s*/ ) {
248 $ENV{PATH}=$';
249 }elsif( m/^http_expires:\s*/ ) {
250 $inner::http_expires=$';
251 }elsif( m/^git_temp:\s*/ ) {
252 $git::inner::git_temp=$';
253 }elsif( m/^repos:\s*/ ) {
254 $section="repos";
261 package main;
263 sub validate_input {
264 my $input = shift;
266 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
267 return $input;
269 if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
270 return undef;
272 if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\*\~\%\,]/) {
273 return undef;
275 return $input;
278 inner::read_config();
280 my $repo=$request->param( "repo" );
281 my $id1=$request->param( "id1" );
282 my $id2=$request->param( "id2" );
284 git::inner::die_error( "403 Forbidden", "malformed value for repo param" ) unless defined validate_input( $repo );
285 git::inner::die_error( "403 Forbidden", "malformed value for id1 param" ) unless defined validate_input( $id1 );
286 git::inner::die_error( "403 Forbidden", "malformed value for id2 param" ) unless defined validate_input( $id2 );
288 $ENV{'GIT_DIR'}=$inner::known_repos{$repo};
290 git::inner::git_commitdiff( $id1, $id2 );