diff options
Diffstat (limited to 'git-cvsserver.perl')
| -rwxr-xr-x | git-cvsserver.perl | 500 | 
1 files changed, 442 insertions, 58 deletions
| diff --git a/git-cvsserver.perl b/git-cvsserver.perl index 29dbfc940b..920bbe15a3 100755 --- a/git-cvsserver.perl +++ b/git-cvsserver.perl @@ -21,6 +21,7 @@ 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); @@ -86,6 +87,17 @@ 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 = @@ -189,6 +201,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 @@ -487,7 +502,7 @@ sub req_add                  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";                  # permissions @@ -518,9 +533,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++;      } @@ -600,7 +632,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++; @@ -770,6 +802,7 @@ sub req_co      argsplit("co");      my $module = $state->{args}[0]; +    $state->{module} = $module;      my $checkout_path = $module;      # use the user specified directory if we're given it @@ -847,6 +880,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 './' @@ -877,7 +911,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"; @@ -1086,7 +1120,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"; @@ -1101,10 +1135,10 @@ 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}, { targetfile => $file_old }); @@ -1115,11 +1149,13 @@ sub req_update              $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"); @@ -1132,7 +1168,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";                  } @@ -1148,7 +1185,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";                  }              } @@ -1168,13 +1206,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 "/";          }      } @@ -1195,6 +1231,7 @@ sub req_ci      if ( $state->{method} eq 'pserver')      {          print "error 1 pserver access cannot commit\n"; +        cleanupWorkTree();          exit;      } @@ -1202,6 +1239,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;      } @@ -1209,31 +1247,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_WORK_TREE} = "."; -    $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 -    system("git-read-tree", $parenthash); -    unless ($? == 0) -    { -	die "Error running git-read-tree $state->{module} $file_index $!"; -    } -    $log->info("Created index '$file_index' 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; @@ -1271,7 +1298,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;          } @@ -1313,7 +1340,7 @@ sub req_ci      {          print "E No files to commit\n";          print "ok\n"; -        chdir "/"; +        cleanupWorkTree();          return;      } @@ -1336,7 +1363,7 @@ sub req_ci      {          $log->warn("Commit failed (Invalid commit hash)");          print "error 1 Commit failed (unknown reason)\n"; -        chdir "/"; +        cleanupWorkTree();          exit;      } @@ -1348,7 +1375,7 @@ sub req_ci  		{  			$log->warn("Commit failed (update hook declined to update ref)");  			print "error 1 Commit failed (update hook declined)\n"; -			chdir "/"; +			cleanupWorkTree();  			exit;  		}  	} @@ -1358,6 +1385,7 @@ sub req_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;  	} @@ -1409,12 +1437,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";  } @@ -1757,15 +1785,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_WORK_TREE} = "."; -    $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}} ) @@ -1789,10 +1811,10 @@ sub req_annotate  	system("git-read-tree", $lastseenin);  	unless ($? == 0)  	{ -	    print "E error running git-read-tree $lastseenin $file_index $!\n"; +	    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); @@ -1808,7 +1830,7 @@ sub req_annotate          # git-jsannotate telling us about commits we are hiding          # from the client. -        my $a_hints = "$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; @@ -1862,7 +1884,7 @@ sub req_annotate      }      # done; get out of the tempdir -    chdir "/"; +    cleanupWorkDir();      print "ok\n"; @@ -2115,26 +2137,388 @@ 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 | 
