3 # (C) 2005, Artem Khodush <greenkaa@gmail.com>
4 # (C) 2020, Kyle J. McKay <mackyle@gmail.com>
7 # This program contains parts from gitweb.cgi,
8 # (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
9 # (C) 2005, Christian Gierke <ch@gierke.de>
11 # This program is licensed under the GPL v2, or a later version
24 # location of the git-core binaries
25 $git::inner::gitbin="git";
27 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
29 open(NULL, '>', File::Spec->devnull) or die "Cannot open devnull: $!\n";
30 open(SAVEERR, ">&STDERR") || die "couldn't dup STDERR: $!\n";
31 open(STDERR, ">&NULL") || die "couldn't dup NULL to STDERR: $!\n";
32 my $result = open(my $fd, "-|", @_);
33 open(STDERR, ">&SAVEERR") || die "couldn't dup SAVERR to STDERR: $!\n";
34 close(SAVEERR) or die "couldn't close SAVEERR: $!\n";
35 close(NULL) or die "couldn't close NULL: $!\n";
36 return $result ? $fd : undef;
39 # opens a "|-|" cmd pipe with 2>/dev/null and returns ($pid,$wfd,$rdf)
41 open(NULL, '>', File::Spec->devnull) or die "Cannot open devnull: $!\n";
42 open(SAVEERR, ">&STDERR") || die "couldn't dup STDERR: $!\n";
43 open(STDERR, ">&NULL") || die "couldn't dup NULL to STDERR: $!\n";
44 my ($pid, $rfd, $wfd);
46 $pid = open2($rfd, $wfd, @_);
48 open(STDERR, ">&SAVEERR") || die "couldn't dup SAVERR to STDERR: $!\n";
49 close(SAVEERR) or die "couldn't close SAVEERR: $!\n";
50 close(NULL) or die "couldn't close NULL: $!\n";
51 return $pid ? ($pid, $wfd, $rfd) : ();
54 # closes a cmd_bidi_pipe if passed the return array from cmd_bidi_pipe
56 my ($pid, $wfd, $rfd) = @_;
57 defined($wfd) and close($wfd);
58 defined($rfd) and close($rfd);
59 $pid and waitpid($pid, 0);
62 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
63 # returns undef and sets a non-zero $! if the pipe is empty
65 my $p = cmd_pipe $git::inner::gitbin, "--git-dir=".$gitdir, @_;
66 defined($p) or return undef;
70 $e = ($e >= 256) ? ($e > 32768 ? 128 : $e >> 8) : ($e & 0x7f) + 128;
76 # opens a "-|" git_cmd pipe and returns the entire result as one chomp'd string
77 # or undef if there was no result or an error
79 my $p = git_cmd_pipe @_;
80 defined($p) or return undef;
87 defined($result) and chomp($result);
91 my $fallback_encoding;
93 $fallback_encoding = Encode::find_encoding('Windows-1252');
94 $fallback_encoding = Encode::find_encoding('ISO-8859-1')
95 unless $fallback_encoding;
98 # decode sequences of octets in utf8 into Perl's internal form,
99 # which is utf-8 with utf8 flag set if needed. git-browser writes out
100 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
102 my $str = shift || '';
103 if (Encode::is_utf8($str) || utf8::decode($str)) {
106 return $fallback_encoding->decode($str, Encode::FB_DEFAULT);
110 sub git_master_branch
112 my $head_id = git_cmd_value 'rev-parse', '--verify', 'HEAD', '--';
113 defined($head_id) && $head_id =~ /^[0-9a-f]{40,}$/ or return undef;
114 my $head_sym = git_cmd_value 'symbolic-ref', '-q', 'HEAD';
115 defined($head_sym) and $head_sym = to_utf8($head_sym);
116 return "HEAD" unless defined($head_sym) && $head_sym =~ m{^refs/heads/(.+)$};
124 my $MAX_COUNT= $arg->{shortcomment} ? 400 : 200;
125 my @command=('rev-list', '--stdin', '--header', '--parents', '--date-order', "--max-count=$MAX_COUNT", '--');
126 my @input=(@{$arg->{id}}, map("^$_", @{$arg->{x}}));
128 push(@input, @{$arg->{path}}) if @{$arg->{path}};
133 my ($pid, $wfd, $rfd) = cmd_bidi_pipe($git::inner::gitbin, "--git-dir=".$gitdir, @command);
134 $pid or die "git_read_commits: error running git rev-list: @{[0+$!]}\n";
138 local $SIG{'PIPE'} = sub {};
139 print $wfd map("$_\n", @input)
140 or cmd_bidi_close($pid, $wfd, $rfd), die "git_read_commits: git rev-list --stdin failed: $!\n";
142 close($wfd); $wfd = undef;
143 while( my $commit_line=<$rfd> ) {
144 $commit_line =~ s/\r$//;
145 my @commit_lines = ();
146 foreach (split '\n', $commit_line) {
147 push @commit_lines, to_utf8($_);
152 my $header = shift @commit_lines;
153 if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
156 ($co{'id'}, my @parents) = split ' ', $header;
157 $co{'parents'} = \@parents;
158 while (my $line = shift @commit_lines) {
159 last if $line eq "\n";
160 # minimize http traffic - do not read not used things
161 # if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
164 if ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
166 $co{'author_epoch'} = $2;
167 # $co{'author_tz'} = $3;
168 }elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
169 # $co{'committer'} = $1;
170 $co{'committer_epoch'} = $2;
171 # $co{'committer_tz'} = $3;
174 # if (!defined $co{'tree'}) {
178 # remove added spaces
179 foreach my $line (@commit_lines) {
182 if( $arg->{shortcomment} ) {
183 $co{'comment'} = [$commit_lines[0]];
185 $co{'comment'} = \@commit_lines;
188 $commits{$co{'id'}}=\%co;
190 cmd_bidi_close($pid, $wfd, $rfd);
197 defined(my $fd = git_cmd_pipe 'show-ref', '--head', '--dereference') or
198 die "get_ref_ids: error running git show-ref: @{[0+$!]}\n";
202 while( my $line=<$fd> ) {
204 my ($id,$name)=split /[ \t]+/, to_utf8($line);
205 if( $name=~s/^refs\/heads\/// || $name eq "HEAD" ) {
206 push @refs, { type=>"h", id=>$id, name=>$name };
207 }elsif( $name=~s/^refs\/tags\/// ) {
209 if( $name=~m/\^\{\w*\}$/ ) { # it's dereferenced
213 # if several ids for a name is seen, we are interested only in the last dereferenced one
214 ++$tagcnt, $names{$name}={} unless exists $names{$name};
215 $names{$name}->{$deref}=$id;
216 push @refs, { type=>"t", id=>$id, name=>$name };
223 my ($pid, $wfd, $rfd);
224 local $SIG{'PIPE'} = sub {};
226 ($pid, $wfd, $rfd) = cmd_bidi_pipe($git::inner::gitbin, "--git-dir=".$gitdir, 'cat-file', '--batch-check');
227 $pid or die "get_ref_ids: unable to run git cat-file --batch-check\n";
229 my $git_get_type = sub {
230 return "" unless $tagcnt;
232 defined($id) && $id ne "" or return "";
233 print $wfd $id, "\n" or cmd_bidi_close($pid, $wfd, $rfd), die "get_ref_ids: git cat-file --batch-check failed: $!\n";
235 defined($bcl) or return "";
236 my @bcl = split(' ', $bcl);
237 @bcl >= 2 or return "";
241 for my $ref (@refs) {
242 if( $ref->{type} eq "h" ) {
243 # assume all heads are commits
246 my $id_kind=$names{$ref->{name}};
247 # so. if several ids for a name is seen, keep only in the last dereferenced one
248 if( $ref->{id} eq $id_kind->{1} || ($ref->{id} eq $id_kind->{0} && !exists $id_kind->{1}) ) {
249 # and only if it's a commit
250 push @result, $ref if &$git_get_type( $ref->{id} ) eq "commit";
254 cmd_bidi_close($pid, $wfd, $rfd) if $tagcnt;
262 my $refs=git::inner::get_ref_ids;
263 my $result={ tags=>[], heads=>[] };
264 for my $ref (@$refs) {
265 push @{$result->{tags}}, $ref->{name} if $ref->{type} eq "t";
266 push @{$result->{heads}}, $ref->{name} if $ref->{type} eq "h";
271 sub commits_from_refs
274 # can't just do git_read_commits. mapping from ref names to ids must also be returned for labels to work.
275 my $refs=git::inner::get_ref_ids;
277 for (@{$arg->{ref}}) {
278 my ($type,$name)=split ",";
279 $name = git::inner::to_utf8($name);
281 push @start_ids, $_->{id} for (grep( $_->{type} =~ /^[ht]$/, @$refs )); # all heads & tags
283 push @start_ids, $_->{id} for (grep( $name eq $_->{name} && $type eq $_->{type}, @$refs ));
286 my $master=git::inner::git_master_branch;
287 my $ans = { refs=>$refs, commits=>commits_from_ids( { id=>\@start_ids, x=>$arg->{x}, path=>$arg->{path}, shortcomment=>$arg->{shortcomment} } ) };
288 defined($master) && $master ne "" and $ans->{master} = $master;
295 return git::inner::git_read_commits( $arg );
300 # Set the global doconfig setting in the GITBROWSER_CONFIG file to the full
301 # path to a perl source file to run to alter these settings
303 # If $check_path is set to a subroutine reference, it will be called
304 # by get_repo_path with two arguments, the name of the repo and its
305 # path which will be undef if it's not a known repo. If the function
306 # returns false, access to the repo will be denied.
307 # $check_path = sub { my ($name, $path) = @_; $name ~! /restricted/i; };
308 use vars qw($check_path);
310 use Cwd qw(abs_path);
311 use File
::Basename
qw(dirname);
312 use File
::Spec
::Functions
qw(file_name_is_absolute catdir);
317 my $GITBROWSER_CONFIG = $ENV{'GITBROWSER_CONFIG'} || "git-browser.conf";
318 -e
$GITBROWSER_CONFIG or $GITBROWSER_CONFIG = "/etc/git-browser.conf";
320 open $f, '<', $GITBROWSER_CONFIG or return;
321 my $confdir = dirname
(abs_path
($GITBROWSER_CONFIG));
327 if( $section eq "repos" ) {
332 my ($name,$path)=split;
333 if( $name && $path ) {
334 file_name_is_absolute
($path) or
335 $path = catdir
($confdir, $path);
336 $inner::known_repos
{$name}=$path;
340 if( m/^gitbin:\s*/ ) {
341 $git::inner
::gitbin
=$';
342 }elsif( m/^path:\s*/ ) {
344 }elsif( m/^http_expires:\s*/ ) {
345 $inner::http_expires
=$';
346 }elsif( m/^warehouse:\s*/ ) {
348 file_name_is_absolute
($path) or
349 $path = catdir
($confdir, $path);
350 $inner::warehouse
=$path;
351 }elsif( m/^doconfig:\s*/ ) {
353 }elsif( m/^repos:\s*/ ) {
358 if ($configfile && -e $configfile) {
366 use Cwd qw(abs_path);
367 use File
::Basename
qw(dirname);
368 use File
::Spec
::Functions
qw(catdir);
369 use CGI
qw(:standard :escapeHTML -nosticky);
370 use CGI
::Util
qw(unescape);
371 use CGI
::Carp
qw(fatalsToBrowser);
373 eval 'sub CGI::multi_param {CGI::param(@_)}'
374 unless CGI
->can("multi_param");
376 BEGIN {dirname
(abs_path
($0)) =~ m{^(.+)$}os and eval 'use lib $1'}
379 binmode STDOUT
, ':utf8';
384 my $path = $inner::known_repos
{$name};
386 if ref($inner::check_path
) eq 'CODE' && !&{$inner::check_path
}($name, $path);
387 if (not defined $path and $inner::warehouse
and -d catdir
($inner::warehouse
, $name)) {
388 $path = catdir
($inner::warehouse
, $name);
395 my @a=sort({lc($a) cmp lc($b) || $a cmp $b} keys %inner::known_repos
);
401 if ($input =~ m/^[0-9a-fA-F]{40,}$/) {
404 if ($input =~ m/(?:^|\/)(?
:|\
.|\
.\
.)(?
:$|\
/)/) {
407 if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\
-\
+\
*\
~\
%\
,\x21-\x7e]/) {
413 inner
::read_config
();
415 my $converter=JSON
::Converter
->new;
416 my $request=CGI
::new
();
425 my @names=$request->multi_param;
426 for my $pn (@names) {
427 if( $pn eq "repo" ) {
428 $repo=$request->param( "repo" );
429 }elsif( $pn eq "sub" ) {
430 $sub=$request->param( "sub" );
432 my @v=$request->multi_param( $pn );
434 $error=$converter->valueToJson( "invalid cgi param value for '$pn': '$v'\n" ) unless defined validate_input
( $v );
440 if( $error eq "null" ) {
441 if( !defined( $sub ) ) {
442 $error=$converter->valueToJson( "git-browser.cgi: 'sub' cgi parameter is omitted" );
443 }elsif( exists $main::{$sub} ) {
445 $result=&{$main::{$sub}}( $arg );
448 $error=$converter->valueToJson( "error in main::$sub: $@" );
450 $result=$converter->objToJson( $result );
452 }elsif( exists $git::{$sub} ) {
453 if( !defined( $repo ) ) {
454 $error=$converter->valueToJson( "git-browser.cgi: 'repo' cgi parameter is omitted" );
455 }elsif( !get_repo_path
($repo) ) {
456 $error=$converter->valueToJson( "git-browser.cgi: unknown repository name specified: $repo" );
458 local $git::inner
::gitdir
=get_repo_path
($repo);
460 $result=&{$git::{$sub}}( $arg );
463 $error=$converter->valueToJson( "error in git::$sub: $@" );
465 $result=$converter->objToJson( $result );
469 $error=$converter->valueToJson( "git-browser.cgi: no procedure '$sub' in either git or main package" );
473 print $request->header(-type
=>'text/html', -charset
=> 'utf-8', -status
=> "200 OK", -expires
=> $inner::http_expires
);
476 <html xmlns="http://www.w3.org/1999/xhtml">
478 <meta charset="utf-8" />
479 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
480 <script type="text/javascript">
482 document.error=$error;
483 document.result=$result;