use Fcntl;
use File::Temp qw/tempdir tempfile/;
use File::Basename;
+use Getopt::Long qw(:config require_order no_ignore_case);
+
+my $VERSION = '@@GIT_VERSION@@';
my $log = GITCVS::log->new();
my $cfg;
my $state = { prependdir => '' };
$log->info("--------------- STARTING -----------------");
+my $usage =
+ "Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n".
+ " --base-path <path> : Prepend to requested CVSROOT\n".
+ " --strict-paths : Don't allow recursing into subdirectories\n".
+ " --export-all : Don't check for gitcvs.enabled in config\n".
+ " --version, -V : Print version information and exit\n".
+ " --help, -h, -H : Print usage information and exit\n".
+ "\n".
+ "<directory> ... is a list of allowed directories. If no directories\n".
+ "are given, all are allowed. This is an additional restriction, gitcvs\n".
+ "access still needs to be enabled by the gitcvs.enabled config option.\n";
+
+my @opts = ( 'help|h|H', 'version|V',
+ 'base-path=s', 'strict-paths', 'export-all' );
+GetOptions( $state, @opts )
+ or die $usage;
+
+if ($state->{version}) {
+ print "git-cvsserver version $VERSION\n";
+ exit;
+}
+if ($state->{help}) {
+ print $usage;
+ exit;
+}
+
my $TEMP_DIR = tempdir( CLEANUP => 1 );
$log->debug("Temporary directory is '$TEMP_DIR'");
+$state->{method} = 'ext';
+if (@ARGV) {
+ if ($ARGV[0] eq 'pserver') {
+ $state->{method} = 'pserver';
+ shift @ARGV;
+ } elsif ($ARGV[0] eq 'server') {
+ shift @ARGV;
+ }
+}
+
+# everything else is a directory
+$state->{allowed_roots} = [ @ARGV ];
+
+# don't export the whole system unless the users requests it
+if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
+ die "--export-all can only be used together with an explicit whitelist\n";
+}
+
# if we are called with a pserver argument,
# deal with the authentication cat before entering the
# main loop
-if (@ARGV && $ARGV[0] eq 'pserver') {
+if ($state->{method} eq 'pserver') {
my $line = <STDIN>; chomp $line;
- unless( $line eq 'BEGIN AUTH REQUEST') {
+ unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
}
+ my $request = $1;
$line = <STDIN>; chomp $line;
req_Root('root', $line) # reuse Root
or die "E Invalid root $line \n";
unless ($line eq 'anonymous') {
print "E Only anonymous user allowed via pserver\n";
print "I HATE YOU\n";
+ exit 1;
}
$line = <STDIN>; chomp $line; # validate the password?
$line = <STDIN>; chomp $line;
- unless ($line eq 'END AUTH REQUEST') {
- die "E Do not understand $line -- expecting END AUTH REQUEST\n";
+ unless ($line eq "END $request REQUEST") {
+ die "E Do not understand $line -- expecting END $request REQUEST\n";
}
print "I LOVE YOU\n";
+ exit if $request eq 'VERIFICATION'; # cvs login
# and now back to our regular programme...
}
my ( $cmd, $data ) = @_;
$log->debug("req_Root : $data");
- $state->{CVSROOT} = $data;
+ unless ($data =~ m#^/#) {
+ print "error 1 Root must be an absolute pathname\n";
+ return 0;
+ }
+
+ my $cvsroot = $state->{'base-path'} || '';
+ $cvsroot =~ s#/+$##;
+ $cvsroot .= $data;
+
+ if ($state->{CVSROOT}
+ && ($state->{CVSROOT} ne $cvsroot)) {
+ print "error 1 Conflicting roots specified\n";
+ return 0;
+ }
+
+ $state->{CVSROOT} = $cvsroot;
$ENV{GIT_DIR} = $state->{CVSROOT} . "/";
+
+ if (@{$state->{allowed_roots}}) {
+ my $allowed = 0;
+ foreach my $dir (@{$state->{allowed_roots}}) {
+ next unless $dir =~ m#^/#;
+ $dir =~ s#/+$##;
+ if ($state->{'strict-paths'}) {
+ if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
+ $allowed = 1;
+ last;
+ }
+ } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
+ $allowed = 1;
+ last;
+ }
+ }
+
+ unless ($allowed) {
+ print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
+ print "E \n";
+ print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
+ return 0;
+ }
+ }
+
unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
- print "E \n";
- print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
+ print "E \n";
+ print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
return 0;
}
}
foreach my $line ( @gitvars )
{
- next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ );
- $cfg->{$1}{$2} = $3;
+ next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
+ unless ($2) {
+ $cfg->{$1}{$3} = $4;
+ } else {
+ $cfg->{$1}{$2}{$3} = $4;
+ }
}
- unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i )
- {
+ my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
+ || $cfg->{gitcvs}{enabled});
+ unless ($state->{'export-all'} ||
+ ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
print "E GITCVS emulation needs to be enabled on this repo\n";
print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
print "E \n";
return 0;
}
- if ( defined ( $cfg->{gitcvs}{logfile} ) )
+ my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
+ if ( $logfile )
{
- $log->setfile($cfg->{gitcvs}{logfile});
+ $log->setfile($logfile);
} else {
$log->nofile();
}
argsplit("add");
+ my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ $updater->update();
+
+ argsfromdir($updater);
+
my $addcount = 0;
foreach my $filename ( @{$state->{args}} )
{
$filename = filecleanup($filename);
+ my $meta = $updater->getmeta($filename);
+ my $wrev = revparse($filename);
+
+ if ($wrev && $meta && ($wrev < 0))
+ {
+ # previously removed file, add back
+ $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
+
+ print "MT +updated\n";
+ print "MT text U \n";
+ print "MT fname $filename\n";
+ print "MT newline\n";
+ print "MT -updated\n";
+
+ unless ( $state->{globaloptions}{-n} )
+ {
+ my ( $filepart, $dirpart ) = filenamesplit($filename,1);
+
+ print "Created $dirpart\n";
+ print $state->{CVSROOT} . "/$state->{module}/$filename\n";
+
+ # this is an "entries" line
+ my $kopts = kopts_from_path($filepart);
+ $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
+ print "/$filepart/1.$meta->{revision}//$kopts/\n";
+ # permissions
+ $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
+ print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
+ # transmit file
+ transmitfile($meta->{filehash});
+ }
+
+ next;
+ }
+
unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
{
print "E cvs add: nothing known about `$filename'\n";
$log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
- if ( @ARGV && $ARGV[0] eq 'pserver')
+ if ( $state->{method} eq 'pserver')
{
print "error 1 pserver access cannot commit\n";
exit;
bless $self, $class;
- $self->{dbdir} = $config . "/";
- die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} );
-
$self->{module} = $module;
- $self->{file} = $self->{dbdir} . "/gitcvs.$module.sqlite";
-
$self->{git_path} = $config . "/";
$self->{log} = $log;
die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
- $self->{dbh} = DBI->connect("dbi:SQLite:dbname=" . $self->{file},"","");
+ $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
+ $cfg->{gitcvs}{dbdriver} || "SQLite";
+ $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
+ $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
+ $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
+ $cfg->{gitcvs}{dbuser} || "";
+ $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
+ $cfg->{gitcvs}{dbpass} || "";
+ my %mapping = ( m => $module,
+ a => $state->{method},
+ u => getlogin || getpwuid($<) || $<,
+ G => $self->{git_path},
+ g => mangle_dirname($self->{git_path}),
+ );
+ $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
+ $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
+
+ die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
+ die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
+ $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
+ $self->{dbuser},
+ $self->{dbpass});
+ die "Error connecting to database\n" unless defined $self->{dbh};
$self->{tables} = {};
- foreach my $table ( $self->{dbh}->tables )
+ foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
{
- $table =~ s/^"//;
- $table =~ s/"$//;
$self->{tables}{$table} = 1;
}
return wantarray ? @output : join('',@output);
}
+=head2 mangle_dirname
+
+create a string from a directory name that is suitable to use as
+part of a filename, mainly by converting all chars except \w.- to _
+
+=cut
+sub mangle_dirname {
+ my $dirname = shift;
+ return unless defined $dirname;
+
+ $dirname =~ s/[^\w.-]/_/g;
+
+ return $dirname;
+}
1;