#!/usr/bin/perl -T
#
# cvsweb - a CGI interface to CVS trees.
#
# Written in their spare time by
#             Bill Fenner           The server on which the CVS tree lives is probably down.  Please try again in a few minutes.');
}
#
# Short-circuit forbidden things.  Note that $fullname should not change
# after this, because the rest of the code assumes this check has already
# been done.
#
fatal('403 Forbidden', 'Access to %s forbidden.', $where)
  if forbidden($fullname);
#
# Handle tarball downloads before any headers are output.
#
if ($input{tarball}) {
  fatal('403 Forbidden', 'Downloading tarballs is prohibited.')
    unless $allow_tar;
  my ($module)  =  ($where =~ m,^/?(.*),);    # untaint
  $module       =~ s,/([^/]*)$,,;
  my ($ext)     =  ($1 =~ /(\.t(?:ar\.)?gz|\.zip)$/);
  my ($basedir) =  ($module =~ m,([^/]+)$,);
  if ($basedir eq '' || $module eq '') {
    fatal('500 Internal Error',
          'You cannot download the top level directory.');
  }
  my $istar = ($ext eq '.tar.gz' || $ext eq '.tgz');
  if ($istar) {
    fatal('500 Internal Error', 'tar command not found.') unless $CMD{tar};
    fatal('500 Internal Error', 'gzip command not found.') unless $CMD{gzip};
  }
  my $iszip = ($ext eq '.zip');
  if ($iszip && !$CMD{zip}) {
    fatal('500 Internal Error', 'zip command not found.');
  }
  if (!$istar && !$iszip) {
    fatal('500 Internal Error', 'Unsupported archive type.');
  }
  my $tmpexportdir;
  eval {
    local $SIG{__DIE__};
    # Don't use the CLEANUP argument to tempdir() here, since we might be under
    # mod_perl (the process runs for a long time), unlink explicitly later.
    $tmpexportdir = tempdir('.cvsweb.XXXXXXXX', TMPDIR => 1);
  };
  if ($@) {
    fatal('500 Internal Error', 'Unable to make temporary directory: %s', $@);
  }
  if (!chdir($tmpexportdir)) {
    fatal('500 Internal Error',
          "Can't cd to temporary directory %s: %s", $tmpexportdir, $!);
  }
  my @fatal;
  my $tag = $input{only_with_tag} || 'HEAD';
  $tag = 'HEAD' if ($tag eq 'MAIN');
  my @cmd =
    ($CMD{cvs}, @cvs_options, '-Qd', $cvsroot, 'export', '-r', $tag,
     '-d', $basedir, $module);
  my $export_err;
  my ($errcode, $err) = runproc(\@cmd, '2>', \$export_err);
  if ($errcode) {
    @fatal =
      ('500 Internal Error',
       'Export failure (exit status %s), output:  \n";
        while ( Current directory: ', clickablePath($where, 0), '';
    if ($cvshistory_url) {
      (my $d = $where) =~ s|^/*(.*?)/*$|$1|;
      print ' - ', history_link($d, '');
    }
    print " Current tag: ", htmlquote($input{only_with_tag}), "%s',
             $ENV{PATH_INFO});
}
if ($ENV{SCRIPT_NAME}) {
  ($scriptname) = ($ENV{SCRIPT_NAME} =~ VALID_PATH)
    or fatal('500 Internal Error',
             'Illegal SCRIPT_NAME in environment: %s',
             $ENV{SCRIPT_NAME});
}
$scriptname    = '' unless defined($scriptname);
$where         =  $pathinfo;
$doCheckout    =  $where =~ s|^/$CheckoutMagic/|/|o;
$where         =~ s|^/||;
$scriptname    =~ s|^/*|/|;
# Let's workaround thttpd's stupidity..
if ($scriptname =~ m|/$|) {
  $pathinfo .= '/';
  my $re = quotemeta $pathinfo;
  $scriptname =~ s/$re$//;
}
# $scriptname : the URI escaped path to this script
# $where      : the path in the CVS repository (without leading /, or only /)
# $scriptwhere: the URI escaped $scriptname + '/' + $where
$scriptname   = uri_escape_path($scriptname);
$scriptwhere  = join('/', $scriptname, uri_escape_path($where));
$where        = '/' if ($where eq '');
# In text-based browsers, it's very annoying to have two links per file;
# skip linking the image for them.
$Browser     = $ENV{HTTP_USER_AGENT} || '';
$is_links    = ($Browser =~ m`^E?Links `);
$is_lynx     = ($Browser =~ m`^Lynx/`i);
$is_w3m      = ($Browser =~ m`^w3m/`i);
$is_msie     = ($Browser =~ m`MSIE`);
$is_mozilla3 = ($Browser =~ m`^Mozilla/[3-9]`);
$is_textbased = ($is_links || $is_lynx || $is_w3m);
$nofilelinks = $is_textbased;
# newer browsers accept gzip content encoding
# and state this in a header
# (netscape did always but didn't state it)
# It has been reported that these
#  braindamaged MS-Internet Exploders claim that they
# accept gzip .. but don't in fact and
# display garbage then :-/
# Turn off gzip if running under mod_perl and no zlib is available,
# piping does not work as expected inside the server.
$maycompress = (
  ((defined($ENV{HTTP_ACCEPT_ENCODING})
    && $ENV{HTTP_ACCEPT_ENCODING} =~ /gzip/)
   || $is_mozilla3)
  && !$is_msie
  && !(defined($ENV{MOD_PERL}) && !HAS_ZLIB)
);
# Parameters that will be sticky in all constructed links/query strings.
@stickyvars =
  qw(cvsroot hideattic ignorecase sortby logsort f only_with_tag ln
     hidecvsroot hidenonreadable);
#
# Load configuration.
#
if (-f $config) {
  do "$config" or config_error($config, $@);
} else {
  fatal("500 Internal Error",
        'Configuration not found.  Set the parameter $config in cvsweb.cgi to your cvsweb.conf configuration file first.');
}
# Try to find a readable dir where we can cd into.  Some abs_path()
# implementations as well as various cvs operations require such a dir to
# work properly.
{
  local $^W = 0;
  for my $dir (tmpdir(), rootdir()) {
    last if (-r $dir && chdir($dir));
  }
}
$CSS = $cssurl ?
  sprintf("\n",
          htmlquote($cssurl)) : '';
# --- input parameters
my %query = ();
if (defined($ENV{QUERY_STRING})) {
  for my $p (split(/[;&]+/, $ENV{QUERY_STRING})) {
    next unless $p;
    $p =~ y/+/ /;
    my ($key, $val) = split(/=/, $p, 2);
    next unless defined($key);
    $val = 1 unless defined($val);
    ($key = uri_unescape($key)) =~ /[[:graph:]]/ or next;
    ($val = uri_unescape($val)) =~ /[[:graph:]]/ or next;
    $query{$key} = $val;
  }
}
undef %input;
my $t;
for my $p (qw(graph hideattic hidecvsroot hidenonreadable ignorecase ln copt
              makeimage options tarball)) {
  $t = $query{$p};
  if (defined($t)) {
    ($input{$p}) = ($t =~ /^([01]|on)$/)
      or fatal('500 Internal Error',
               'Invalid boolean value: %s=%s', $p, $t);
  }
}
for my $p (qw(annotate r1 r2 rev tr1 tr2)) {
  $t = $query{$p};
  if (defined($t)) {
    if (($p eq 'r1' || $p eq 'r2') && $t eq 'text') {
      # Special case for the "Use text field" option in the log view diff form.
      $input{$p} = $t;
      next;
    } elsif (($p eq 'rev' || $p eq 'annotate') && ($t eq '.' || $t eq 'HEAD')){
      # Another special case, allow linking to latest revision using these.
      $input{$p} = '.';
      next;
    }
    my ($rev, $tag) = split(/:/, $t, 2);
    ($input{$p}) = ($rev =~ /^(\d+(?:\.\d+)*)$/)
      or fatal('500 Internal Error',
               'Invalid revision: %s=%s', $p, $t);
    if (defined($tag)) {
      ($tag) = ($tag =~ VALID_TAG1)
        or fatal('500 Internal Error',
                 'Invalid tag/branch name in revision: %s=%s',
                 $p, $t);
      ($tag) = ($tag =~ VALID_TAG2)
        or fatal('500 Internal Error',
                 'Invalid tag/branch name in revision: %s=%s',
                 $p, $t);
      $input{$p} .= ':' . $tag;
    }
  }
}
$t = defined($query{only_with_tag}) ?
  $query{only_with_tag} : $query{only_on_branch}; # Backwards compatibility.
if (defined($t)) {
  ($input{only_with_tag}) = ($t =~ VALID_TAG1)
    or fatal('500 Internal Error',
             'Invalid tag/branch name: %s', $t);
  ($input{only_with_tag}) = ($t =~ VALID_TAG2)
    or fatal('500 Internal Error',
             'Invalid tag/branch name: %s', $t);
}
$t = $query{logsort};
if (defined($t)) {
  ($input{logsort}) = ($t =~ /^(cvs|date|rev)$/)
    or fatal('500 Internal Error',
             'Unsupported log sort key: %s', $t);
}
$t = $query{f};
if (defined($t)) {
  ($input{f}) = ($t =~ /^(([hH]|[ucs]c?)|ext\d*)$/)
    or fatal('500 Internal Error',
             'Unsupported diff format: %s', $t);
}
$t = $query{sortby};
if (defined($t)) {
  ($input{sortby}) = ($t =~ /^(file|date|rev|author|log)$/)
    or fatal('500 Internal Error',
             'Unsupported dir sort key: %s', $t);
}
$t = $query{'content-type'};
if (defined($t)) {
  ($input{'content-type'}) = ($t =~ m|^([-0-9A-Za-z]+/[-0-9A-Za-z\.\+]+)$|)
    or fatal('500 Internal Error',
             'Unsupported content type: %s', $t);
}
$t = $query{cvsroot};
if (defined($t)) {
  ($input{cvsroot}) = ($t =~ /^([[:print:]]+)$/)
    or fatal('500 Internal Error',
             'Invalid symbolic CVS root name: %s', $t);
}
$t = $query{path};
if (defined($t)) {
  ($input{path}) = ($t =~ VALID_PATH)
    or fatal('500 Internal Error',
             'Invalid path: %s', $t);
}
undef($t);
undef(%query);
# --- end input parameters
#
# CVS roots
#
my $rootfound = 0;
for (my $i = 0; $i < scalar(@CVSrepositories); $i += 2) {
  my $key = $CVSrepositories[$i];
  my ($descr, $root) = @{$CVSrepositories[$i+1]};
  $root = canonpath($root);
  unless (-d $root) {
    warn("Root '$root' defined in \@CVSrepositories is not a directory, " .
         'entry ignored');
    next;
  }
  $rootfound ||= 1;
  $cvstreedefault = $key unless defined($cvstreedefault);
  $CVSROOTdescr{$key} = $descr;
  $CVSROOT{$key} = $root;
  push(@CVSROOT, $key);
}
unless ($rootfound) {
  fatal('500 Internal Error',
        'No valid CVS roots found!  See @CVSrepositories in ' .
        'the configuration file (%s).',
        $config);
}
undef $rootfound;
#
# Default CVS root
#
if (!defined($CVSROOT{$cvstreedefault})) {
  fatal("500 Internal Error",
        '$cvstreedefault points to a repository (%s) not ' .
        'defined in @CVSrepositories in your configuration ' .
        'file (%s).',
        $cvstreedefault,
        $config);
}
$DEFAULTVALUE{cvsroot} = $cvstreedefault;
while (my ($key, $defval) = each %DEFAULTVALUE) {
  # Replace not given parameters with defaults.
  next unless (defined($defval) && $defval =~ /\S/ && !defined($input{$key}));
  # Empty checkboxes in forms return nothing, so we define a helper parameter
  # in these forms (copt) which indicates that we just set parameters with a
  # checkbox.
  if ($input{copt}) {
    # 'copt' is set -> the result of empty input checkbox
    # -> set to zero (disable) if default is a boolean (0|1).
    $input{$key} = 0 if ($defval eq '0' || $defval eq '1');
  } else {
    # 'copt' isn't set --> empty input is not the result
    # of empty input checkbox --> set default.
    $input{$key} = $defval;
  }
}
$barequery = "";
my @barequery;
foreach (@stickyvars) {
  # construct a query string with the sticky non default parameters set
  if (defined($input{$_})
      && !(defined($DEFAULTVALUE{$_}) && $input{$_} eq $DEFAULTVALUE{$_}))
  {
    push(@barequery, join('=', uri_escape($_), uri_escape($input{$_})));
  }
}
if ($allow_enscript) {
  push(@DIFFTYPES, qw(uc cc sc));
  @DIFFTYPES{qw(uc cc sc)} = (
    {
     'descr'   => 'unified, colored',
     'opts'    => ['-u'],
     'colored' => 0,
    },
    {
     'descr'   => 'context, colored',
     'opts'    => ['-c'],
     'colored' => 0,
    },
    {
     'descr'   => 'side by side, colored',
     # width=168 should be enough to support 80 character line lengths
     'opts'    => ['--side-by-side', '--width=168'],
     'colored' => 0,
    },
  );
} else {
  # No Enscript -> respect difftype, but don't offer colorization.
  if ($input{f} && $input{f} =~ /^([ucs])c$/) {
    $input{f} = $1;
  }
}
# is there any query ?
if (@barequery) {
  $barequery = join (';', @barequery);
  $query     = "?$barequery";
  $barequery = ";$barequery";
} else {
  $query = "";
}
undef @barequery;
if (defined($input{path})) {
  redirect("$scriptname/$input{path}$query");
}
# get actual parameters
{
  my $sortby = $input{sortby} || 'file';
  $bydate    = 0;
  $byrev     = 0;
  $byauthor  = 0;
  $bylog     = 0;
  $byfile    = 0;
  if ($sortby eq 'date') {
    $bydate = 1;
  } elsif ($sortby eq 'rev') {
    $byrev = 1;
  } elsif ($sortby eq 'author') {
    $byauthor = 1;
  } elsif ($sortby eq 'log') {
    $bylog = 1;
  } else {
    $byfile = 1;
  }
}
$defaultDiffType = $input{f};
$logsort = $input{logsort};
# alternate CVS-Tree, configured in cvsweb.conf
if ($input{cvsroot} && $CVSROOT{$input{cvsroot}}) {
  $cvstree = $input{cvsroot};
} else {
  $cvstree = $cvstreedefault;
}
$cvsroot = $CVSROOT{$cvstree};
# create icons out of description
foreach my $k (keys %ICONS) {
  my ($itxt, $ipath, $iwidth, $iheight) = @{$ICONS{$k}};
  no strict 'refs';
  if ($ipath) {
    ${"${k}icon"} =
      sprintf('',
              htmlquote($ipath), htmlquote($itxt), $iwidth, $iheight);
  } else {
    ${"${k}icon"} = $itxt;
  }
}
my $config_cvstree = "$config-$cvstree";
# Do some special configuration for cvstrees
if (-f $config_cvstree) {
  do "$config_cvstree"
    or fatal("500 Internal Error",
             'Error in loading configuration file: %s
%s
',
             $config_cvstree, $@);
}
undef $config_cvstree;
$re_prcategories  = '(?:' . join ('|', @prcategories) . ')' if @prcategories;
$re_prkeyword     = quotemeta($prkeyword) if defined($prkeyword);
$prcgi           .= '%s' if defined($prcgi) && $prcgi !~ /%s/;
$fullname         = catfile($cvsroot, $where);
my $rewrite = 0;
if ($pathinfo =~ m|//|) {
  $pathinfo =~ y|/|/|s;
  $rewrite = 1;
}
if (-d $fullname) {
  if ($pathinfo !~ m|/$|) {
    $pathinfo .= '/';
    $rewrite   = 1;
  }
} elsif ($pathinfo =~ m|/$|) {
  chop $pathinfo;
  $rewrite = 1;
}
if ($rewrite) {
  redirect($scriptname . uri_escape_path($pathinfo) . $query, 1);
}
undef $rewrite;
undef $pathinfo;
if (!-d $cvsroot) {
  fatal("500 Internal Error",
        '$CVSROOT not found!%s
',
       $errcode, $err || $export_err);
  } else {
    $| = 1;    # Essential to get the buffering right.
    local (*TAR_OUT);
    my (@cmd, $ctype);
    if ($istar) {
      my @tar = ($CMD{tar}, @tar_options, '-cf', '-', $basedir);
      my @gzip = ($CMD{gzip}, @gzip_options, '-c');
      push(@cmd, \@tar, '|', \@gzip);
      $ctype = 'application/x-gzip';
    } elsif ($iszip) {
      my @zip = ($CMD{zip}, @zip_options, '-r', '-', $basedir);
      push(@cmd, \@zip, \'');
      $ctype = 'application/zip';
    }
    push(@cmd, '>pipe', \*TAR_OUT);
    my ($h, $err) = startproc(@cmd);
    if ($h) {
      print "Content-Type: $ctype\r\n\r\n";
      local $/ = undef;
      print %s
',
                $istar ? 'Tar' : 'Zip', $? >> 8 || -1, $err);
    }
  }
  # Clean up.
  rmtree($tmpexportdir);
  &fatal(@fatal) if @fatal;
  exit;
}
##############################
# View a directory
###############################
if (-d $fullname) {
  my $dh = do { local (*DH); };
  opendir($dh, $fullname) or fatal("404 Not Found", '%s: %s', $where, $!);
  my @dir = grep(!forbidden(catfile($fullname, $_)), readdir($dh));
  closedir($dh);
  my @subLevelFiles = findLastModifiedSubdirs(@dir) if $show_subdir_lastmod;
  my @unreadable = getDirLogs($cvsroot, $where, @subLevelFiles);
  if ($where eq '/') {
    html_header($defaulttitle);
    $long_intro =~ s/!!CVSROOTdescr!!/$CVSROOTdescr{$cvstree}/g;
    print $long_intro;
  } else {
    html_header($where);
    my $html = (-f catfile($fullname, 'README.cvs.html,v') ||
                -f catfile($fullname, 'Attic', 'README.cvs.html,v'));
    my $text = (!$html &&
                (-f catfile($fullname, 'README.cvs,v') ||
                 -f catfile($fullname, 'Attic', 'README.cvs,v')));
    if ($html || $text) {
      my $rev = $input{only_with_tag} || 'HEAD';
      my $cr = abs_path($cvsroot) || $cvsroot;
      my $co = "$where/README.cvs.html" if $html;
      $co ||= "$where/README.cvs" if $text;
      # abs_path() taints when run as a CGI...
      if ($cr =~ VALID_PATH) {
        $cr = $1;
      } else {
        fatal('500 Internal Error', 'Illegal CVS root: %s', $cr);
      }
      my @cmd = ($CMD{cvs}, @cvs_options, '-d', $cr, 'co', '-p', "-r$rev",$co);
      local (*CVS_OUT, *CVS_ERR);
      my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*CVS_OUT,
                                '2>pipe', \*CVS_ERR);
      fatal('500 Internal Error', $err) unless $h;
      if ($html) {
        local $/ = undef;
        print 
';
        }
        print "
\n";
  my $infocols = 1;
  printf(<
EOF
  printf(' \n";
  my $dirrow = 0;
  my $i;
  lookingforattic:
  for ($i = 0; $i <= $#dir; $i++) {
    if ($dir[$i] eq "Attic") {
      last lookingforattic;
    }
  }
  if (!$input{hideattic}
      && ($i <= $#dir)
      && opendir($dh, $fullname . '/Attic'))
  {
    splice(@dir, $i, 1, grep((s|^|Attic/|, !m|/\.|), readdir($dh)));
    closedir($dh);
  }
  my $hideAtticToggleLink =
    $input{hideattic}
    ? ''
    : &link('[hide]', sprintf('./%s#dirlist', &toggleQuery('hideattic')));
  # Sort without the Attic/ pathname.
  # place directories first
  my $filesexists;
  my $filesfound;
  foreach my $file (sort { &fileSortCmp } @dir) {
    next if ($file eq curdir());
    # ignore CVS lock and stale NFS files
    next if ($file =~ /^\#cvs\.|^,|^\.nfs/); # \# for XEmacs cperl-mode...
    # Check whether to show the CVSROOT path
    next if ($input{hidecvsroot} && $where eq '/' && $file eq 'CVSROOT');
    # Is it a directory?
    my $isdir = -d catdir($fullname, $file);
    # Ignore non-readable files and directories?
    next if ($input{hidenonreadable} && (! -r _ || ($isdir && ! -x _)));
    my $attic = '';
    if ($file =~ s|^Attic/||) {
      $attic = ' (in the Attic) ' .
        $hideAtticToggleLink . '';
    }
    if ($file eq updir() || $isdir) {
      next if ($file eq updir() && $where eq '/');
      my ($rev, $date, $log, $author, $filename, $keywordsubst) =
        @{$fileinfo{$file}} if (defined($fileinfo{$file}));
      printf "', ($byfile ? ' class="sorted"' : ''));
  if ($byfile) {
    print 'File';
  } else {
    print &link('File',
                sprintf('./%s#dirlist', toggleQuery('sortby', 'file')));
  }
  print " \n";
  # Do not display the other column headers if we do not have any files
  # with revision information.
  if (scalar(%fileinfo)) {
    $infocols++;
    printf('', ($byrev ? ' class="sorted"' : ''));
    if ($byrev) {
      print 'Rev.';
    } else {
      print &link('Rev.',
                  sprintf('./%s#dirlist', toggleQuery('sortby', 'rev')));
    }
    print " \n";
    $infocols++;
    printf('', ($bydate ? ' class="sorted"' : ''));
    if ($bydate) {
      print 'Age';
    } else {
      print &link('Age',
                  sprintf('./%s#dirlist', toggleQuery('sortby', 'date')));
    }
    print " \n";
    if ($show_author) {
      $infocols++;
      printf('', ($byauthor ? ' class="sorted"' : ''));
      if ($byauthor) {
        print 'Author';
      } else {
        print
          &link('Author',
                sprintf('./%s#dirlist', toggleQuery('sortby', 'author')));
      }
      print " \n";
    }
    $infocols++;
    printf('', ($bylog ? ' class="sorted"' : ''));
    if ($bylog) {
      print 'Last log entry';
    } else {
      print &link('Last log entry',
                  sprintf('./%s#dirlist', toggleQuery('sortby', 'log')));
    }
    print " \n";
  } elsif ($use_descriptions) {
    print "Description \n";
    $infocols++;
  }
  print "\n \n";
      $dirrow++;
    } elsif ($file =~ s/,v$//) {
      my $fileurl = ($attic ? 'Attic/' : '') . uri_escape_path($file);
      my $url     = './' . $fileurl . $query;
      $filesexists++;
      next if (!defined($fileinfo{$file}));
      my ($rev, $date, $log, $author, $filename, $keywordsubst) =
        @{$fileinfo{$file}};
      my $isbinary = $keywordsubst eq 'b' ? 1 : 0;
      $filesfound++;
      printf "",
        ($dirrow % 2) ? 'even' : 'odd';
      if ($file eq updir()) {
        my $url = "../$query";
        print $nofilelinks ? $backicon : &link($backicon, $url);
        print ' ', &link("Parent Directory", $url);
      } else {
        my $url = './' . uri_escape_path($file) . "/$query";
        print '';
        print $nofilelinks ? $diricon : &link($diricon, $url);
        print ' ', &link(htmlquote("$file/"), $url), $attic;
        if ($file eq "Attic") {
          print ' ',
            &link('[show]',
                  sprintf('./%s#dirlist', &toggleQuery('hideattic'))),
                    '';
        }
      }
      # Show last change in dir
      if ($filename) {
        print " \n  \n";
        print readableTime(time() - $date, 0) if $date;
        print " \n", htmlquote($author)
          if $show_author;
        print " \n";
        $filename =~ s%^[^/]+/%%;
        print &link(htmlquote("$filename/$rev"),
                    sprintf('%s/%s%s#rev%s',
                            uri_escape($file), uri_escape($filename),
                            $query, $rev)), ' 
';
        if ($log) {
          print htmlify(substr($log, 0, $shortLogLen), $allow_dir_extra);
          print '...' if (length($log) > 80);
        }
      } else {
        my $dwhere = ($where ne '/' ? $where : '') . $file;
        if ($use_descriptions && defined $descriptions{$dwhere}) {
          print '';
          print $descriptions{$dwhere};
        } elsif ($infocols > 1) {
          # close the row with the appropriate number of
          # columns, so that the vertical seperators are visible
          my ($cols) = $infocols;
          while ($cols > 1) {
            print " \n ";
            $cols--;
          }
        }
      }
      print " \n\n", ($dirrow % 2) ? 'even' : 'odd';
      printf ' ";
      $dirrow++;
    }
    print "\n";
  }
  print "\n";
  if ((my $num = scalar(@unreadable)) && ! $input{hidenonreadable}) {
    printf(<', $allow_cvsgraph ? '' : ' colspan="2"';
      my $icon = $isbinary ? $binfileicon : $fileicon;
      print $nofilelinks ? $icon : &link($icon, $url);
      print ' ', &link(htmlquote($file), $url), $attic;
      print ' ', graph_link($fileurl) if $allow_cvsgraph;
      print " \n", display_link($fileurl, $rev);
      print " \n";
      print readableTime(time() - $date, 0) if $date;
      print " \n", htmlquote($author) if $show_author;
      print " \n";
      if ($log) {
        print htmlify(substr($log, 0, $shortLogLen), $allow_dir_extra);
        print '...' if (length $log > 80);
      }
      print " \n
 %s
%s":This document has moved ", &link('here', $url), ".
\n"; html_footer(); exit(1); } sub safeglob($) { my ($filename) = @_; (my $dirname = $filename) =~ s|/[^/]+$||; $filename =~ s|.*/||; my @results; my $dh = do { local (*DH); }; if (opendir($dh, $dirname)) { my $glob = $filename; my $t; # transform filename from glob to regex. Deal with: # [, {, ?, * as glob chars # make sure to escape all other regex chars $glob =~ s/([\.\(\)\|\+])/\\$1/g; $glob =~ s/\*/.*/g; $glob =~ s/\?/./g; $glob =~ s/{([^}]+)}/($t = $1) =~ s-,-|-g; "($t)"/eg; $glob = qr/^$glob$/; foreach (readdir($dh)) { if ($_ =~ $glob && $_ =~ VALID_PATH) { push(@results, catfile($dirname, $1)); # untaint } } closedir($dh); } return @results; } # # Searches @command_path for the given executable file. # sub search_path($) { my ($command) = @_; for my $d (@command_path) { my $cmd = catfile($d, $command); return $cmd if (-x $cmd && !-d _); } return ''; } # # Gets the enscript(1) highlight mode corresponding to the given filename, # or undef if unsupported. # sub getEnscriptHL($) { return undef unless $allow_enscript; my ($filename) = @_; while (my ($hl, $regex) = each %enscript_types) { return $hl if ($filename =~ $regex); } return undef; } # # Gets the MIME type for the given file name. # sub getMimeType($;$) { my ($fullname, $binary) = @_; $binary = ($keywordsubstitution && $keywordsubstitution =~ /b/) unless defined($binary); (my $suffix = $fullname) =~ s/^.*\.([^.]*)$/$1/; my $mimetype = $MTYPES{$suffix}; $mimetype ||= $MimeTypes->mimeTypeOf($fullname) if defined($MimeTypes); if (!$mimetype && $suffix ne '*' && -f $mime_types && -r _) { my $fh = do { local (*FH); }; if (open($fh, $mime_types)) { my $re = sprintf('^\s*(\S+\/\S+)\s.+\b%s\b', quotemeta($suffix)); $re = qr/$re/; while (my $line = <$fh>) { if ($line =~ $re) { $mimetype = $1; $MTYPES{$suffix} = $mimetype; last; } } close($fh); } else { warn("Can't open MIME types file $mime_types for reading: $!"); } } $mimetype ||= $MTYPES{'*'}; $mimetype ||= $binary ? 'application/octet-stream' : 'text/plain'; return $mimetype; } ############################### # read first lines like head(1) ############################### sub head($;$) { my ($fh, $linecount) = @_; $linecount ||= 10; my @buf; if ($linecount > 0) { for (my $i = 0; !eof($fh) && $i < $linecount; $i++) { push @buf, scalar <$fh>; } } else { @buf = <$fh>; } return @buf; } ############################### # scan vim and Emacs directives ############################### sub scan_directives(@) { my $ts = undef; for (@_) { $ts = $1 if /\b(?:ts|tabstop|tab-width)[:=]\s*([1-9]\d*)\b/; } ('tabstop' => $ts); } sub openOutputFilter() { return unless $output_filter; open(STDOUT, "|-") and return; # child of child open(STDERR, '>', devnull()) unless $DEBUG; exec($output_filter) or exit -1; } ############################### # show Annotation ############################### sub doAnnotate($$) { my ($rev, $tag) = @_; $rev = $tag || 'HEAD' if ($rev eq '.'); (my $pathname = $where) =~ s|((?<=/)Attic/)?[^/]*$||; (my $filename = $where) =~ s|^.*/||; # This annotate version is based on the cvs annotate-demo Perl script by # Cyclic Software. It was written by Cyclic Software, # http://www.cyclic.com/, and is in the public domain. # We could abandon the use of rlog, rcsdiff and co using # the cvs server in a similiar way one day (..after rewrite). local (*CVS_IN, *CVS_OUT); my $annotate_err; my ($h, $err) = startproc([ $CMD{cvs}, @annotate_options, 'server' ], '%s', $? >> 8 || -1, $err) unless $h; # OK, first send the request to the server. A simplified example is: # Root /home/kingdon/zwork/cvsroot # Argument foo/xx # Directory foo # /home/kingdon/zwork/cvsroot/foo # Directory . # /home/kingdon/zwork/cvsroot # annotate # although as you can see there are a few more details. print CVS_IN "Root $cvsroot\n"; print CVS_IN "Valid-responses ok error Valid-requests Checked-in Updated Merged Removed M E\n"; # Don't worry about sending valid-requests, the server just needs to # support "annotate" and if it doesn't, there isn't anything to be done. print CVS_IN "UseUnchanged\n"; print CVS_IN "Argument -r\n"; print CVS_IN "Argument $rev\n"; print CVS_IN "Argument $where\n"; # The protocol requires us to fully fake a working directory (at # least to the point of including the directories down to the one # containing the file in question). # So if $where is "dir/sdir/file", then dirs will be ("dir","sdir","file") my $path = ''; foreach my $dir (split('/', $where)) { if ($path eq "") { # In our example, $dir is "dir". $path = $dir; } else { print CVS_IN "Directory $path\n"; print CVS_IN "$cvsroot/$path\n"; # In our example, $_ is "sdir" and $path becomes "dir/sdir" # And the next time, "file" and "dir/sdir/file" (which then gets # ignored, because we don't need to send Directory for the file). $path .= "/$dir"; } } undef $path; # And the last "Directory" before "annotate" is the top level. print CVS_IN "Directory .\n"; print CVS_IN "$cvsroot\n"; print CVS_IN "annotate\n"; # OK, we've sent our command to the server. Thing to do is to # close the writer side and get all the responses. if (!close(CVS_IN)) { $h->finish(); fatal('500 Internal Error', 'Annotate failure (exit status %s):
%s, output: ' .
          '%s', $? >> 8, $!, $annotate_err); } navigateHeader($scriptwhere, $pathname, $filename, $rev, 'annotate'); my $revtype = ($rev =~ /\./) ? 'revision' : 'tag'; # TODO: tag -> branch/tag? print '
";
  }
  # prefetch several lines
  my @buf = head(*CVS_OUT);
  my %d = scan_directives(@buf);
  while (@buf || !eof(*CVS_OUT)) {
    $_ = @buf ? shift @buf : ;
    my @words = split;
    # Adding one is for the (single) space which follows $words[0].
    my $rest = substr($_, length($words[0]) + 1);
    if ($words[0] eq "E") {
      next;
    } elsif ($words[0] eq "M") {
      $lineNr++;
      (my $lrev = substr($_, 2,  13)) =~ y/ //d;
      (my $lusr = substr($_, 16, 9))  =~ y/ //d;
      my $line = substr($_, 36);
      # TODO: this does not work for branch/tag revisions.
      my $isCurrentRev = ($rev eq $lrev);
      # we should parse the date here ..
      if ($lrev eq $oldLrev) {
        $revprint = sprintf('%-8s', '');
      } else {
        $revprint = sprintf('%-8s', $lrev);
        $revprint =~ s|(\S+)|&link($1, uri_escape($filename)."$query#rev$1")|e;
        $oldLusr = '';
      }
      $usrprint = ($lusr eq $oldLusr) ? '' : $lusr;
      $oldLrev = $lrev;
      $oldLusr = $lusr;
      print $is_textbased ? '' : ''
        if $isCurrentRev;
      $usrprint = sprintf('%-8s', $usrprint);
      printf '%s%s %s %4d:', $revprint, $isCurrentRev ? '!' : ' ',
        htmlquote($usrprint), $lineNr;
      print spacedHtmlText($line, $d{tabstop});
      print $is_textbased ? '' : '' if $isCurrentRev;
    } elsif ($words[0] eq "ok") {
      # We could complain about any text received after this, like the
      # CVS command line client.  But for simplicity, we don't.
    } elsif ($words[0] eq "error") {
      fatal("500 Internal Error",
            'Error occured during annotate: %s', $_);
    }
  }
  $h->finish();
  if ($annTable) {
    print "";
  } else {
    print " ";
  }
  html_footer();
}
###############################
# make Checkout
###############################
sub doCheckout($$$)
{
  my ($fullname, $rev, $tag) = @_;
  $rev = $tag || undef if (!$rev || $rev eq '.');
  # Start resolving whether we will do a markup view or not.
  my $do_markup = undef;
  my $want_type = $input{'content-type'};
  # No markup if markup disallowed.
  $do_markup = 0 unless $allow_markup;
  # No markup if checkout magic cookie in URL.
  $do_markup = 0 if (!defined($do_markup) && $doCheckout);
  # Do markup if explicitly asked using cvsweb-markup content type.  If the
  # asked content type is anything else, no markup.
  if (!defined($do_markup) && $want_type) {
    if ($want_type =~ CVSWEBMARKUP) {
      $want_type = undef;
      $do_markup = 1;
    } else {
      $do_markup = 0;
    }
  }
  # Ok, if $do_markup is still undefined, we know that a download has not been
  # explicitly asked.  For the last check further down below we'll need to
  # know if the file is binary, and possibly run a log on it.
  my $needlog = $do_markup || $use_moddate;
  my $moddate = undef;
  my $revopt;
  if (defined($rev)) {
    $revopt = "-r$rev";
    if ($needlog) {
      readLog($fullname, $rev);
      $moddate   = $date{$rev};
      # TODO: even this does not work for branch tags, but only normal tags :(
      $moddate ||= $date{$symrev{$rev}} if defined($symrev{$rev});
    }
  } else {
    $revopt = "-rHEAD";
    if ($needlog) {
      readLog($fullname);
      $moddate = $date{$symrev{HEAD}};
    }
  }
  my $cr = abs_path($cvsroot) || $cvsroot;
  # abs_path() taints when run as a CGI...
  if ($cr =~ VALID_PATH) {
    $cr = $1;
  } else {
    fatal('500 Internal Error', 'Illegal CVS root: %s', $cr);
  }
  # Use abs_path() to work around a bug of cvs -p; expand symlinks if we can.
  my @cmd = ($CMD{cvs}, @cvs_options, '-d', $cr, 'co', '-p', $revopt, $where);
  local (*CVS_OUT, *CVS_ERR);
  my ($h, $err) =
    startproc(\@cmd, \"", '>pipe', \*CVS_OUT, '2>pipe', \*CVS_ERR);
  fatal('500 Internal Error',
        'Checkout failure (exit status %s), output: %s', $? >> 8 || -1, $err) unless $h; if (eof(CVS_ERR)) { $h->finish(); fatal("404 Not Found", '%s is not (any longer) pertinent', $where); } #=================================================================== #Checking out squid/src/ftp.c #RCS: /usr/src/CVS/squid/src/ftp.c,v #VERS: 1.1.1.28.6.2 #*************** # Parse CVS header my ($revision, $filename, $cvsheader); $filename = ""; while (
%s' . '(expected "
%s" but got "%s")',
          $cvsheader, $where, $filename);
  }
  # Last checks whether we'll do markup or not.
  my $isbin = $keywordsubstitution && $keywordsubstitution =~ /b/;
  my $mimetype = getMimeType($fullname, $isbin);
  # If we still are not sure whether to do markup or not:
  # if the MIME type is "viewable" or this is not a binary file, do.
  $do_markup = !$isbin || viewable($mimetype) unless defined($do_markup);
  if ($do_markup) {
    # If this is something we'll be linking to in the markup view, we are
    # done with this particular output from "cvs co" and must discard it.
    my $linked = $mimetype =~ m{^image/|application/pdf$}i;
    if ($linked) {
      close(CVS_OUT);
      $h->finish();
    }
    # Here we know the last modified date, but don't know if tags have been
    # added afterwards (those are shown in the markup view): no last-modified.
    cvswebMarkup(\*CVS_OUT, $fullname, $revision, $isbin, $mimetype, $needlog);
    $h->finish() unless $linked;
  } else {
    http_header($want_type || $mimetype, $moddate);
    local $/ = undef;
    print \n";
    my $linenumbers = $input{ln} || 0;
    if (my $enscript_hl = getEnscriptHL($filename)) {
      doEnscript($filehandle, $enscript_hl, $linenumbers);
    } else {
      my $ln  = 0;
      my @buf = ();
      my $ts  = undef;
      if ($preformat_in_markup) {
        # prefetch several lines
        @buf = head($filehandle);
        my %d = scan_directives(@buf);
        $ts = $d{tabstop};
      }
      while (@buf || !eof($filehandle)) {
        $_ = @buf ? shift @buf : <$filehandle>;
        if ($linenumbers) {
          $ln++;
          printf '%5d: ', ($ln) x 2;
        }
        print $preformat_in_markup ? spacedHtmlText($_, $ts) : htmlquote($_);
      }
    }
    print "\n";
  }
  html_footer();
}
sub viewable($)
{
  return shift =~ m{^((text|image)/|application/pdf)}i;
}
###############################
# Show Colored Diff
###############################
sub doDiff($$$$$$)
{
  my ($fullname, $r1, $tr1, $r2, $tr2, $f) = @_;
  if (forbidden($fullname)) {
    fatal('403 Forbidden', 'Access to %s forbidden.', $where);
  }
  my ($rev1, $sym1);
  if ($r1 =~ /([^:]+)(:(.+))?/) {
    $rev1 = $1;
    $sym1 = $3;
  }
  if ($r1 eq 'text') {
    $rev1 = $tr1;
    $sym1 = "";
  }
  my ($rev2, $sym2);
  if ($r2 =~ /([^:]+)(:(.+))?/) {
    $rev2 = $1;
    $sym2 = $3;
  }
  if ($r2 eq 'text') {
    $rev2 = $tr2;
    $sym2 = "";
  }
  #
  # rev1 and rev2 are now both numeric revisions.
  # Thus we do a DWIM here and swap them if rev1 is after rev2.
  # XXX should we warn about the fact that we do this?
  if (&revcmp($rev1, $rev2) > 0) {
    my ($tmp1, $tmp2) = ($rev1, $sym1);
    ($rev1, $sym1) = ($rev2, $sym2);
    ($rev2, $sym2) = ($tmp1, $tmp2);
  }
  my $mimetype = getMimeType($fullname);
  #
  #  Check for per-MIME type diff commands.
  #
  my $diffcmd = undef;
  if (my $diffcmds = $DIFF_COMMANDS{lc($mimetype)}) {
    if ($f =~ /^ext(\d*)$/) {
      my $n = $1 || 0;
      $diffcmd = $diffcmds->[$n];
    }
  }
  if ($diffcmd && $diffcmd->{cmd} && $diffcmd->{name}) {
    if ($diffcmd->{args} && ref($diffcmd->{args}) ne 'ARRAY') {
      fatal('500 Internal Error',
            'Configuration error: arguments to external diff tools must ' .
            'be given as array refs.  See "%s" in ' .
            '%%DIFF_COMMANDS.',
            $diffcmd->{name});
    }
    (my $cvsname = $where) =~ s/\.diff$//;
    # Create two temporary files with the two revisions
    my $temp_fn1 = checkout_to_temp($cvsroot, $cvsname, $rev1);
    my $temp_fn2 = checkout_to_temp($cvsroot, $cvsname, $rev2);
    # Execute chosen diff binary.
    local (*DIFF_OUT);
    my @cmd = ($diffcmd->{cmd});
    push(@cmd, @{$diffcmd->{args}}) if $diffcmd->{args};
    push(@cmd, $temp_fn1, $temp_fn2);
    my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*DIFF_OUT);
    if (!$h) {
      unlink($temp_fn1);
      unlink($temp_fn2);
      fatal('500 Internal Error',
            'Diff failure (exit status %s), output: %s', $? >> 8 || -1, $err); } http_header($diffcmd->{type} || 'text/plain'); local $/ = undef; print
EOF
    doEnscript(\$fh, $hl, 0, 'cvsweb_diff');
    print <
\n";
    html_footer();
    gzipclose();
    exit;
  } else {
    #
    # Plain diff.
    #
    http_header("text/plain");
  }
  #
  #===================================================================
  #RCS file: /home/ncvs/src/sys/netinet/tcp_output.c,v
  #retrieving revision 1.16
  #retrieving revision 1.17
  #diff -c -r1.16 -r1.17
  #*** /home/ncvs/src/sys/netinet/tcp_output.c     1995/11/03 22:08:08     1.16
  #--- /home/ncvs/src/sys/netinet/tcp_output.c     1995/12/05 17:46:35     1.17
  #
  # Ideas:
  # - nuke the stderr output if it's what we expect it to be
  # - Add "no differences found" if the diff command supplied no output.
  #
  #*** src/sys/netinet/tcp_output.c     1995/11/03 22:08:08     1.16
  #--- src/sys/netinet/tcp_output.c     1995/12/05 17:46:35     1.17 RELENG_2_1_0
  # (bogus example, but...)
  #
  my ($f1, $f2);
  if (grep { $_ eq '-u' } @difftype) {
    $f1 = '---';
    $f2 = '\+\+\+';
  } else {
    $f1 = '\*\*\*';
    $f2 = '---';
  }
  while (<$fh>) {
    if (m|^$f1 $cvsroot|o) {
      s|$cvsroot/||o;
      if ($sym1) {
        chop;
        $_ .= " $sym1\n";
      }
    } elsif (m|^$f2 $cvsroot|o) {
      s|$cvsroot/||o;
      if ($sym2) {
        chop;
        $_ .= " $sym2\n";
      }
    }
    print $_;
  }
  close($fh);
}
###############################
# Show Logs ..
###############################
sub getDirLogs($$@)
{
  my ($cvsroot, $dirname, @otherFiles) = @_;
  my $tag = $input{only_with_tag};
  my $DirName = catdir($cvsroot, $where);
  my @files = &safeglob("$DirName/*,v");
  push (@files, &safeglob("$DirName/Attic/*,v")) unless $input{hideattic};
  foreach my $file (@otherFiles) {
    push(@files, catfile($DirName, $file));
  }
  # Weed out unreadable files.
  my $i = 0;
  my @unreadable = ();
  while ($i < scalar(@files)) {
    # Note: last modified files from subdirs returned by
    # findLastModifiedSubdirs() come without the ,v suffix so they're not
    # found here, but have already been checked for readability.  *cough*
    if (-r $files[$i] || !-e _) {
      $i++;
    } else {
      push(@unreadable, splice(@files, $i, 1));
    }
  }
  # If there are no files, we're done.
  return @unreadable unless @files;
  my @cmd = ($CMD{rlog});
  # Can't use -r as '-' is allowed in tagnames,
  # but misinterpreted by rlog.
  push(@cmd, '-r') unless defined($tag);
  my $fh = do { local (*FH); };
  if (!open($fh, '-|')) {                       # Child
    open(STDERR, '>', devnull()) unless $DEBUG; # Ignore rlog's complaints.
    openOutputFilter();
    if ($file_list_len && $file_list_len > 1) {
      while (scalar(@files) > $file_list_len) {  # Process files in chunks.
        system(@cmd, splice(@files, 0, $file_list_len)) == 0 or exit -1;
      }
    }
    exec(@cmd, @files) or exit -1;
  }
  undef @cmd;
  my $state = 'start';
  my ($date, $branchpoint, $branch, $log, @filetags);
  my ($rev, $revision, $revwanted, $filename, $head, $author, $keywordsubst);
  while (<$fh>) {
    if ($state eq "start") {
      #Next file. Initialize file variables
      $rev         = '';
      $revwanted   = '';
      $branch      = '';
      $branchpoint = '';
      $filename    = '';
      $log         = '';
      $revision    = '';
      %symrev      = ();
      @filetags    = ();
      $keywordsubst= '';
      #jump to head state
      $state = "head";
    }
    again:
    if ($state eq "head") {
      #$rcsfile = $1 if (/^RCS file: (.+)$/); #not used (yet)
      if (/^Working file: (.+)$/) {
        $filename = $1;
      } elsif (/^head: (.+)$/) {
        $head = $1;
      } elsif (/^branch: (.+)$/) {
        $branch = $1;
      } elsif (/^keyword substitution: (.+)$/) {
        $keywordsubst = $1;
      } elsif (/^symbolic names:/) {
        $state = "tags";
        ($branch = $head) =~ s/\.\d+$//
          if $branch eq '';
        $branch =~ s/(\d+)$/0.$1/;
        $symrev{MAIN}  = $branch;
        $symrev{HEAD}  = $branch;
        $alltags{MAIN} = 1;
        $alltags{HEAD} = 1;
        push (@filetags, "MAIN", "HEAD");
      } elsif ($_ =~ LOG_REVSEPR) {
        $state = "log";
        $rev   = '';
        $date  = '';
        $log   = '';
        # Try to reconstruct the relative filename if RCS spits out a full path
        $filename =~ s%^\Q$DirName\E/%%;
      }
      next;
    }
    if ($state eq "tags") {
      if (/^\s+([^:]+):\s+([\d\.]+)\s*$/) {
        push (@filetags, $1);
        $symrev{$1}  = $2;
        $alltags{$1} = 1;
        next;
      } elsif (/^\S/) {
        if (defined($tag)) {
          if (defined($symrev{$tag}) || $tag eq "HEAD") {
            $revwanted    = $symrev{$tag eq "HEAD" ? "MAIN" : $tag};
            ($branch      = $revwanted) =~ s/\b0\.//;
            ($branchpoint = $branch)    =~ s/\.?\d+$//;
            $revwanted    = '' if ($revwanted ne $branch);
          } elsif ($tag ne "HEAD") {
            $state = "skip";
            next;
          }
        }
        foreach my $tagfound (@filetags) {
          $tags{$tagfound} = 1;
        }
        $state = "head";
        goto again;
      }
    }
    if ($state eq "log") {
      if ($_ =~ LOG_REVSEPR || $_ =~ LOG_FILESEPR) {
        # End of a log entry.
        my $revbranch = $rev;
        $revbranch =~ s/\.\d+$//;
        if ($revwanted eq '' && $branch ne '' && $branch eq $revbranch
            || !defined($tag))
        {
          $revwanted = $rev;
        }
        if ($revwanted ne ''
            ? $rev eq $revwanted
            : $branchpoint ne ''
              ? $rev eq $branchpoint
              : 0
            && ($rev eq $head))
        {    # Don't think head is needed here..
          my @finfo = ($rev, $date, $log, $author, $filename, $keywordsubst);
          (my $name = $filename) =~ s%/.*%%;
          $fileinfo{$name} = [@finfo];
          $state = "done" if ($rev eq $revwanted);
        }
        $rev  = '';
        $date = '';
        $log  = '';
      } elsif ($date eq ''
               && m|^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);|)
      {
        my $yr    = $1;
        $yr      -= 1900 if ($yr > 100); # Damn 2-digit year routines :-)
        $date     = timegm($6, $5, $4, $3, $2 - 1, $yr);
        ($author) = /author: ([^;]+)/;
        $state    = 'log';
        $log      = '';
        next;
      } elsif ($rev eq '' && /^revision (\d+(?:\.\d+)+).*$/) {
        $rev = $1;    # .*$ eats up the locker(lockers?) info, if any
        next;
      } else {
        $log .= $_;
      }
    }
    if ($_ =~ LOG_FILESEPR) {
      $state = "start";
      next;
    }
  }
  my $linesread = $. || 0;
  close($fh);
  if ($linesread == 0) {
    fatal('500 Internal Error',
          'Failed to spawn GNU rlog on "%s".
Did you set the @command_path in your configuration file correctly? (Currently: "%s")',
          htmlquote(join(', ', @files)), join(':', @command_path));
  }
  return @unreadable;
}
sub readLog($;$)
{
  my ($fullname, $revision) = @_;
  my ($symnames, $head, $rev, $br, $brp, $branch, $branchrev);
  undef %symrev;
  undef %revsym;
  undef @allrevisions;
  undef %date;
  undef %author;
  undef %state;
  undef %difflines;
  undef %log;
  $keywordsubstitution = '';
  my $fh = do { local (*FH); };
  if (!open($fh, "-|")) {    # child
    openOutputFilter();
    $revision = defined($revision) ? "-r$revision" : '';
    if ($revision =~ /\./) {
      # Normal revision, not a branch/tag name.
      exec($CMD{rlog}, $revision, $fullname) or exit -1;
    } else {
      exec($CMD{rlog}, $fullname) or exit -1;
    }
  }
  my $curbranch = undef;
  while (<$fh>) {
    if ($symnames) {
      if (/^\s+([^:]+):\s+([\d\.]+)/) {
        $symrev{$1} = $2;
        next;
      } else {
        $symnames = 0;
      }
    }
    if (/^head:\s+([\d\.]+)/) {
      $head = $1;
    } elsif (/^branch:\s+([\d\.]+)/) {
      $curbranch = $1;
    } elsif (/^symbolic names/) {
      $symnames = 1;
    } elsif (/^keyword substitution: (.+)$/) {
      $keywordsubstitution = $1;
    } elsif (/^-----/) {
      last;
    }
  }
  ($curbranch = $head) =~ s/\.\d+$// if (!defined($curbranch));
  # each log entry is of the form:
  # ----------------------------
  # revision 3.7.1.1
  # date: 1995/11/29 22:15:52;  author: fenner;  state: Exp;  lines: +5 -3
  # log info
  # ----------------------------
  # For a locked revision, the first line after the separator 
  # becomes smth like
  # revision 9.19       locked by: vassilii;
  logentry:
  while ($_ !~ LOG_FILESEPR) {
    $_ = <$fh>;
    last logentry if (!defined($_));    # EOF
    if (/^revision (\d+(?:\.\d+)+)/) {
      $rev = $1;
      unshift(@allrevisions, $rev);
    } elsif ($_ =~ LOG_FILESEPR || $_ =~ LOG_REVSEPR) {
      next logentry;
    } else {
      # The rlog output is syntactically ambiguous.  We must
      # have guessed wrong about where the end of the last log
      # message was.
      # Since this is likely to happen when people put rlog output
      # in their commit messages, don't even bother keeping
      # these lines since we don't know what revision they go with
      # any more.
      next logentry;
    }
    $_ = <$fh>;
    if (
      m|^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);\s+author:\s+(\S+);\s+state:\s+(\S+);\s+(lines:\s+([0-9\s+-]+))?|
      )
    {
      my $yr           = $1;
      $yr             -= 1900 if ($yr > 100); # Damn 2-digit year routines :-)
      $date{$rev}      = timegm($6, $5, $4, $3, $2 - 1, $yr);
      $author{$rev}    = $7;
      $state{$rev}     = $8;
      $difflines{$rev} = $10;
    } else {
      fatal("500 Internal Error", 'Error parsing RCS output: %s', $_);
    }
  line:
    while (<$fh>) {
      next line if (/^branches:\s/);
      last line if ($_ =~ LOG_FILESEPR || $_ =~ LOG_REVSEPR);
      $log{$rev} .= $_;
    }
  }
  close($fh);
  @revorder = reverse sort { revcmp($a, $b) } @allrevisions;
  #
  # HEAD is an artificial tag which is simply the highest tag number on the main
  # branch, unless there is a branch tag in the RCS file in which case it's the
  # highest revision on that branch.  Find it by looking through @revorder; it
  # is the first commit listed on the appropriate branch.
  # This is not neccesary the same revision as marked as head in the RCS file.
  my $headrev = $curbranch || "1";
  ($symrev{MAIN} = $headrev) =~ s/(\d+)$/0.$1/;
  foreach $rev (@revorder) {
    if ($rev =~ /^(\S*)\.\d+$/ && $headrev eq $1) {
      $symrev{HEAD} = $rev;
      last;
    }
  }
  ($symrev{HEAD} = $headrev) =~ s/\.\d+$// unless defined($symrev{HEAD});
  #
  # Now that we know all of the revision numbers, we can associate
  # absolute revision numbers with all of the symbolic names, and
  # pass them to the form so that the same association doesn't have
  # to be built then.
  #
  undef @branchnames;
  undef %branchpoint;
  undef $sel;
  foreach (reverse sort keys %symrev) {
    $rev = $symrev{$_};
    if ($rev =~ /^((.*)\.)?\b0\.(\d+)$/) {
      push (@branchnames, $_);
      #
      # A revision number of A.B.0.D really translates into
      # "the highest current revision on branch A.B.D".
      #
      # If there is no branch A.B.D, then it translates into
      # the head A.B .
      #
      # This reasoning also applies to the main branch A.B,
      # with the branch number 0.A, with the exception that
      # it has no head to translate to if there is nothing on
      # the branch, but I guess this can never happen?
      #
      # (the code below gracefully forgets about the branch
      # if it should happen)
      #
      $head = defined($2) ? $2 : "";
      $branch = $3;
      $branchrev = $head . ($head ne "" ? "." : "") . $branch;
      $rev = $head;
      my $regex = '^' . quotemeta($branchrev) . '\b';
      $regex = qr/$regex/;
      foreach my $r (@revorder) {
        if ($r =~ $regex) {
          $rev = $branchrev;
          last;
        }
      }
      next if ($rev eq "");
      if ($rev ne $head && $head ne "") {
        $branchpoint{$head} .= ', ' if ($branchpoint{$head});
        $branchpoint{$head} .= $_;
      }
    }
    $revsym{$rev} .= ", " if ($revsym{$rev});
    $revsym{$rev} .= $_;
    $sel .= sprintf("\n",
                    htmlquote($rev), (htmlquote($_)) x 2);
  }
  my ($onlyonbranch, $onlybranchpoint);
  if ($onlyonbranch = $input{only_with_tag}) {
    $onlyonbranch = $symrev{$onlyonbranch};
    if ($onlyonbranch && $onlyonbranch =~ s/\b0\.//) {
      ($onlybranchpoint = $onlyonbranch) =~ s/\.\d+$//;
    } else {
      $onlybranchpoint = $onlyonbranch;
    }
    if (!defined($onlyonbranch) || $onlybranchpoint eq "") {
      fatal("404 Tag not found", 'Tag "%s" is not defined.',
            $input{only_with_tag});
    }
  }
  undef @revisions;
  foreach (@allrevisions) {
    ($br  = $_)  =~ s/\.\d+$//;
    ($brp = $br) =~ s/\.\d+$//;
    next if ($onlyonbranch
             && $br ne $onlyonbranch
             && $_  ne $onlybranchpoint);
    unshift(@revisions, $_);
  }
  if ($logsort eq "date") {
    # Sort the revisions in commit order an secondary sort on revision
    # (secondary sort needed for imported sources, or the first main
    # revision gets before the same revision on the 1.1.1 branch)
    @revdisplayorder =
      sort { $date{$b} <=> $date{$a} || -revcmp($a, $b) } @revisions;
  } elsif ($logsort eq "rev") {
    # Sort the revisions in revision order, highest first
    @revdisplayorder = reverse sort { revcmp($a, $b) } @revisions;
  } else {
    # No sorting. Present in the same order as rlog / cvs log
    @revdisplayorder = @revisions;
  }
  return $curbranch;
}
sub getDiffLinks($$$)
{
  my ($url, $mimetype, $isbin) = @_;
  my @links = ();
  if (!$isbin) { # Offer ordinary diff only for non-binary files.
    push(@links, &link('preferred', $url));
    for my $difftype ($DIFFTYPES{$defaultDiffType}{colored} ? qw(u) : qw(h)) {
      my $f = $difftype eq $defaultDiffType ? '' : $difftype;
      push(@links,
           &link(htmlquote(lc($DIFFTYPES{$difftype}{descr})), "$url;f=$f"));
    }
  }
  if (my $extdiffs = $DIFF_COMMANDS{lc($mimetype)}) {
    for my $i (0 .. scalar(@$extdiffs)-1) {
      my $extdiff = $extdiffs->[$i];
      push(@links, &link(htmlquote($extdiff->{name}), "$url;f=ext$i"))
        if ($extdiff->{cmd} && $extdiff->{name});
    }
  }
  return @links;
}
sub printLog($$$;$$)
{
  # inlogview: 1 if in log view, otherwise in markup view.
  ($_, my $mimetype, my $isbin, my $inlogview, my $isSelected) = @_;
  (my $br  = $_)  =~ s/\.\d+$//;
  (my $brp = $br) =~ s/\.?\d+$//;
  print "";
  if (defined($revsym{$_})) {
    foreach my $sym (split(", ", $revsym{$_})) {
      print '';
    }
  }
  if ($revsym{$br} && !defined($nameprinted{$br})) {
    foreach my $sym (split(", ", $revsym{$br})) {
      print '';
    }
    $nameprinted{$br} = 1;
  }
  print "\n Revision $_";
  if (/^1\.1\.1\.\d+$/) {
    print " (vendor branch)";
  }
  (my $filename = $where) =~ s|^.*/||;
  my $fileurl   = uri_escape($filename);
  undef $filename;
  my $isDead = ($state{$_} eq 'dead');
  if (!$isDead) {
    print ': ', download_link($fileurl, $_, 'download', $mimetype);
    my @vlinks = ();
    push(@vlinks, display_link($fileurl, $_, 'text', 'text/plain'))
      unless $isbin;
    push(@vlinks, display_link($fileurl, $_, 'markup', 'text/x-cvsweb-markup'))
      if ($allow_markup && $inlogview && (!$isbin || viewable($mimetype)));
    if (!$isbin && $allow_annotate) {
      push(@vlinks,
           &link('annotated',
                 sprintf('%s?annotate=%s%s', $fileurl, $_, $barequery)));
    }
    print ' - view: ', join(', ', @vlinks) if @vlinks;
    undef @vlinks;
    if (!$isbin && $allow_version_select) {
      print ' - ';
      if ($isSelected) {
        print '[selected for diffs]';
      } else {
        print &link('select for diffs',
                    sprintf('%s?r1=%s%s#rev%s',
                            $fileurl, $_, $barequery, $_));
      }
    }
    print ' - ', graph_link('', 'revision graph')
      if (!$inlogview && $allow_cvsgraph);
  }
  print "
\n";
  print '';
  if (defined @mytz) {
    my ($est) = $mytz[(localtime($date{$_}))[8]];
    print scalar localtime($date{$_}), " $est (";
  } else {
    print scalar gmtime($date{$_}), " UTC (";
  }
  print readableTime(time() - $date{$_}, 1), ' ago)';
  print ' by ', htmlquote($author{$_}), "
\n";
  printf("Branches: %s
\n", link_tags($revsym{$br})) if $revsym{$br};
  printf("CVS tags: %s
\n", link_tags($revsym{$_}))  if $revsym{$_};
  printf("Branch point for: %s
\n", link_tags($branchpoint{$_}))
    if $branchpoint{$_};
  # Find the previous revision
  my $prev;
  my @prevrev = split(/\./, $_);
  do {
    if (--$prevrev[$#prevrev] <= 0) {
      # If it was X.Y.Z.1, just make it X.Y
      pop (@prevrev);
      pop (@prevrev);
    }
    $prev = join (".", @prevrev);
  } until (defined($date{$prev}) || $prev eq "");
  if ($isDead) {
    print "FILE REMOVED
\n";
  } else {
    my %diffrev = ();
    $diffrev{$_} = 1;
    $diffrev{""} = 1;
    my $diff = 'Diff to:';
    my $printed = 0;
    #
    # Offer diff to previous revision
    if ($prev) {
      $diffrev{$prev} = 1;
      my $url =
        sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $prev, $_, $barequery);
      if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) {
        print $diff, ' previous ', $prev, ': ', join(', ', @dlinks);
        $diff = ';'; $printed = 1;
      }
    }
    #
    # Plus, if it's on a branch, and it's not a vendor branch,
    # offer a diff with the branch point.
    if ($revsym{$brp}
      && !/^1\.1\.1\.\d+$/
      && !defined($diffrev{$brp}))
    {
      my $url =
        sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $brp, $_, $barequery);
      if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) {
        print $diff, ' branchpoint ', $brp, ': ', join(', ', @dlinks);
        $diff = ';'; $printed = 1;
      }
    }
    #
    # Plus, if it's on a branch, and it's not a vendor branch,
    # offer to diff with the next revision of the higher branch.
    # (e.g. change gets committed and then brought
    # over to -stable)
    if (/^\d+\.\d+\.\d+/ && !/^1\.1\.1\.\d+$/) {
      my ($i, $nextmain);
      for ($i = 0; $i < $#revorder && $revorder[$i] ne $_; $i++) {
      }
      my @tmp2 = split(/\./, $_);
      for ($nextmain = ""; $i > 0; $i--) {
        my $next = $revorder[$i - 1];
        my @tmp1 = split(/\./, $next);
        if (@tmp1 < @tmp2) {
          $nextmain = $next;
          last;
        }
        # Only the highest version on a branch should have
        # a diff for the "next MAIN".
        last
          if (@tmp1 - 1 <= @tmp2
          && join (".", @tmp1[0 .. $#tmp1 - 1]) eq
          join (".", @tmp2[0 .. $#tmp1 - 1]));
      }
      if (!defined($diffrev{$nextmain})) {
        $diffrev{$nextmain} = 1;
        my $url = sprintf('%s.diff?r1=%s;r2=%s%s',
          $fileurl, $nextmain, $_, $barequery);
        if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) {
          print $diff, ' next MAIN ', $nextmain, ': ', join(', ', @dlinks);
          $diff = ';'; $printed = 1;
        }
      }
    }
    # Plus if user has selected only r1, then present a link
    # to make a diff to that revision
    if (defined($input{r1}) && !defined($diffrev{$input{r1}})) {
      $diffrev{$input{r1}} = 1;
      my $url = sprintf('%s.diff?r1=%s;r2=%s%s',
        $fileurl, $input{r1}, $_, $barequery);
      if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) {
        print $diff, ' selected ', $input{r1}, ': ', join(', ', @dlinks);
        $diff = ';'; $printed = 1;
      }
    }
    print "
\n" if $printed;
  }
  if ($prev ne "" && $difflines{$_}) {
    printf "Changes since revision %s: %s lines
\n",
      htmlquote($prev), htmlquote($difflines{$_});
  }
  print "\n";
  print &htmlify($log{$_}, $allow_log_extra);
  print "\n";
}
#
# Generates the HTML view for CvsGraph.
#
sub doGraphView()
{
  (my $pathname = $where) =~ s|[^/]*$||;
  (my $filename = $where) =~ s|^.*/||;
  navigateHeader($scriptwhere, $pathname, $filename, undef, 'graph');
  my $title = 'Revision graph of ' . htmlquote($pathname . $filename);
  my $mapname = 'CvsGraphMap';
  printf(<%s
 EOF
  # Remove any pre-existing tag/branch names from branch links.
  (my $notag_query = $barequery) =~ s/;+only_with_tag=.*?(?=;|$)//g;
  my @graph_cmd =
    ($CMD{cvsgraph},
     '-r', $cvsroot,
     '-m', $pathname,
     '-i',
     '-M', $mapname,
     '-x', 'x',
     "-Omap_branch_href=\"href=\\\"./?only_with_tag=%(%t%)$notag_query\\\"\"",
     "-Omap_rev_href=\"href=\\\"?rev=%(%R%)$barequery\\\"\"",
     "-Omap_diff_href=\"href=\\\"%(%F%).diff" .
     "?r1=%(%P%);r2=%(%R%)$barequery\\\"\"",
     );
  push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config;
  push(@graph_cmd, $filename . ',v');
  local *CVSGRAPH_OUT;
  my ($h, $err) =
    startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT);
  fatal('500 Internal Error', $err) unless $h;
  # Browser compatibility kludge: many browsers do not support client side
  # image maps where the \n";
  html_footer();
}
#
# Generates a graph using CvsGraph.
#
sub doGraph()
{
  (my $pathname = $where) =~ s|[^/]*$||;
  (my $filename = $where) =~ s|^.*/||;
  http_header('image/png');
  my @graph_cmd = ($CMD{cvsgraph}, '-r', $cvsroot, '-m', $pathname);
  push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config;
  push(@graph_cmd, $filename . ',v');
  local *CVSGRAPH_OUT;
  my ($h, $err) =
    startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT);
  fatal('500 Internal Error', $err) unless $h;
  {
    local $/ = undef;
    binmode(\*STDOUT);
    print
EOF
  # Remove any pre-existing tag/branch names from branch links.
  (my $notag_query = $barequery) =~ s/;+only_with_tag=.*?(?=;|$)//g;
  my @graph_cmd =
    ($CMD{cvsgraph},
     '-r', $cvsroot,
     '-m', $pathname,
     '-i',
     '-M', $mapname,
     '-x', 'x',
     "-Omap_branch_href=\"href=\\\"./?only_with_tag=%(%t%)$notag_query\\\"\"",
     "-Omap_rev_href=\"href=\\\"?rev=%(%R%)$barequery\\\"\"",
     "-Omap_diff_href=\"href=\\\"%(%F%).diff" .
     "?r1=%(%P%);r2=%(%R%)$barequery\\\"\"",
     );
  push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config;
  push(@graph_cmd, $filename . ',v');
  local *CVSGRAPH_OUT;
  my ($h, $err) =
    startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT);
  fatal('500 Internal Error', $err) unless $h;
  # Browser compatibility kludge: many browsers do not support client side
  # image maps where the \n";
  html_footer();
}
#
# Generates a graph using CvsGraph.
#
sub doGraph()
{
  (my $pathname = $where) =~ s|[^/]*$||;
  (my $filename = $where) =~ s|^.*/||;
  http_header('image/png');
  my @graph_cmd = ($CMD{cvsgraph}, '-r', $cvsroot, '-m', $pathname);
  push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config;
  push(@graph_cmd, $filename . ',v');
  local *CVSGRAPH_OUT;
  my ($h, $err) =
    startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT);
  fatal('500 Internal Error', $err) unless $h;
  {
    local $/ = undef;
    binmode(\*STDOUT);
    print ;
  }
  $h->finish();
}
sub doLog($)
{
  my ($fullname) = @_;
  my $curbranch = readLog($fullname);
  html_header("CVS log for $where");
  my $upwhere = $where;
  (my $filename = $where) =~ s|^.*/||;
  my $backurl = "./$query#" . uri_escape($filename);
  if ($where =~ m|^(.*?)((?<=/)Attic/)?[^/]+$|) {
    $upwhere = $1;
    $backurl = ".$backurl" if $2; # skip over Attic
  }
  my $isbin = $keywordsubstitution =~ /b/;
  my $mimetype = getMimeType($filename, $isbin);
  print "\n ";
  print &link($backicon, $backurl), " Up to ",
    &clickablePath($upwhere, 1), "\n
\n";
  print "\n ";
  print &link('Request diff between arbitrary revisions', '#diff');
  print ' - ', &graph_link('', 'Display revisions graphically')
    if $allow_cvsgraph;
  if ($cvshistory_url) {
    (my $d = $upwhere) =~ s|/+$||;
    print ' - ', history_link($d, $filename);
  }
  print "\n
\n
\n";
  print "\n";
  my $explain = $isbin ? ' (i.e.: CVS considers this a binary file)' : '';
  print "Keyword substitution: $keywordsubstitution$explain
\n";
  if ($curbranch) {
    print "Default branch: ", ($revsym{$curbranch} || $curbranch);
  } else {
    print "No default branch";
  }
  print "
\n";
  print 'Current tag: ', htmlquote($input{only_with_tag}), "
\n"
    if $input{only_with_tag};
  print "
\n";
  undef %nameprinted;
  for my $r (@revdisplayorder) {
    print "
\n";
    my $sel = (defined($input{r1}) && $input{r1} eq $r);
    print "\n" if $sel;
    printLog($r, $mimetype, $isbin, 1, $sel);
    print "\n" if $sel;
  }
  printf(<
  %s 
   
EOF
    }
  } elsif ($state eq "PreChange") {     # state eq "PreChange"
                                        # we got removes with subsequent adds
    if (HAS_EDIFF) {
      # construct the suffix tree
      my $left_diff = join("\n", @$leftColRef[0..$leftRow-1]);
      my $right_diff = join("\n", @$rightColRef[0..$rightRow-1]);
      my $diff_str = String::Ediff::ediff($left_diff, $right_diff);
      my @diff_str = split(/ /, $diff_str);
      my $INFINITY = 10000000;
      push(@diff_str, ($INFINITY) x 8);
      my ($idx, $b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
        (0, @diff_str[0..7]);
      my ($l_cul, $r_cul) = (0, 0);
      my ($ldx, $rdx) = (0, 0);
      my (@left_html, @right_html);
      for (my $j = 0; $j < $leftRow; $j++) {
        my $line_len = length(@$leftColRef[$j]);
        my $line = @$leftColRef[$j];
        $l_cul += length($line) + 1; # includes "\n"
        my $l_culx = $l_cul - 1; # not includes "\n"
        if ($j < $lb1) {
          $line = spacedHtmlText($line);
          push(@left_html, "$line ");
        } elsif ($lb1 == $j) {
          my $html_line;
          while ($lb1 == $j) {
            my $begin_char = $l_culx - $b1;
            $line =~ /^(.*)(.{$begin_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            last if ($j != $le1);
            my $end_char = $l_culx - $e1;
            $line =~ /^(.*)(.{$end_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            $idx++;
            my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) =
              ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2);
            ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
              @diff_str[$idx*8..($idx+1)*8-1];
            $lb1 = $INFINITY if ($lb1 < 0);
            $lb2 = $INFINITY if ($lb2 < 0);
            $le1 = $INFINITY if ($le1 < 0);
            $le2 = $INFINITY if ($le2 < 0);
            if ($te1 > $b1) {
              ($b1, $lb1) = ($te1, $tle1);
            }
            if ($te2 > $b2) {
              ($b2, $lb2) = ($te2, $tle2);
            }
          }
          push(@left_html,
               sprintf('%s%s ',
                       $html_line, spacedHtmlText($line)));
        } elsif ($le1 == $j) {
          my $html_line;
          while ($le1 == $j) {
            my $end_char = $l_culx - $e1;
            $line =~ /^(.*)(.{$end_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            $idx++;
            my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) =
              ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2);
            ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
              @diff_str[$idx*8..($idx+1)*8-1];
            $lb1 = $INFINITY if ($lb1 < 0);
            $lb2 = $INFINITY if ($lb2 < 0);
            $le1 = $INFINITY if ($le1 < 0);
            $le2 = $INFINITY if ($le2 < 0);
            if ($te1 > $b1) {
              ($b1, $lb1) = ($te1, $tle1);
            }
            if ($te2 > $b2) {
              ($b2, $lb2) = ($te2, $tle2);
            }
            last if ($lb1 != $j);
            my $begin_char = $l_culx - $b1;
            $line =~ /^(.*)(.{$begin_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
          }
          push(@left_html,
              sprintf('%s%s ',
                      $html_line, spacedHtmlText($line)));
        } else {
          $line = spacedHtmlText($line);
          push(@left_html, "$line ");
        }
      }
      ($idx, $b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
        (0, @diff_str[0..7]);
      $lb1 = $INFINITY if ($lb1 < 0);
      $lb2 = $INFINITY if ($lb2 < 0);
      $le1 = $INFINITY if ($le1 < 0);
      $le2 = $INFINITY if ($le2 < 0);
      for (my $j = 0; $j < $rightRow; $j++) {
        my $line_len = length(@$rightColRef[$j]);
        my $line = @$rightColRef[$j];
        $r_cul += length($line) + 1; # includes "\n"
        my $r_culx = $r_cul - 1; # not includes "\n"
        if ($j < $lb2) {
          $line = spacedHtmlText($line);
          push(@right_html, "$line ");
        } elsif ($lb2 == $j) {
          my $html_line;
          while ($lb2 == $j) {
            my $begin_char = $r_culx - $b2;
            $line =~ /^(.*)(.{$begin_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            last if ($j != $le2);
            my $end_char = $r_culx - $e2;
            $line =~ /^(.*)(.{$end_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            $idx++;
            my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) =
              ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2);
            ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
              @diff_str[$idx*8..($idx+1)*8-1];
            $lb1 = $INFINITY if ($lb1 < 0);
            $lb2 = $INFINITY if ($lb2 < 0);
            $le1 = $INFINITY if ($le1 < 0);
            $le2 = $INFINITY if ($le2 < 0);
            if ($te1 > $b1) {
              ($b1, $lb1) = ($te1, $tle1);
            }
            if ($te2 > $b2) {
              ($b2, $lb2) = ($te2, $tle2);
            }
          }
          push(@right_html,
               sprintf('%s%s ',
                       $html_line, spacedHtmlText($line)));
        } elsif ($le2 == $j) {
          my $html_line;
          while ($le2 == $j) {
            my $end_char = $r_culx - $e2;
            $line =~ /^(.*)(.{$end_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
            $idx++;
            my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) =
              ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2);
            ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) =
              @diff_str[$idx*8..($idx+1)*8-1];
            $lb1 = $INFINITY if ($lb1 < 0);
            $lb2 = $INFINITY if ($lb2 < 0);
            $le1 = $INFINITY if ($le1 < 0);
            $le2 = $INFINITY if ($le2 < 0);
            if ($te1 > $b1) {
              ($b1, $lb1) = ($te1, $tle1);
            }
            if ($te2 > $b2) {
              ($b2, $lb2) = ($te2, $tle2);
            }
            last if ($lb2 != $j);
            my $begin_char = $r_culx - $b2;
            $line =~ /^(.*)(.{$begin_char})$/;
            $html_line .= spacedHtmlText($1) .
              '';
            $line = $2;
          }
          push(@right_html,
               sprintf('%s%s ',
                       $html_line, spacedHtmlText($line)));
        } else {
          $line = spacedHtmlText ($line);
          push @right_html, "$line ";
        }
      }
      for (my $j = 0; $j < $leftRow || $j < $rightRow ; $j++) { # dump out both cols
        print  '';
        if ($j < $leftRow) {
          print $left_html[$j];
        } else {
          print '  ';
        }
        if ($j < $rightRow) {
          print $right_html[$j];
        } else {
          print '  ';
        }
        print " \n";
      }
    } else {
      for (my $j = 0; $j < $leftRow || $j < $rightRow; $j++) { # dump both cols
        print "\n";
        if ($j < $leftRow) {
          print ' ' .
            spacedHtmlText(@$leftColRef[$j]) . ' ';
        } else {
          print '  ';
        }
        print "\n";
        if ($j < $rightRow) {
          print ' ' .
            spacedHtmlText(@$rightColRef[$j]) . ' ';
        } else {
          print '  ';
        }
        print "\n \n";
      }
    }
  }
}
#
# Generates "human readable", HTMLified diffs.
#
sub human_readable_diff($$)
{
  my ($fh, $rev) = @_;
  (my $where_nd       = $where)       =~ s|\.diff$||;
  (my $filename       = $where_nd)    =~ s|^.*/||;
  (my $pathname       = $where_nd)    =~ s|((?<=/)Attic/)?[^/]*$||;
  (my $scriptwhere_nd = $scriptwhere) =~ s|\.diff$||;
  navigateHeader($scriptwhere_nd, $pathname, $filename, $rev, 'diff');
  # Read header to pick up read revision and date, if possible.
  my ($r1d, $r1r, $r2d, $r2r);
  while (<$fh>) {
    ($r1d, $r1r) = /\t(.*)\t(.*)$/ if (/^--- /);
    ($r2d, $r2r) = /\t(.*)\t(.*)$/ if (/^\+\+\+ /);
    last if (/^\+\+\+ /);
  }
  my ($rev1, $date1);
  if (defined($r1r) && $r1r =~ /^(\d+\.)+\d+$/) {
    $rev1  = $r1r;
    $date1 = $r1d;
  }
  my ($rev2, $date2);
  if (defined($r2r) && $r2r =~ /^(\d+\.)+\d+$/) {
    $rev2  = $r2r;
    $date2 = $r2d;
  }
  $rev1  = $input{r1}      unless defined($rev1);
  $rev1  = $input{tr1}     if (defined($rev1) && $rev1 eq 'text');
  $rev1  = 'unknown-left'  unless defined($rev1);
  $rev2  = $input{r2}      unless defined($rev2);
  $rev2  = $input{tr2}     if (defined($rev2) && $rev2 eq 'text');
  $rev2  = 'unknown-right' unless defined($rev2);
  $date1 = defined($date1) ? ', ' . htmlquote($date1) : '';
  $date2 = defined($date2) ? ', ' . htmlquote($date2) : '';
  my $link = uri_escape($filename) . ($query ? "$query;" : '?');
  # Using Diff for /$where_nd between versions $rev1 and $rev2
version $rev1$date1
 
version $rev2$date2
 
 
EOF
  # Process diff text
  # prefetch several lines
  my @buf = head($fh);
  my %d = scan_directives(@buf);
  my $leftRow  = 0;
  my $rightRow = 0;
  my ($difftxt, @rightCol, @leftCol, $oldline, $newline, $funname);
  $link .= 'content-type=text%2Fx-cvsweb-markup;';
  $link .= 'ln=1;' unless ($link =~ /\?.*\bln=1\b/);
  while (@buf || !eof($fh)) {
    $difftxt = @buf ? shift @buf : <$fh>;
    if ($difftxt =~ /^@@/) {
      ($oldline, $newline, $funname) =
        $difftxt =~ /@@ \-([0-9]+).*\+([0-9]+).*@@(.*)/;
      $funname = htmlquote($funname);
      $funname =~ s/\s/ /go;
      $funname &&= " $funname";
      my $ol = $oldline || 1;
      my $nl = $newline || 1;
      print <
 Line $oldline$funname
 
 Line $newline$funname
 
EOF
      $state    = "dump";
      $leftRow  = 0;
      $rightRow = 0;
    } else {
      my ($diffcode, $rest) = $difftxt =~ /^([-+ ])(.*)/;
      $diffcode = '' unless defined($diffcode);
      $_ = $rest;
      #########
      # little state machine to parse unified-diff output (Hen, zeller@think.de)
      # in order to get some nice 'ediff'-mode output
      # states:
      #  "dump"             - just dump the value
      #  "PreChangeRemove"  - we began with '-' .. so this could be the start of a 'change' area or just remove
      #  "PreChange"        - okey, we got several '-' lines and moved to '+' lines -> this is a change block
      ##########
      if ($diffcode eq '+') {
        if ($state eq "dump")
        {    # 'change' never begins with '+': just dump out value
          $_ = spacedHtmlText($rest, $d{tabstop});
          printf(<
   
  %s 
EOF
        } else {    # we got minus before
          $state = "PreChange";
          $rightCol[$rightRow++] = $_;
        }
      } elsif ($diffcode eq '-') {
        $state = "PreChangeRemove";
        $leftCol[$leftRow++] = $_;
      } else {    # empty diffcode
        flush_diff_rows \@leftCol, \@rightCol, $leftRow, $rightRow;
        $_ = spacedHtmlText($rest, $d{tabstop});
        printf(<
  %s 
  %s 
EOF
        $state    = "dump";
        $leftRow  = 0;
        $rightRow = 0;
      }
    }
  }
  close($fh);
  flush_diff_rows \@leftCol, \@rightCol, $leftRow, $rightRow;
  # state is empty if we didn't have any change
  if (!$state) {
    print <
   
 - No viewable change - 
 
EOF
  }
  printf(<
EOF
}
sub doEnscript($$$;$)
{
  my ($filehandle, $highlight, $linenumbers, $lang) = @_;
  $lang ||= 'cvsweb';
  my @cmd = ($CMD{enscript},
             @enscript_options,
             '-q', "--language=$lang", '-o', '-', "--highlight=$highlight");
  local *ENSCRIPT_OUT;
  my ($h, $err) =
    startproc(\@cmd, $filehandle, '>pipe', \*ENSCRIPT_OUT);
  fatal('500 Internal Error', $err) unless $h;
  # We could short-circuit and have enscript output directly to STDOUT above,
  # but that doesn't work with mod_perl (at least some 1.99 versions).
  if ($linenumbers) {
    my $ln = 0;
    while () {
      printf '%5d: ', (++$ln) x 2;
      print $_;
    }
  } else {
    local $/ = undef;
    print ;
  }
  $h->finish();
}
#
# The passed in $path and $filename should not be URI escaped, and $swhere
# *should* be.
#
sub navigateHeader($$$$$;$)
{
  my ($swhere, $path, $filename, $rev, $title, $moddate) = @_;
  $swhere = "" if ($swhere eq $scriptwhere);
  $swhere = './' . uri_escape($filename) if ($swhere eq "");
  my $qfile = htmlquote($filename);
  my $qpath = htmlquote($path);
  my $trev  = $rev ? " - " . htmlquote($rev) : '';
  http_header('text/html', $moddate);
  (my $header = &cgi_style::html_header($title, 0)) =~ s,\A.*,\n$HTML_META,s;
  $header .= $CSS .
'
 
  
   ';
  print $header;
  my $frag = '';
  if ($rev) {
    $frag  = '#';
    $frag .= 'rev' if ($rev =~ /\./);  # Normal revision: prefix with "rev".
    $frag .= $rev;                     # Append revision/branch/tag.
  }
  my $backurl = "$swhere$query$frag";
  print &link($backicon, $backurl);
  printf 'Return to %s CVS log', &link($qfile, $backurl);
  print " $fileicon ";
  printf(<%s Up to %s
   
EOF
}
sub plural_write($$)
{
  my ($num, $text) = @_;
  if ($num != 1) {
    $text .= "s";
  }
  if ($num > 0) {
    return join (' ', $num, $text);
  } else {
    return "";
  }
}
##
# print readable timestamp in terms of
# '..time ago'
# H. Zeller 
##
sub readableTime($$)
{
  my ($secs, $long) = @_;
  # This function works correctly for time >= 2 seconds.
  return 'very little time' if ($secs < 2);
  my %desc = (
              1        => 'second',
              60       => 'minute',
              3600     => 'hour',
              86400    => 'day',
              604800   => 'week',
              2628000  => 'month',
              31536000 => 'year'
             );
  my @breaks = sort { $a <=> $b } keys %desc;
  my $i = 0;
  while ($i <= $#breaks && $secs >= 2 * $breaks[$i]) {
    $i++;
  }
  $i--;
  my $break  = $breaks[$i];
  my $retval = plural_write(int($secs / $break), $desc{$break});
  if ($long == 1 && $i > 0) {
    my $rest = $secs % $break;
    $i--;
    $break = $breaks[$i];
    my $resttime = plural_write(int($rest / $break), $desc{$break});
    if ($resttime) {
      $retval .= ", $resttime";
    }
  }
  return $retval;
}
#
# Returns a htmlified path where each directory is a link for faster
# navigation.  $clickLast controls whether the basename
# (last directory/file) is a link as well.  The passed in $pathname should
# *not* be URI escaped.
#
sub clickablePath($$)
{
  my ($pathname, $clickLast) = @_;
  my $root = '[' . htmlquote($CVSROOTdescr{$cvstree} || $cvstree) . ']';
  # This should never happen (see chooseCVSRoot()), but let's be sure...
  return $root if ($pathname eq '/');
  my $retval =
    ' ' . &link($root, sprintf('%s/%s#dirlist', $scriptname, $query));
  my $wherepath = '';
  my ($lastslash) = $pathname =~ m|/$|;
  foreach (split(m|/|, $pathname)) {
    $retval .= ' / ';
    $wherepath .= "/$_";
    my $last = "$wherepath/" eq "/$pathname" || $wherepath eq "/$pathname";
    if ($clickLast || !$last) {
      $retval .= &link(htmlquote($_),
                       join ('',
                             $scriptname, uri_escape_path($wherepath),
                             (!$last || $lastslash ? '/' : ''), $query,
                             (!$last || $lastslash ? "#dirlist" : "")));
    } else {    # do not make a link to the current dir
      $retval .= htmlquote($_);
    }
  }
  return $retval;
}
sub chooseCVSRoot()
{
  print "
EOF
}
sub chooseMirror()
{
  # This code comes from the original BSD-cvsweb
  # and may not be useful for your site; If you don't
  # set %MIRRORS this won't show up, anyway.
  scalar(%MIRRORS) or return;
  # Should perhaps exclude the current site somehow...
  print "\n\nThis CVSweb is mirrored in\n";
  my @tmp = map(&link(htmlquote($_), $MIRRORS{$_}), sort keys %MIRRORS);
  my $tmp = pop (@tmp);
  if (scalar(@tmp)) {
    print join (', ', @tmp), ' and ';
  }
  print "$tmp.\n
\n";
}
sub fileSortCmp()
{
  (my $af = $a) =~ s/,v$//;
  (my $bf = $b) =~ s/,v$//;
  my ($rev1, $date1, $log1, $author1, $filename1) = @{$fileinfo{$af}}
    if (defined($fileinfo{$af}));
  my ($rev2, $date2, $log2, $author2, $filename2) = @{$fileinfo{$bf}}
    if (defined($fileinfo{$bf}));
  my $comp = 0;
  if (defined($filename1) && defined($filename2) &&
      $af eq $filename1   && $bf eq $filename2)
  {
    # Two files
    $comp = -revcmp($rev1, $rev2)             if ($byrev  && $rev1  && $rev2);
    $comp = ($date2   <=> $date1)             if ($bydate && $date1 && $date2);
    if ($input{ignorecase}) {
      $comp = (uc($log1)    cmp uc($log2))    if ($bylog && $log1 && $log2);
      $comp = (uc($author1) cmp uc($author2)) if ($byauthor &&
                                                  $author1 && $author2);
    } else {
      $comp = ($log1    cmp $log2)            if ($bylog && $log1 && $log2);
      $comp = ($author1 cmp $author2)         if ($byauthor &&
                                                  $author1 && $author2);
    }
  }
  if ($comp == 0) {
    # Directories first, then files under version control,
    # then other, "rogue" files.
    # Sort by filename if no other criteria available.
    my $ad = (
      (-d "$fullname/$a")
      ? 'D'
      : (defined($fileinfo{$af}) ? 'F' : 'R')
    );
    my $bd = (
      (-d "$fullname/$b")
      ? 'D'
      : (defined($fileinfo{$bf}) ? 'F' : 'R')
    );
    (my $c = $a) =~ s|.*/||;
    (my $d = $b) =~ s|.*/||;
    my ($l, $r) = ("$ad$c", "$bd$d");
    $comp = $input{ignorecase} ? (uc($l) cmp uc($r)) : ($l cmp $r);
    # Parent dir is always first, then Attic.
    if ($comp != 0) {
      if ($l eq 'D..') {
        $comp = -1;
      } elsif ($r eq 'D..') {
        $comp = 1;
      } elsif ($l eq 'DAttic') {
        $comp = -1;
      } elsif ($r eq 'DAttic') {
        $comp = 1;
      }
    }
  }
  return $comp;
}
#
# Returns a URL to download the selected revision.
# Expects the passed in URL to be URI escaped, relative, and without a query
# string.
#
sub download_url($$;$)
{
  my ($url, $revision, $mimetype) = @_;
  my @dots = $revision =~ /\./g;
  $revision =~ s/\b0\.(?=\d+$)// if (scalar(@dots) & 1);
  if (!defined($mimetype) || $mimetype !~ CVSWEBMARKUP) {
    my $path = $where;
    $path =~ s|[^/]+$||;
    $url = "$scriptname/$CheckoutMagic/$path$url";
  }
  $url .= '?rev=' . uri_escape($revision);
  $url .= ';content-type=' . uri_escape($mimetype) if $mimetype;
  return $url;
}
#
# Returns a link to download the selected revision.
# Expects the passed in URL to be URI escaped, relative,
# and without a query string.
#
sub download_link($$$;$)
{
  my ($url, $revision, $textlink, $mimetype) = @_;
  return sprintf('%s',
                 download_url($url, $revision, $mimetype) . $barequery,
                 htmlquote($textlink));
}
#
# Returns a URL to display the selected revision.
# Expects the passed in URL to be URI escaped, and without a query string.
#
sub display_url($$;$)
{
  my ($url, $revision, $mimetype) = @_;
  $url .= '?rev=' . uri_escape($revision);
  $url .= ';content-type=' . uri_escape($mimetype) if $mimetype;
  return $url;
}
#
# Returns a link to display the selected revision.
# Expects the passed in URL to be URI escaped, and without a query string.
#
sub display_link($$;$$)
{
  my ($url, $revision, $textlink, $mtype) = @_;
  $textlink = $revision unless defined($textlink);
  return sprintf('%s',
                 display_url($url, $revision, $mtype) . $barequery,
                 htmlquote($textlink));
}
#
# Expects the passed in URL to be URI escaped, and without a query string.
# The passed in link text should be already HTML escaped as appropriate.
#
sub graph_link($;$)
{
  my ($url, $text) = @_;
  $text ||= $graphicon;
  return sprintf('%s', $url, $barequery, $text);
}
#
# Returns a link to CVSHistory for the given directory and filename.
#
sub history_link($$;$)
{
  my ($dir, $file, $text) = @_;
  $dir  ||= '';
  $file ||= '';
  $text ||= 'History';
  return &link($text,
               sprintf('%s?cvsroot=%s;dsearch=%s;fsearch=%s;limit=1',
                       $cvshistory_url, uri_escape($input{cvsroot} || ''),
                       uri_escape($dir), uri_escape($file)));
}
# Returns a Query string with the
# specified parameter toggled
sub toggleQuery($;$)
{
  my ($toggle, $value) = @_;
  my %vars = %input;
  if (defined($value)) {
    $vars{$toggle} = $value;
  } else {
    $vars{$toggle} = $vars{$toggle} ? 0 : 1;
  }
  # Build a new query of non-default paramenters
  my $newquery = "";
  foreach my $var (@stickyvars) {
    my ($value)   = defined($vars{$var})         ? $vars{$var}         : "";
    my ($default) = defined($DEFAULTVALUE{$var}) ? $DEFAULTVALUE{$var} : "";
    if ($value ne $default) {
      $newquery .= ';' if ($newquery ne "");
      $newquery .= uri_escape($var) . '=' . uri_escape($value);
    }
  }
  if ($newquery) {
    return '?' . $newquery;
  }
  return "";
}
sub htmlquote($)
{
  local ($_) = @_;
  # Special Characters; RFC 1866
  s/&/&/g;
  s/\"/"/g;
  s/</g;
  s/>/>/g;
  return $_;
}
sub htmlunquote($)
{
  local ($_) = @_;
  # Special Characters; RFC 1866
  s/"/\"/g;
  s/<//g;
  s/&/&/g;
  return $_;
}
sub uri_escape_path($)
{
  return join('/', map(uri_escape($_), split(m|/+|, shift, -1)));
}
sub http_header(;$$)
{
  my ($content_type, $moddate) = @_;
  $content_type ||= 'text/html';
  $content_type .= "; charset=$charset"
    if ($charset && $content_type =~ m,^text/,);
  # Note that in the following, we explicitly join() and concatenate the
  # headers instead of printing them as an array.  This is because some
  # systems, eg. early versions of mod_perl 2 don't quite get it if the
  # last \r\n\r\n isn't included in the last "payload" header print().
  my @headers = ();
  # TODO: ctime(3) from scalar gmtime() isn't HTTP compliant, see HTTP::Date.
  push(@headers, 'Last-Modified: ' . scalar gmtime($moddate) . ' GMT')
    if $moddate;
  push(@headers, 'Content-Type: ' . $content_type);
  if ($allow_compress && $maycompress) {
    if (HAS_ZLIB
        || (defined($CMD{gzip}) && open(GZIP, "| $CMD{gzip} -1 -c")))
    {
      push(@headers, 'Content-Encoding: x-gzip');
      push(@headers, 'Vary: Accept-Encoding');     # RFC 2616, 14.44
      print join("\r\n", @headers) . "\r\n\r\n";
      $| = 1;
      $| = 0;                                      # Flush header output.
      tie(*GZIP, __PACKAGE__, \*STDOUT) if HAS_ZLIB;
      select(GZIP);
      $gzip_open = 1;
    } else {
      print join("\r\n", @headers) . "\r\n\r\n";
      printf
        'Unable to find gzip binary in the $command_path (%s) to compress output
',
          htmlquote(join(':', @command_path));
    }
  } else {
    print join("\r\n", @headers) . "\r\n\r\n";
  }
}
sub html_header($;$)
{
  my ($title, $moddate) = @_;
  $title  = htmlquote($title);
  my $css = $CSS || '';
  http_header('text/html', $moddate);
  (my $header = &cgi_style::html_header($title, 0)) =~ s,\A.*,\n$HTML_META,s;
  $header .= $css;
  print $header;
}
sub html_footer()
{
  print &cgi_style::html_footer;
}
sub link_tags($)
{
  my ($tags) = @_;
  (my $filename = $where) =~ s|^.*/||;
  my $fileurl = './' . uri_escape($filename);
  my $ret = "";
  foreach my $sym (split(", ", $tags)) {
    $ret .= ",\n" if ($ret ne "");
    $ret .= &link(htmlquote($sym),
                  $fileurl . toggleQuery('only_with_tag', $sym));
  }
  return $ret;
}
#
# See if a file/dir is listed in the config file's @ForbiddenFiles list.
# Takes a full file system path or one relative to $cvsroot, and strips the
# trailing ",v" if present, then compares.  Returns 1 if forbidden, else 0.
#
sub forbidden($)
{
  (my $path = canonpath(shift)) =~ s/,v$//;
  $path =~ s|^$cvsroot/+||;
  for my $forbidden_re (@ForbiddenFiles) {
    return 1 if ($path =~ $forbidden_re);
  }
  return 0;
}
#
# Starts a process using IPC::Run.  All arguments are passed to
# IPC::Run::start() as-is.  Returns an array ($harness, $error) where
# $harness is from IPC::Run if start() succeeds, undef otherwise.  In case
# of an error, $error contains the error message.
#
sub startproc(@)
{
  my $h = my $err = undef;
  eval {
    local $SIG{__DIE__};
    $h = IPC::Run::start(@_) or die("return code: $?");
  };
  if ($@) {
    $h->finish() if $h;
    $h = undef;
    $err = "'@{$_[0]}' failed: $@";
  }
  return ($h, $err);
}
#
# Runs a process using IPC::Run.  All arguments are passed to
# IPC::Run::run() as-is.  Returns an array ($exitcode, $errormsg).
#
sub runproc(@)
{
  eval {
    local $SIG{__DIE__};
    IPC::Run::run(@_);
  };
  my $exitcode = $? >> 8;
  my $errormsg  = undef;
  if ($@) {
    $exitcode ||= -1;
    $errormsg = "'@{$_[0]}' failed: $@";
  }
  return ($exitcode, $errormsg);
}
#
# Check out a file to a temporary file.
#
sub checkout_to_temp($$$)
{
  my ($cvsroot, $cvsname, $rev) = @_;
  # Pipe given cvs file into a temporary place.
  my ($temp_fh, $temp_fn) = tempfile('.cvsweb.XXXXXXXX', DIR => tmpdir());
  my @cmd = ($CMD{cvs}, @cvs_options, '-Qd', $cvsroot,
             'co', '-p', "-r$rev", $cvsname);
  local (*DIFF_OUT);
  my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*DIFF_OUT);
  if ($h) {
    local $/ = undef;
    print $temp_fh ;
    $h->finish();
    close($temp_fh);
  } else {
    close($temp_fh);
    unlink($temp_fn);
    fatal('500 Internal Error',
          'Checkout failure (exit status %s), output: %s
',
          $? >> 8 || -1, $err);
  }
  return $temp_fn;
}
#
# Close the GZIP handle, and remove the tie.
#
sub gzipclose
{
  if ($gzip_open) {
    select(STDOUT);
    close(GZIP);
    untie *GZIP;
    $gzip_open = 0;
  }
}
# implement a gzipped file handle via the Compress:Zlib compression
# library.
sub MAGIC1() { 0x1f }
sub MAGIC2() { 0x8b }
sub OSCODE() { 3 }
sub TIEHANDLE
{
  my ($class, $out) = @_;
  my ($d) = Compress::Zlib::deflateInit(
    -Level      => Compress::Zlib::Z_BEST_COMPRESSION(),
    -WindowBits => -Compress::Zlib::MAX_WBITS()
    )
    or return undef;
  my ($o) = { handle => $out,
              dh     => $d,
              crc    => 0,
              len    => 0,
            };
  my ($header) = pack("c10",
                      MAGIC1, MAGIC2, Compress::Zlib::Z_DEFLATED(),
                      0, 0, 0, 0, 0, 0, OSCODE);
  print {$o->{handle}} $header;
  return bless($o, $class);
}
sub PRINT
{
  my ($o)   = shift;
  my ($buf) = join (defined($,) ? $, : "", @_);
  my ($len) = length($buf);
  my ($compressed, $status) = $o->{dh}->deflate($buf);
  print {$o->{handle}} $compressed if defined($compressed);
  $o->{crc} = Compress::Zlib::crc32($buf, $o->{crc});
  $o->{len} += $len;
  return $len;
}
sub PRINTF
{
  my ($o)   = shift;
  my ($fmt) = shift;
  my ($buf) = sprintf($fmt, @_);
  my ($len) = length($buf);
  my ($compressed, $status) = $o->{dh}->deflate($buf);
  print {$o->{handle}} $compressed if defined($compressed);
  $o->{crc} = Compress::Zlib::crc32($buf, $o->{crc});
  $o->{len} += $len;
  return $len;
}
sub WRITE
{
  my ($o, $buf, $len, $off) = @_;
  my ($compressed, $status) = $o->{dh}->deflate(substr($buf, 0, $len));
  print {$o->{handle}} $compressed if defined($compressed);
  $o->{crc} = Compress::Zlib::crc32(substr($buf, 0, $len), $o->{crc});
  $o->{len} += $len;
  return $len;
}
sub CLOSE
{
  my ($o) = @_;
  return if !defined($o->{dh});
  my ($buf) = $o->{dh}->flush();
  $buf .= pack("V V", $o->{crc}, $o->{len});
  print {$o->{handle}} $buf;
  undef $o->{dh};
}
sub DESTROY
{
  my ($o) = @_;
  CLOSE($o);
}
# Local variables:
# indent-tabs-mode: nil
# cperl-indent-level: 2
# End: