diff options
Diffstat (limited to 'git-cvsserver.perl')
-rwxr-xr-x | git-cvsserver.perl | 1047 |
1 files changed, 883 insertions, 164 deletions
diff --git a/git-cvsserver.perl b/git-cvsserver.perl index 68aa75255..ab6cea3e5 100755 --- a/git-cvsserver.perl +++ b/git-cvsserver.perl @@ -21,7 +21,11 @@ use bytes; use Fcntl; use File::Temp qw/tempdir tempfile/; +use File::Path qw/rmtree/; use File::Basename; +use Getopt::Long qw(:config require_order no_ignore_case); + +my $VERSION = '@@GIT_VERSION@@'; my $log = GITCVS::log->new(); my $cfg; @@ -70,8 +74,9 @@ my $methods = { 'status' => \&req_status, 'admin' => \&req_CATCHALL, 'history' => \&req_CATCHALL, - 'watchers' => \&req_CATCHALL, - 'editors' => \&req_CATCHALL, + 'watchers' => \&req_EMPTY, + 'editors' => \&req_EMPTY, + 'noop' => \&req_EMPTY, 'annotate' => \&req_annotate, 'Global_option' => \&req_Globaloption, #'annotate' => \&req_CATCHALL, @@ -83,33 +88,93 @@ my $methods = { # $state holds all the bits of information the clients sends us that could # potentially be useful when it comes to actually _doing_ something. my $state = { prependdir => '' }; + +# Work is for managing temporary working directory +my $work = + { + state => undef, # undef, 1 (empty), 2 (with stuff) + workDir => undef, + index => undef, + emptyDir => undef, + tmpDir => undef + }; + $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 (req_Root('root', $line)) { # reuse Root + print "E Invalid root $line \n"; + exit 1; + } $line = <STDIN>; chomp $line; 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... } @@ -137,6 +202,9 @@ while (<STDIN>) $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]); $log->info("--------------- FINISH -----------------"); +chdir '/'; +exit 0; + # Magic catchall method. # This is the method that will handle all commands we haven't yet # implemented. It simply sends a warning to the log file indicating a @@ -147,6 +215,11 @@ sub req_CATCHALL $log->warn("Unhandled command : req_$cmd : $data"); } +# This method invariably succeeds with an empty response. +sub req_EMPTY +{ + print "ok\n"; +} # Root pathname \n # Response expected: no. Tell the server which CVSROOT to use. Note that @@ -162,13 +235,53 @@ sub req_Root 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; } @@ -181,12 +294,18 @@ sub req_Root } 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"; @@ -194,9 +313,10 @@ sub req_Root 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(); } @@ -350,12 +470,52 @@ sub req_add 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($filename,"sha1",$meta->{filehash}); + $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"; @@ -374,9 +534,26 @@ sub req_add print "Checked-in $dirpart\n"; print "$filename\n"; - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path($filename,"file", + $state->{entries}{$filename}{modified_filename}); print "/$filepart/0//$kopts/\n"; + my $requestedKopts = $state->{opt}{k}; + if(defined($requestedKopts)) + { + $requestedKopts = "-k$requestedKopts"; + } + else + { + $requestedKopts = ""; + } + if( $kopts ne $requestedKopts ) + { + $log->warn("Ignoring requested -k='$requestedKopts'" + . " for '$filename'; detected -k='$kopts' instead"); + #TODO: Also have option to send warning to user? + } + $addcount++; } @@ -456,7 +633,7 @@ sub req_remove print "Checked-in $dirpart\n"; print "$filename\n"; - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash}); print "/$filepart/-1.$wrev//$kopts/\n"; $rmcount++; @@ -486,8 +663,12 @@ sub req_Modified my ( $cmd, $data ) = @_; my $mode = <STDIN>; + defined $mode + or (print "E end of file reading mode for $data\n"), return; chomp $mode; my $size = <STDIN>; + defined $size + or (print "E end of file reading size of $data\n"), return; chomp $size; # Grab config information @@ -507,7 +688,8 @@ sub req_Modified $bytesleft -= $blocksize; } - close $fh; + close $fh + or (print "E failed to write temporary, $filename: $!\n"), return; # Ensure we have something sensible for the file mode if ( $mode =~ /u=(\w+)/ ) @@ -620,7 +802,20 @@ sub req_co argsplit("co"); + # Provide list of modules, if -c was used. + if (exists $state->{opt}{c}) { + my $showref = `git show-ref --heads`; + for my $line (split '\n', $showref) { + if ( $line =~ m% refs/heads/(.*)$% ) { + print "M $1\t$1\n"; + } + } + print "ok\n"; + return 1; + } + my $module = $state->{args}[0]; + $state->{module} = $module; my $checkout_path = $module; # use the user specified directory if we're given it @@ -698,6 +893,7 @@ sub req_co # Don't want to check out deleted files next if ( $git->{filehash} eq "deleted" ); + my $fullName = $git->{name}; ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name}); if (length($git->{dir}) && $git->{dir} ne './' @@ -728,7 +924,7 @@ sub req_co print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n"; # this is an "entries" line - my $kopts = kopts_from_path($git->{name}); + my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash}); print "/$git->{name}/1.$git->{revision}//$kopts/\n"; # permissions print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; @@ -764,16 +960,15 @@ sub req_update # projects (heads in this case) to checkout. # if ($state->{module} eq '') { + my $showref = `git show-ref --heads`; print "E cvs update: Updating .\n"; - opendir HEADS, $state->{CVSROOT} . '/refs/heads'; - while (my $head = readdir(HEADS)) { - if (-f $state->{CVSROOT} . '/refs/heads/' . $head) { - print "E cvs update: New directory `$head'\n"; - } - } - closedir HEADS; - print "ok\n"; - return 1; + for my $line (split '\n', $showref) { + if ( $line =~ m% refs/heads/(.*)$% ) { + print "E cvs update: New directory `$1'\n"; + } + } + print "ok\n"; + return 1; } @@ -809,6 +1004,17 @@ sub req_update $meta = $updater->getmeta($filename); } + # If -p was given, "print" the contents of the requested revision. + if ( exists ( $state->{opt}{p} ) ) { + if ( defined ( $meta->{revision} ) ) { + $log->info("Printing '$filename' revision " . $meta->{revision}); + + transmitfile($meta->{filehash}, { print => 1 }); + } + + next; + } + if ( ! defined $meta ) { $meta = { @@ -843,6 +1049,7 @@ sub req_update if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} + and defined($state->{entries}{$filename}{modified_hash}) and not exists ( $state->{opt}{C} ) ) { $log->info("Tell the client the file is modified"); @@ -920,7 +1127,7 @@ sub req_update print $state->{CVSROOT} . "/$state->{module}/$filename\n"; # this is an "entries" line - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash}); $log->debug("/$filepart/1.$meta->{revision}//$kopts/"); print "/$filepart/1.$meta->{revision}//$kopts/\n"; @@ -935,25 +1142,27 @@ sub req_update $log->info("Updating '$filename'"); my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1); - my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/"; + my $mergeDir = setupTmpDir(); - chdir $dir; my $file_local = $filepart . ".mine"; + my $mergedFile = "$mergeDir/$file_local"; system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local); my $file_old = $filepart . "." . $oldmeta->{revision}; - transmitfile($oldmeta->{filehash}, $file_old); + transmitfile($oldmeta->{filehash}, { targetfile => $file_old }); my $file_new = $filepart . "." . $meta->{revision}; - transmitfile($meta->{filehash}, $file_new); + transmitfile($meta->{filehash}, { targetfile => $file_new }); # we need to merge with the local changes ( M=successful merge, C=conflict merge ) $log->info("Merging $file_local, $file_old, $file_new"); print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n"; - $log->debug("Temporary directory for merge is $dir"); + $log->debug("Temporary directory for merge is $mergeDir"); my $return = system("git", "merge-file", $file_local, $file_old, $file_new); $return >>= 8; + cleanupTmpDir(); + if ( $return == 0 ) { $log->info("Merged successfully"); @@ -966,7 +1175,8 @@ sub req_update print "Merged $dirpart\n"; $log->debug($state->{CVSROOT} . "/$state->{module}/$filename"); print $state->{CVSROOT} . "/$state->{module}/$filename\n"; - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path("$dirpart/$filepart", + "file",$mergedFile); $log->debug("/$filepart/1.$meta->{revision}//$kopts/"); print "/$filepart/1.$meta->{revision}//$kopts/\n"; } @@ -982,7 +1192,8 @@ sub req_update { print "Merged $dirpart\n"; print $state->{CVSROOT} . "/$state->{module}/$filename\n"; - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path("$dirpart/$filepart", + "file",$mergedFile); print "/$filepart/1.$meta->{revision}/+/$kopts/\n"; } } @@ -1002,13 +1213,11 @@ sub req_update # transmit file, format is single integer on a line by itself (file # size) followed by the file contents # TODO : we should copy files in blocks - my $data = `cat $file_local`; + my $data = `cat $mergedFile`; $log->debug("File size : " . length($data)); print length($data) . "\n"; print $data; } - - chdir "/"; } } @@ -1026,9 +1235,10 @@ sub req_ci $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"; + cleanupWorkTree(); exit; } @@ -1036,6 +1246,7 @@ sub req_ci { $log->warn("file 'index' already exists in the git repository"); print "error 1 Index already exists in git repo\n"; + cleanupWorkTree(); exit; } @@ -1043,30 +1254,20 @@ sub req_ci my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); $updater->update(); - my $tmpdir = tempdir ( DIR => $TEMP_DIR ); - my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); - $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'"); - - $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; - $ENV{GIT_INDEX_FILE} = $file_index; - # Remember where the head was at the beginning. my $parenthash = `git show-ref -s refs/heads/$state->{module}`; chomp $parenthash; if ($parenthash !~ /^[0-9a-f]{40}$/) { print "error 1 pserver cannot find the current HEAD of module"; + cleanupWorkTree(); exit; } - chdir $tmpdir; + setupWorkTree($parenthash); - # populate the temporary index based - system("git-read-tree", $parenthash); - unless ($? == 0) - { - die "Error running git-read-tree $state->{module} $file_index $!"; - } - $log->info("Created index '$file_index' with for head $state->{module} - exit status $?"); + $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'"); + + $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?"); my @committedfiles = (); my %oldmeta; @@ -1086,7 +1287,7 @@ sub req_ci my ( $filepart, $dirpart ) = filenamesplit($filename); - # do a checkout of the file if it part of this tree + # do a checkout of the file if it is part of this tree if ($wrev) { system('git-checkout-index', '-f', '-u', $filename); unless ($? == 0) { @@ -1104,7 +1305,7 @@ sub req_ci { # fail everything if an up to date check fails print "error 1 Up to date check failed for $filename\n"; - chdir "/"; + cleanupWorkTree(); exit; } @@ -1146,7 +1347,7 @@ sub req_ci { print "E No files to commit\n"; print "ok\n"; - chdir "/"; + cleanupWorkTree(); return; } @@ -1158,7 +1359,13 @@ sub req_ci # write our commit message out if we have one ... my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR ); print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) ); - print $msg_fh "\n\nvia git-CVS emulator\n"; + if ( defined ( $cfg->{gitcvs}{commitmsgannotation} ) ) { + if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/ ) { + print $msg_fh "\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n" + } + } else { + print $msg_fh "\n\nvia git-CVS emulator\n"; + } close $msg_fh; my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`; @@ -1169,32 +1376,52 @@ sub req_ci { $log->warn("Commit failed (Invalid commit hash)"); print "error 1 Commit failed (unknown reason)\n"; - chdir "/"; + cleanupWorkTree(); exit; } - # Check that this is allowed, just as we would with a receive-pack - my @cmd = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}", + ### Emulate git-receive-pack by running hooks/update + my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}", $parenthash, $commithash ); - if( -x $cmd[0] ) { - unless( system( @cmd ) == 0 ) + if( -x $hook[0] ) { + unless( system( @hook ) == 0 ) { $log->warn("Commit failed (update hook declined to update ref)"); print "error 1 Commit failed (update hook declined)\n"; - chdir "/"; + cleanupWorkTree(); exit; } } + ### Update the ref if (system(qw(git update-ref -m), "cvsserver ci", "refs/heads/$state->{module}", $commithash, $parenthash)) { $log->warn("update-ref for $state->{module} failed."); print "error 1 Cannot commit -- update first\n"; + cleanupWorkTree(); exit; } + ### Emulate git-receive-pack by running hooks/post-receive + my $hook = $ENV{GIT_DIR}.'hooks/post-receive'; + if( -x $hook ) { + open(my $pipe, "| $hook") || die "can't fork $!"; + + local $SIG{PIPE} = sub { die 'pipe broke' }; + + print $pipe "$parenthash $commithash refs/heads/$state->{module}\n"; + + close $pipe || die "bad pipe: $! $?"; + } + $updater->update(); + ### Then hooks/post-update + $hook = $ENV{GIT_DIR}.'hooks/post-update'; + if (-x $hook) { + system($hook, "refs/heads/$state->{module}"); + } + # foreach file specified on the command line ... foreach my $filename ( @committedfiles ) { @@ -1223,12 +1450,12 @@ sub req_ci } print "Checked-in $dirpart\n"; print "$filename\n"; - my $kopts = kopts_from_path($filepart); + my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash}); print "/$filepart/1.$meta->{revision}//$kopts/\n"; } } - chdir "/"; + cleanupWorkTree(); print "ok\n"; } @@ -1253,6 +1480,8 @@ sub req_status { $filename = filecleanup($filename); + next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0; + my $meta = $updater->getmeta($filename); my $oldmeta = $meta; @@ -1296,8 +1525,10 @@ sub req_status $status ||= "Unknown"; + my ($filepart) = filenamesplit($filename); + print "M ===================================================================\n"; - print "M File: $filename\tStatus: $status\n"; + print "M File: $filepart\tStatus: $status\n"; if ( defined($state->{entries}{$filename}{revision}) ) { print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n"; @@ -1371,14 +1602,14 @@ sub req_diff print "E File $filename at revision 1.$revision1 doesn't exist\n"; next; } - transmitfile($meta1->{filehash}, $file1); + transmitfile($meta1->{filehash}, { targetfile => $file1 }); } # otherwise we just use the working copy revision else { ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); $meta1 = $updater->getmeta($filename, $wrev); - transmitfile($meta1->{filehash}, $file1); + transmitfile($meta1->{filehash}, { targetfile => $file1 }); } # if we have a second -r switch, use it too @@ -1393,7 +1624,7 @@ sub req_diff next; } - transmitfile($meta2->{filehash}, $file2); + transmitfile($meta2->{filehash}, { targetfile => $file2 }); } # otherwise we just use the working copy else @@ -1406,7 +1637,7 @@ sub req_diff { ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); $meta2 = $updater->getmeta($filename, $wrev); - transmitfile($meta2->{filehash}, $file2); + transmitfile($meta2->{filehash}, { targetfile => $file2 }); } # We need to have retrieved something useful @@ -1538,8 +1769,7 @@ sub req_log print "M revision 1.$revision->{revision}\n"; # reformat the date for log output $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) ); - $revision->{author} =~ s/\s+.*//; - $revision->{author} =~ s/^(.{8}).*/$1/; + $revision->{author} = cvs_author($revision->{author}); print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n"; my $commitmessage = $updater->commitmessage($revision->{commithash}); $commitmessage =~ s/^/M /mg; @@ -1568,14 +1798,9 @@ sub req_annotate argsfromdir($updater); # we'll need a temporary checkout dir - my $tmpdir = tempdir ( DIR => $TEMP_DIR ); - my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); - $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'"); - - $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; - $ENV{GIT_INDEX_FILE} = $file_index; + setupWorkTree(); - chdir $tmpdir; + $log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'"); # foreach file specified on the command line ... foreach my $filename ( @{$state->{args}} ) @@ -1599,14 +1824,16 @@ sub req_annotate system("git-read-tree", $lastseenin); unless ($? == 0) { - die "Error running git-read-tree $lastseenin $file_index $!"; + print "E error running git-read-tree $lastseenin $ENV{GIT_INDEX_FILE} $!\n"; + return; } - $log->info("Created index '$file_index' with commit $lastseenin - exit status $?"); + $log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit $lastseenin - exit status $?"); # do a checkout of the file system('git-checkout-index', '-f', '-u', $filename); unless ($? == 0) { - die "Error running git-checkout-index -f -u $filename : $!"; + print "E error running git-checkout-index -f -u $filename : $!\n"; + return; } $log->info("Annotate $filename"); @@ -1616,7 +1843,11 @@ sub req_annotate # git-jsannotate telling us about commits we are hiding # from the client. - open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!"; + my $a_hints = "$work->{workDir}/.annotate_hints"; + if (!open(ANNOTATEHINTS, '>', $a_hints)) { + print "E failed to open '$a_hints' for writing: $!\n"; + return; + } for (my $i=0; $i < @$revisions; $i++) { print ANNOTATEHINTS $revisions->[$i][2]; @@ -1627,11 +1858,14 @@ sub req_annotate } print ANNOTATEHINTS "\n"; - close ANNOTATEHINTS; + close ANNOTATEHINTS + or (print "E failed to write $a_hints: $!\n"), return; - my $annotatecmd = 'git-annotate'; - open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename) - or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!"; + my @cmd = (qw(git-annotate -l -S), $a_hints, $filename); + if (!open(ANNOTATE, "-|", @cmd)) { + print "E error invoking ". join(' ',@cmd) .": $!\n"; + return; + } my $metadata = {}; print "E Annotations for $filename\n"; print "E ***************\n"; @@ -1644,8 +1878,7 @@ sub req_annotate unless ( defined ( $metadata->{$commithash} ) ) { $metadata->{$commithash} = $updater->getmeta($filename, $commithash); - $metadata->{$commithash}{author} =~ s/\s+.*//; - $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/; + $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author}); $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ ); } printf("M 1.%-5d (%-8s %10s): %s\n", @@ -1664,7 +1897,7 @@ sub req_annotate } # done; get out of the tempdir - chdir "/"; + cleanupWorkTree(); print "ok\n"; @@ -1675,14 +1908,14 @@ sub req_annotate # the second is $state->{files} which is everything after it. sub argsplit { - return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" ); - - my $type = shift; - $state->{args} = []; $state->{files} = []; $state->{opt} = {}; + return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" ); + + my $type = shift; + if ( defined($type) ) { my $opt = {}; @@ -1825,14 +2058,17 @@ sub revparse return undef; } -# This method takes a file hash and does a CVS "file transfer" which transmits the -# size of the file, and then the file contents. -# If a second argument $targetfile is given, the file is instead written out to -# a file by the name of $targetfile +# This method takes a file hash and does a CVS "file transfer". Its +# exact behaviour depends on a second, optional hash table argument: +# - If $options->{targetfile}, dump the contents to that file; +# - If $options->{print}, use M/MT to transmit the contents one line +# at a time; +# - Otherwise, transmit the size of the file, followed by the file +# contents. sub transmitfile { my $filehash = shift; - my $targetfile = shift; + my $options = shift; if ( defined ( $filehash ) and $filehash eq "deleted" ) { @@ -1854,16 +2090,25 @@ sub transmitfile if ( open my $fh, '-|', "git-cat-file", "blob", $filehash ) { - if ( defined ( $targetfile ) ) + if ( defined ( $options->{targetfile} ) ) { + my $targetfile = $options->{targetfile}; open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!"); print NEWFILE $_ while ( <$fh> ); - close NEWFILE; + close NEWFILE or die("Failed to write '$targetfile': $!"); + } elsif ( defined ( $options->{print} ) && $options->{print} ) { + while ( <$fh> ) { + if( /\n\z/ ) { + print 'M ', $_; + } else { + print 'MT text ', $_, "\n"; + } + } } else { print "$size\n"; print while ( <$fh> ); } - close $fh or die ("Couldn't close filehandle for transmitfile()"); + close $fh or die ("Couldn't close filehandle for transmitfile(): $!"); } else { die("Couldn't execute git-cat-file"); } @@ -1905,26 +2150,404 @@ sub filecleanup return $filename; } +sub validateGitDir +{ + if( !defined($state->{CVSROOT}) ) + { + print "error 1 CVSROOT not specified\n"; + cleanupWorkTree(); + exit; + } + if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') ) + { + print "error 1 Internally inconsistent CVSROOT\n"; + cleanupWorkTree(); + exit; + } +} + +# Setup working directory in a work tree with the requested version +# loaded in the index. +sub setupWorkTree +{ + my ($ver) = @_; + + validateGitDir(); + + if( ( defined($work->{state}) && $work->{state} != 1 ) || + defined($work->{tmpDir}) ) + { + $log->warn("Bad work tree state management"); + print "error 1 Internal setup multiple work trees without cleanup\n"; + cleanupWorkTree(); + exit; + } + + $work->{workDir} = tempdir ( DIR => $TEMP_DIR ); + + if( !defined($work->{index}) ) + { + (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); + } + + chdir $work->{workDir} or + die "Unable to chdir to $work->{workDir}\n"; + + $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'"); + + $ENV{GIT_WORK_TREE} = "."; + $ENV{GIT_INDEX_FILE} = $work->{index}; + $work->{state} = 2; + + if($ver) + { + system("git","read-tree",$ver); + unless ($? == 0) + { + $log->warn("Error running git-read-tree"); + die "Error running git-read-tree $ver in $work->{workDir} $!\n"; + } + } + # else # req_annotate reads tree for each file +} + +# Ensure current directory is in some kind of working directory, +# with a recent version loaded in the index. +sub ensureWorkTree +{ + if( defined($work->{tmpDir}) ) + { + $log->warn("Bad work tree state management [ensureWorkTree()]"); + print "error 1 Internal setup multiple dirs without cleanup\n"; + cleanupWorkTree(); + exit; + } + if( $work->{state} ) + { + return; + } + + validateGitDir(); + + if( !defined($work->{emptyDir}) ) + { + $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0); + } + chdir $work->{emptyDir} or + die "Unable to chdir to $work->{emptyDir}\n"; + + my $ver = `git show-ref -s refs/heads/$state->{module}`; + chomp $ver; + if ($ver !~ /^[0-9a-f]{40}$/) + { + $log->warn("Error from git show-ref -s refs/head$state->{module}"); + print "error 1 cannot find the current HEAD of module"; + cleanupWorkTree(); + exit; + } + + if( !defined($work->{index}) ) + { + (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); + } + + $ENV{GIT_WORK_TREE} = "."; + $ENV{GIT_INDEX_FILE} = $work->{index}; + $work->{state} = 1; + + system("git","read-tree",$ver); + unless ($? == 0) + { + die "Error running git-read-tree $ver $!\n"; + } +} + +# Cleanup working directory that is not needed any longer. +sub cleanupWorkTree +{ + if( ! $work->{state} ) + { + return; + } + + chdir "/" or die "Unable to chdir '/'\n"; + + if( defined($work->{workDir}) ) + { + rmtree( $work->{workDir} ); + undef $work->{workDir}; + } + undef $work->{state}; +} + +# Setup a temporary directory (not a working tree), typically for +# merging dirty state as in req_update. +sub setupTmpDir +{ + $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR ); + chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n"; + + return $work->{tmpDir}; +} + +# Clean up a previously setupTmpDir. Restore previous work tree if +# appropriate. +sub cleanupTmpDir +{ + if ( !defined($work->{tmpDir}) ) + { + $log->warn("cleanup tmpdir that has not been setup"); + die "Cleanup tmpDir that has not been setup\n"; + } + if( defined($work->{state}) ) + { + if( $work->{state} == 1 ) + { + chdir $work->{emptyDir} or + die "Unable to chdir to $work->{emptyDir}\n"; + } + elsif( $work->{state} == 2 ) + { + chdir $work->{workDir} or + die "Unable to chdir to $work->{emptyDir}\n"; + } + else + { + $log->warn("Inconsistent work dir state"); + die "Inconsistent work dir state\n"; + } + } + else + { + chdir "/" or die "Unable to chdir '/'\n"; + } +} + # Given a path, this function returns a string containing the kopts # that should go into that path's Entries line. For example, a binary # file should get -kb. sub kopts_from_path { - my ($path) = @_; + my ($path, $srcType, $name) = @_; - # Once it exists, the git attributes system should be used to look up - # what attributes apply to this path. + if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and + $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i ) + { + my ($val) = check_attr( "crlf", $path ); + if ( $val eq "set" ) + { + return ""; + } + elsif ( $val eq "unset" ) + { + return "-kb" + } + else + { + $log->info("Unrecognized check_attr crlf $path : $val"); + } + } - # Until then, take the setting from the config file - unless ( defined ( $cfg->{gitcvs}{allbinary} ) and $cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i ) + if ( defined ( $cfg->{gitcvs}{allbinary} ) ) { - # Return "" to give no special treatment to any path - return ""; - } else { - # Alternatively, to have all files treated as if they are binary (which - # is more like git itself), always return the "-kb" option - return "-kb"; + if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) ) + { + return "-kb"; + } + elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) ) + { + if( $srcType eq "sha1Or-k" && + !defined($name) ) + { + my ($ret)=$state->{entries}{$path}{options}; + if( !defined($ret) ) + { + $ret=$state->{opt}{k}; + if(defined($ret)) + { + $ret="-k$ret"; + } + else + { + $ret=""; + } + } + if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) ) + { + print "E Bad -k option\n"; + $log->warn("Bad -k option: $ret"); + die "Error: Bad -k option: $ret\n"; + } + + return $ret; + } + else + { + if( is_binary($srcType,$name) ) + { + $log->debug("... as binary"); + return "-kb"; + } + else + { + $log->debug("... as text"); + } + } + } } + # Return "" to give no special treatment to any path + return ""; +} + +sub check_attr +{ + my ($attr,$path) = @_; + ensureWorkTree(); + if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path ) + { + my $val = <$fh>; + close $fh; + $val =~ s/.*: ([^:\r\n]*)\s*$/$1/; + return $val; + } + else + { + return undef; + } +} + +# This should have the same heuristics as convert.c:is_binary() and related. +# Note that the bare CR test is done by callers in convert.c. +sub is_binary +{ + my ($srcType,$name) = @_; + $log->debug("is_binary($srcType,$name)"); + + # Minimize amount of interpreted code run in the inner per-character + # loop for large files, by totalling each character value and + # then analyzing the totals. + my @counts; + my $i; + for($i=0;$i<256;$i++) + { + $counts[$i]=0; + } + + my $fh = open_blob_or_die($srcType,$name); + my $line; + while( defined($line=<$fh>) ) + { + # Any '\0' and bare CR are considered binary. + if( $line =~ /\0|(\r[^\n])/ ) + { + close($fh); + return 1; + } + + # Count up each character in the line: + my $len=length($line); + for($i=0;$i<$len;$i++) + { + $counts[ord(substr($line,$i,1))]++; + } + } + close $fh; + + # Don't count CR and LF as either printable/nonprintable + $counts[ord("\n")]=0; + $counts[ord("\r")]=0; + + # Categorize individual character count into printable and nonprintable: + my $printable=0; + my $nonprintable=0; + for($i=0;$i<256;$i++) + { + if( $i < 32 && + $i != ord("\b") && + $i != ord("\t") && + $i != 033 && # ESC + $i != 014 ) # FF + { + $nonprintable+=$counts[$i]; + } + elsif( $i==127 ) # DEL + { + $nonprintable+=$counts[$i]; + } + else + { + $printable+=$counts[$i]; + } + } + + return ($printable >> 7) < $nonprintable; +} + +# Returns open file handle. Possible invocations: +# - open_blob_or_die("file",$filename); +# - open_blob_or_die("sha1",$filehash); +sub open_blob_or_die +{ + my ($srcType,$name) = @_; + my ($fh); + if( $srcType eq "file" ) + { + if( !open $fh,"<",$name ) + { + $log->warn("Unable to open file $name: $!"); + die "Unable to open file $name: $!\n"; + } + } + elsif( $srcType eq "sha1" || $srcType eq "sha1Or-k" ) + { + unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ ) + { + $log->warn("Need filehash"); + die "Need filehash\n"; + } + + my $type = `git cat-file -t $name`; + chomp $type; + + unless ( defined ( $type ) and $type eq "blob" ) + { + $log->warn("Invalid type '$type' for '$name'"); + die ( "Invalid type '$type' (expected 'blob')" ) + } + + my $size = `git cat-file -s $name`; + chomp $size; + + $log->debug("open_blob_or_die($name) size=$size, type=$type"); + + unless( open $fh, '-|', "git", "cat-file", "blob", $name ) + { + $log->warn("Unable to open sha1 $name"); + die "Unable to open sha1 $name\n"; + } + } + else + { + $log->warn("Unknown type of blob source: $srcType"); + die "Unknown type of blob source: $srcType\n"; + } + return $fh; +} + +# Generate a CVS author name from Git author information, by taking the local +# part of the email address and replacing characters not in the Portable +# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS +# Login names are Unix login names, which should be restricted to this +# character set. +sub cvs_author +{ + my $author_line = shift; + (my $author) = $author_line =~ /<([^@>]*)/; + + $author =~ s/[^-a-zA-Z0-9_.]/_/g; + $author =~ s/^-/_/; + + $author; } package GITCVS::log; @@ -2131,33 +2754,63 @@ sub new bless $self, $class; - $self->{dbdir} = $config . "/"; - die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} ); + $self->{valid_tables} = {'revision' => 1, + 'revision_ix1' => 1, + 'revision_ix2' => 1, + 'head' => 1, + 'head_ix1' => 1, + 'properties' => 1, + 'commitmsgs' => 1}; $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} || ""; + $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} || + $cfg->{gitcvs}{dbtablenameprefix} || ""; + 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; + $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg; + $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix}); + + 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; } # Construct the revision table if required - unless ( $self->{tables}{revision} ) + unless ( $self->{tables}{$self->tablename("revision")} ) { + my $tablename = $self->tablename("revision"); + my $ix1name = $self->tablename("revision_ix1"); + my $ix2name = $self->tablename("revision_ix2"); $self->{dbh}->do(" - CREATE TABLE revision ( + CREATE TABLE $tablename ( name TEXT NOT NULL, revision INTEGER NOT NULL, filehash TEXT NOT NULL, @@ -2168,20 +2821,22 @@ sub new ) "); $self->{dbh}->do(" - CREATE INDEX revision_ix1 - ON revision (name,revision) + CREATE INDEX $ix1name + ON $tablename (name,revision) "); $self->{dbh}->do(" - CREATE INDEX revision_ix2 - ON revision (name,commithash) + CREATE INDEX $ix2name + ON $tablename (name,commithash) "); } # Construct the head table if required - unless ( $self->{tables}{head} ) + unless ( $self->{tables}{$self->tablename("head")} ) { + my $tablename = $self->tablename("head"); + my $ix1name = $self->tablename("head_ix1"); $self->{dbh}->do(" - CREATE TABLE head ( + CREATE TABLE $tablename ( name TEXT NOT NULL, revision INTEGER NOT NULL, filehash TEXT NOT NULL, @@ -2192,16 +2847,17 @@ sub new ) "); $self->{dbh}->do(" - CREATE INDEX head_ix1 - ON head (name) + CREATE INDEX $ix1name + ON $tablename (name) "); } # Construct the properties table if required - unless ( $self->{tables}{properties} ) + unless ( $self->{tables}{$self->tablename("properties")} ) { + my $tablename = $self->tablename("properties"); $self->{dbh}->do(" - CREATE TABLE properties ( + CREATE TABLE $tablename ( key TEXT NOT NULL PRIMARY KEY, value TEXT ) @@ -2209,10 +2865,11 @@ sub new } # Construct the commitmsgs table if required - unless ( $self->{tables}{commitmsgs} ) + unless ( $self->{tables}{$self->tablename("commitmsgs")} ) { + my $tablename = $self->tablename("commitmsgs"); $self->{dbh}->do(" - CREATE TABLE commitmsgs ( + CREATE TABLE $tablename ( key TEXT NOT NULL PRIMARY KEY, value TEXT ) @@ -2222,6 +2879,21 @@ sub new return $self; } +=head2 tablename + +=cut +sub tablename +{ + my $self = shift; + my $name = shift; + + if (exists $self->{valid_tables}{$name}) { + return $self->{dbtablenameprefix} . $name; + } else { + return undef; + } +} + =head2 update =cut @@ -2348,17 +3020,21 @@ sub update if ($parent eq $lastpicked) { next; } - open my $p, 'git-merge-base '. $lastpicked . ' ' - . $parent . '|'; - my @output = (<$p>); - close $p; - my $base = join('', @output); + my $base = eval { + safe_pipe_capture('git-merge-base', + $lastpicked, $parent); + }; + # The two branches may not be related at all, + # in which case merge base simply fails to find + # any, but that's Ok. + next if ($@); + chomp $base; if ($base) { my @merged; # print "want to log between $base $parent \n"; - open(GITLOG, '-|', 'git-log', "$base..$parent") - or die "Cannot call git-log: $!"; + open(GITLOG, '-|', 'git-log', '--pretty=medium', "$base..$parent") + or die "Cannot call git-log: $!"; my $mergedhash; while (<GITLOG>) { chomp; @@ -2434,7 +3110,7 @@ sub update }; $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms); } - elsif ( $change eq "M" ) + elsif ( $change eq "M" || $change eq "T" ) { #$log->debug("MODIFIED $name"); $head->{$name} = { @@ -2453,7 +3129,7 @@ sub update #$log->debug("ADDED $name"); $head->{$name} = { name => $name, - revision => 1, + revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1, filehash => $hash, commithash => $commit->{hash}, modified => $commit->{date}, @@ -2583,8 +3259,9 @@ sub insert_rev my $modified = shift; my $author = shift; my $mode = shift; + my $tablename = $self->tablename("revision"); - my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); + my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode); } @@ -2593,16 +3270,18 @@ sub insert_mergelog my $self = shift; my $key = shift; my $value = shift; + my $tablename = $self->tablename("commitmsgs"); - my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1); + my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1); $insert_mergelog->execute($key, $value); } sub delete_head { my $self = shift; + my $tablename = $self->tablename("head"); - my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1); + my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1); $delete_head->execute(); } @@ -2616,8 +3295,9 @@ sub insert_head my $modified = shift; my $author = shift; my $mode = shift; + my $tablename = $self->tablename("head"); - my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); + my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode); } @@ -2625,8 +3305,9 @@ sub _headrev { my $self = shift; my $filename = shift; + my $tablename = $self->tablename("head"); - my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1); + my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1); $db_query->execute($filename); my ( $hash, $revision, $mode ) = $db_query->fetchrow_array; @@ -2637,8 +3318,9 @@ sub _get_prop { my $self = shift; my $key = shift; + my $tablename = $self->tablename("properties"); - my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1); + my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1); $db_query->execute($key); my ( $value ) = $db_query->fetchrow_array; @@ -2650,13 +3332,14 @@ sub _set_prop my $self = shift; my $key = shift; my $value = shift; + my $tablename = $self->tablename("properties"); - my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1); + my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1); $db_query->execute($value, $key); unless ( $db_query->rows ) { - $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1); + $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1); $db_query->execute($key, $value); } @@ -2670,10 +3353,11 @@ sub _set_prop sub gethead { my $self = shift; + my $tablename = $self->tablename("head"); return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) ); - my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1); + my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1); $db_query->execute(); my $tree = []; @@ -2695,8 +3379,9 @@ sub getlog { my $self = shift; my $filename = shift; + my $tablename = $self->tablename("revision"); - my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1); + my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1); $db_query->execute($filename); my $tree = []; @@ -2720,19 +3405,21 @@ sub getmeta my $self = shift; my $filename = shift; my $revision = shift; + my $tablename_rev = $self->tablename("revision"); + my $tablename_head = $self->tablename("head"); my $db_query; if ( defined($revision) and $revision =~ /^\d+$/ ) { - $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1); $db_query->execute($filename, $revision); } elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ ) { - $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1); $db_query->execute($filename, $revision); } else { - $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1); $db_query->execute($filename); } @@ -2748,11 +3435,12 @@ sub commitmessage { my $self = shift; my $commithash = shift; + my $tablename = $self->tablename("commitmsgs"); die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ ); my $db_query; - $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1); $db_query->execute($commithash); my ( $message ) = $db_query->fetchrow_array; @@ -2780,9 +3468,10 @@ sub gethistory { my $self = shift; my $filename = shift; + my $tablename = $self->tablename("revision"); my $db_query; - $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1); $db_query->execute($filename); return $db_query->fetchall_arrayref; @@ -2802,9 +3491,10 @@ sub gethistorydense { my $self = shift; my $filename = shift; + my $tablename = $self->tablename("revision"); my $db_query; - $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1); + $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1); $db_query->execute($filename); return $db_query->fetchall_arrayref; @@ -2847,5 +3537,34 @@ sub safe_pipe_capture { 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; +} + +=head2 mangle_tablename + +create a string from a that is suitable to use as part of an SQL table +name, mainly by converting all chars except \w to _ + +=cut +sub mangle_tablename { + my $tablename = shift; + return unless defined $tablename; + + $tablename =~ s/[^\w_]/_/g; + + return $tablename; +} 1; |