709741920f19a057a352d29a5b24c1ac6f7ed2c1
   1#!/usr/bin/perl
   2
   3####
   4#### This application is a CVS emulation layer for git.
   5#### It is intended for clients to connect over SSH.
   6#### See the documentation for more details.
   7####
   8#### Copyright The Open University UK - 2006.
   9####
  10#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
  11####          Martin Langhoff <martin@catalyst.net.nz>
  12####
  13####
  14#### Released under the GNU Public License, version 2.
  15####
  16####
  17
  18use strict;
  19use warnings;
  20use bytes;
  21
  22use Fcntl;
  23use File::Temp qw/tempdir tempfile/;
  24use File::Path qw/rmtree/;
  25use File::Basename;
  26use Getopt::Long qw(:config require_order no_ignore_case);
  27
  28my $VERSION = '@@GIT_VERSION@@';
  29
  30my $log = GITCVS::log->new();
  31my $cfg;
  32
  33my $DATE_LIST = {
  34    Jan => "01",
  35    Feb => "02",
  36    Mar => "03",
  37    Apr => "04",
  38    May => "05",
  39    Jun => "06",
  40    Jul => "07",
  41    Aug => "08",
  42    Sep => "09",
  43    Oct => "10",
  44    Nov => "11",
  45    Dec => "12",
  46};
  47
  48# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
  49$| = 1;
  50
  51#### Definition and mappings of functions ####
  52
  53my $methods = {
  54    'Root'            => \&req_Root,
  55    'Valid-responses' => \&req_Validresponses,
  56    'valid-requests'  => \&req_validrequests,
  57    'Directory'       => \&req_Directory,
  58    'Entry'           => \&req_Entry,
  59    'Modified'        => \&req_Modified,
  60    'Unchanged'       => \&req_Unchanged,
  61    'Questionable'    => \&req_Questionable,
  62    'Argument'        => \&req_Argument,
  63    'Argumentx'       => \&req_Argument,
  64    'expand-modules'  => \&req_expandmodules,
  65    'add'             => \&req_add,
  66    'remove'          => \&req_remove,
  67    'co'              => \&req_co,
  68    'update'          => \&req_update,
  69    'ci'              => \&req_ci,
  70    'diff'            => \&req_diff,
  71    'log'             => \&req_log,
  72    'rlog'            => \&req_log,
  73    'tag'             => \&req_CATCHALL,
  74    'status'          => \&req_status,
  75    'admin'           => \&req_CATCHALL,
  76    'history'         => \&req_CATCHALL,
  77    'watchers'        => \&req_EMPTY,
  78    'editors'         => \&req_EMPTY,
  79    'noop'            => \&req_EMPTY,
  80    'annotate'        => \&req_annotate,
  81    'Global_option'   => \&req_Globaloption,
  82    #'annotate'        => \&req_CATCHALL,
  83};
  84
  85##############################################
  86
  87
  88# $state holds all the bits of information the clients sends us that could
  89# potentially be useful when it comes to actually _doing_ something.
  90my $state = { prependdir => '' };
  91
  92# Work is for managing temporary working directory
  93my $work =
  94    {
  95        state => undef,  # undef, 1 (empty), 2 (with stuff)
  96        workDir => undef,
  97        index => undef,
  98        emptyDir => undef,
  99        tmpDir => undef
 100    };
 101
 102$log->info("--------------- STARTING -----------------");
 103
 104my $usage =
 105    "Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n".
 106    "    --base-path <path>  : Prepend to requested CVSROOT\n".
 107    "                          Can be read from GIT_CVSSERVER_BASE_PATH\n".
 108    "    --strict-paths      : Don't allow recursing into subdirectories\n".
 109    "    --export-all        : Don't check for gitcvs.enabled in config\n".
 110    "    --version, -V       : Print version information and exit\n".
 111    "    --help, -h, -H      : Print usage information and exit\n".
 112    "\n".
 113    "<directory> ... is a list of allowed directories. If no directories\n".
 114    "are given, all are allowed. This is an additional restriction, gitcvs\n".
 115    "access still needs to be enabled by the gitcvs.enabled config option.\n".
 116    "Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n";
 117
 118my @opts = ( 'help|h|H', 'version|V',
 119             'base-path=s', 'strict-paths', 'export-all' );
 120GetOptions( $state, @opts )
 121    or die $usage;
 122
 123if ($state->{version}) {
 124    print "git-cvsserver version $VERSION\n";
 125    exit;
 126}
 127if ($state->{help}) {
 128    print $usage;
 129    exit;
 130}
 131
 132my $TEMP_DIR = tempdir( CLEANUP => 1 );
 133$log->debug("Temporary directory is '$TEMP_DIR'");
 134
 135$state->{method} = 'ext';
 136if (@ARGV) {
 137    if ($ARGV[0] eq 'pserver') {
 138        $state->{method} = 'pserver';
 139        shift @ARGV;
 140    } elsif ($ARGV[0] eq 'server') {
 141        shift @ARGV;
 142    }
 143}
 144
 145# everything else is a directory
 146$state->{allowed_roots} = [ @ARGV ];
 147
 148# don't export the whole system unless the users requests it
 149if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
 150    die "--export-all can only be used together with an explicit whitelist\n";
 151}
 152
 153# Environment handling for running under git-shell
 154if (exists $ENV{GIT_CVSSERVER_BASE_PATH}) {
 155    if ($state->{'base-path'}) {
 156        die "Cannot specify base path both ways.\n";
 157    }
 158    my $base_path = $ENV{GIT_CVSSERVER_BASE_PATH};
 159    $state->{'base-path'} = $base_path;
 160    $log->debug("Picked up base path '$base_path' from environment.\n");
 161}
 162if (exists $ENV{GIT_CVSSERVER_ROOT}) {
 163    if (@{$state->{allowed_roots}}) {
 164        die "Cannot specify roots both ways: @ARGV\n";
 165    }
 166    my $allowed_root = $ENV{GIT_CVSSERVER_ROOT};
 167    $state->{allowed_roots} = [ $allowed_root ];
 168    $log->debug("Picked up allowed root '$allowed_root' from environment.\n");
 169}
 170
 171# if we are called with a pserver argument,
 172# deal with the authentication cat before entering the
 173# main loop
 174if ($state->{method} eq 'pserver') {
 175    my $line = <STDIN>; chomp $line;
 176    unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
 177       die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
 178    }
 179    my $request = $1;
 180    $line = <STDIN>; chomp $line;
 181    unless (req_Root('root', $line)) { # reuse Root
 182       print "E Invalid root $line \n";
 183       exit 1;
 184    }
 185    $line = <STDIN>; chomp $line;
 186    my $user = $line;
 187    $line = <STDIN>; chomp $line;
 188    my $password = $line;
 189
 190    unless ($user eq 'anonymous') {
 191        # Trying to authenticate a user
 192        if (not exists $cfg->{gitcvs}->{users}) {
 193            print "E the repo config file needs a [gitcvs.users] section with user/password key-value pairs\n";
 194            print "I HATE YOU\n";
 195            exit 1;
 196        } elsif (exists $cfg->{gitcvs}->{users} and not exists $cfg->{gitcvs}->{users}->{$user}) {
 197            #print "E the repo config file has a [gitcvs.users] section but the user $user is not defined in it\n";
 198            print "I HATE YOU\n";
 199            exit 1;
 200        } else {
 201            my $descrambled_password = descramble($password);
 202            my $cleartext_password = $cfg->{gitcvs}->{users}->{$user};
 203            if ($descrambled_password ne $cleartext_password) {
 204                #print "E The password supplied for user $user was incorrect\n";
 205                print "I HATE YOU\n";
 206                exit 1;
 207            }
 208            # else fall through to LOVE
 209        }
 210    }
 211
 212    # For checking whether the user is anonymous on commit
 213    $state->{user} = $user;
 214
 215    $line = <STDIN>; chomp $line;
 216    unless ($line eq "END $request REQUEST") {
 217       die "E Do not understand $line -- expecting END $request REQUEST\n";
 218    }
 219    print "I LOVE YOU\n";
 220    exit if $request eq 'VERIFICATION'; # cvs login
 221    # and now back to our regular programme...
 222}
 223
 224# Keep going until the client closes the connection
 225while (<STDIN>)
 226{
 227    chomp;
 228
 229    # Check to see if we've seen this method, and call appropriate function.
 230    if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
 231    {
 232        # use the $methods hash to call the appropriate sub for this command
 233        #$log->info("Method : $1");
 234        &{$methods->{$1}}($1,$2);
 235    } else {
 236        # log fatal because we don't understand this function. If this happens
 237        # we're fairly screwed because we don't know if the client is expecting
 238        # a response. If it is, the client will hang, we'll hang, and the whole
 239        # thing will be custard.
 240        $log->fatal("Don't understand command $_\n");
 241        die("Unknown command $_");
 242    }
 243}
 244
 245$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
 246$log->info("--------------- FINISH -----------------");
 247
 248chdir '/';
 249exit 0;
 250
 251# Magic catchall method.
 252#    This is the method that will handle all commands we haven't yet
 253#    implemented. It simply sends a warning to the log file indicating a
 254#    command that hasn't been implemented has been invoked.
 255sub req_CATCHALL
 256{
 257    my ( $cmd, $data ) = @_;
 258    $log->warn("Unhandled command : req_$cmd : $data");
 259}
 260
 261# This method invariably succeeds with an empty response.
 262sub req_EMPTY
 263{
 264    print "ok\n";
 265}
 266
 267# Root pathname \n
 268#     Response expected: no. Tell the server which CVSROOT to use. Note that
 269#     pathname is a local directory and not a fully qualified CVSROOT variable.
 270#     pathname must already exist; if creating a new root, use the init
 271#     request, not Root. pathname does not include the hostname of the server,
 272#     how to access the server, etc.; by the time the CVS protocol is in use,
 273#     connection, authentication, etc., are already taken care of. The Root
 274#     request must be sent only once, and it must be sent before any requests
 275#     other than Valid-responses, valid-requests, UseUnchanged, Set or init.
 276sub req_Root
 277{
 278    my ( $cmd, $data ) = @_;
 279    $log->debug("req_Root : $data");
 280
 281    unless ($data =~ m#^/#) {
 282        print "error 1 Root must be an absolute pathname\n";
 283        return 0;
 284    }
 285
 286    my $cvsroot = $state->{'base-path'} || '';
 287    $cvsroot =~ s#/+$##;
 288    $cvsroot .= $data;
 289
 290    if ($state->{CVSROOT}
 291        && ($state->{CVSROOT} ne $cvsroot)) {
 292        print "error 1 Conflicting roots specified\n";
 293        return 0;
 294    }
 295
 296    $state->{CVSROOT} = $cvsroot;
 297
 298    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
 299
 300    if (@{$state->{allowed_roots}}) {
 301        my $allowed = 0;
 302        foreach my $dir (@{$state->{allowed_roots}}) {
 303            next unless $dir =~ m#^/#;
 304            $dir =~ s#/+$##;
 305            if ($state->{'strict-paths'}) {
 306                if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
 307                    $allowed = 1;
 308                    last;
 309                }
 310            } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
 311                $allowed = 1;
 312                last;
 313            }
 314        }
 315
 316        unless ($allowed) {
 317            print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
 318            print "E \n";
 319            print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
 320            return 0;
 321        }
 322    }
 323
 324    unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
 325       print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
 326       print "E \n";
 327       print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
 328       return 0;
 329    }
 330
 331    my @gitvars = `git config -l`;
 332    if ($?) {
 333       print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
 334        print "E \n";
 335        print "error 1 - problem executing git-config\n";
 336       return 0;
 337    }
 338    foreach my $line ( @gitvars )
 339    {
 340        next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver|users)\.)?([\w-]+)=(.*)$/ );
 341        unless ($2) {
 342            $cfg->{$1}{$3} = $4;
 343        } else {
 344            $cfg->{$1}{$2}{$3} = $4;
 345        }
 346    }
 347
 348    my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
 349                   || $cfg->{gitcvs}{enabled});
 350    unless ($state->{'export-all'} ||
 351            ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
 352        print "E GITCVS emulation needs to be enabled on this repo\n";
 353        print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
 354        print "E \n";
 355        print "error 1 GITCVS emulation disabled\n";
 356        return 0;
 357    }
 358
 359    my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
 360    if ( $logfile )
 361    {
 362        $log->setfile($logfile);
 363    } else {
 364        $log->nofile();
 365    }
 366
 367    return 1;
 368}
 369
 370# Global_option option \n
 371#     Response expected: no. Transmit one of the global options `-q', `-Q',
 372#     `-l', `-t', `-r', or `-n'. option must be one of those strings, no
 373#     variations (such as combining of options) are allowed. For graceful
 374#     handling of valid-requests, it is probably better to make new global
 375#     options separate requests, rather than trying to add them to this
 376#     request.
 377sub req_Globaloption
 378{
 379    my ( $cmd, $data ) = @_;
 380    $log->debug("req_Globaloption : $data");
 381    $state->{globaloptions}{$data} = 1;
 382}
 383
 384# Valid-responses request-list \n
 385#     Response expected: no. Tell the server what responses the client will
 386#     accept. request-list is a space separated list of tokens.
 387sub req_Validresponses
 388{
 389    my ( $cmd, $data ) = @_;
 390    $log->debug("req_Validresponses : $data");
 391
 392    # TODO : re-enable this, currently it's not particularly useful
 393    #$state->{validresponses} = [ split /\s+/, $data ];
 394}
 395
 396# valid-requests \n
 397#     Response expected: yes. Ask the server to send back a Valid-requests
 398#     response.
 399sub req_validrequests
 400{
 401    my ( $cmd, $data ) = @_;
 402
 403    $log->debug("req_validrequests");
 404
 405    $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
 406    $log->debug("SEND : ok");
 407
 408    print "Valid-requests " . join(" ",keys %$methods) . "\n";
 409    print "ok\n";
 410}
 411
 412# Directory local-directory \n
 413#     Additional data: repository \n. Response expected: no. Tell the server
 414#     what directory to use. The repository should be a directory name from a
 415#     previous server response. Note that this both gives a default for Entry
 416#     and Modified and also for ci and the other commands; normal usage is to
 417#     send Directory for each directory in which there will be an Entry or
 418#     Modified, and then a final Directory for the original directory, then the
 419#     command. The local-directory is relative to the top level at which the
 420#     command is occurring (i.e. the last Directory which is sent before the
 421#     command); to indicate that top level, `.' should be sent for
 422#     local-directory.
 423sub req_Directory
 424{
 425    my ( $cmd, $data ) = @_;
 426
 427    my $repository = <STDIN>;
 428    chomp $repository;
 429
 430
 431    $state->{localdir} = $data;
 432    $state->{repository} = $repository;
 433    $state->{path} = $repository;
 434    $state->{path} =~ s/^\Q$state->{CVSROOT}\E\///;
 435    $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
 436    $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
 437
 438    $state->{directory} = $state->{localdir};
 439    $state->{directory} = "" if ( $state->{directory} eq "." );
 440    $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
 441
 442    if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
 443    {
 444        $log->info("Setting prepend to '$state->{path}'");
 445        $state->{prependdir} = $state->{path};
 446        foreach my $entry ( keys %{$state->{entries}} )
 447        {
 448            $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
 449            delete $state->{entries}{$entry};
 450        }
 451    }
 452
 453    if ( defined ( $state->{prependdir} ) )
 454    {
 455        $log->debug("Prepending '$state->{prependdir}' to state|directory");
 456        $state->{directory} = $state->{prependdir} . $state->{directory}
 457    }
 458    $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
 459}
 460
 461# Entry entry-line \n
 462#     Response expected: no. Tell the server what version of a file is on the
 463#     local machine. The name in entry-line is a name relative to the directory
 464#     most recently specified with Directory. If the user is operating on only
 465#     some files in a directory, Entry requests for only those files need be
 466#     included. If an Entry request is sent without Modified, Is-modified, or
 467#     Unchanged, it means the file is lost (does not exist in the working
 468#     directory). If both Entry and one of Modified, Is-modified, or Unchanged
 469#     are sent for the same file, Entry must be sent first. For a given file,
 470#     one can send Modified, Is-modified, or Unchanged, but not more than one
 471#     of these three.
 472sub req_Entry
 473{
 474    my ( $cmd, $data ) = @_;
 475
 476    #$log->debug("req_Entry : $data");
 477
 478    my @data = split(/\//, $data);
 479
 480    $state->{entries}{$state->{directory}.$data[1]} = {
 481        revision    => $data[2],
 482        conflict    => $data[3],
 483        options     => $data[4],
 484        tag_or_date => $data[5],
 485    };
 486
 487    $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
 488}
 489
 490# Questionable filename \n
 491#     Response expected: no. Additional data: no. Tell the server to check
 492#     whether filename should be ignored, and if not, next time the server
 493#     sends responses, send (in a M response) `?' followed by the directory and
 494#     filename. filename must not contain `/'; it needs to be a file in the
 495#     directory named by the most recent Directory request.
 496sub req_Questionable
 497{
 498    my ( $cmd, $data ) = @_;
 499
 500    $log->debug("req_Questionable : $data");
 501    $state->{entries}{$state->{directory}.$data}{questionable} = 1;
 502}
 503
 504# add \n
 505#     Response expected: yes. Add a file or directory. This uses any previous
 506#     Argument, Directory, Entry, or Modified requests, if they have been sent.
 507#     The last Directory sent specifies the working directory at the time of
 508#     the operation. To add a directory, send the directory to be added using
 509#     Directory and Argument requests.
 510sub req_add
 511{
 512    my ( $cmd, $data ) = @_;
 513
 514    argsplit("add");
 515
 516    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
 517    $updater->update();
 518
 519    argsfromdir($updater);
 520
 521    my $addcount = 0;
 522
 523    foreach my $filename ( @{$state->{args}} )
 524    {
 525        $filename = filecleanup($filename);
 526
 527        my $meta = $updater->getmeta($filename);
 528        my $wrev = revparse($filename);
 529
 530        if ($wrev && $meta && ($wrev < 0))
 531        {
 532            # previously removed file, add back
 533            $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
 534
 535            print "MT +updated\n";
 536            print "MT text U \n";
 537            print "MT fname $filename\n";
 538            print "MT newline\n";
 539            print "MT -updated\n";
 540
 541            unless ( $state->{globaloptions}{-n} )
 542            {
 543                my ( $filepart, $dirpart ) = filenamesplit($filename,1);
 544
 545                print "Created $dirpart\n";
 546                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
 547
 548                # this is an "entries" line
 549                my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
 550                $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
 551                print "/$filepart/1.$meta->{revision}//$kopts/\n";
 552                # permissions
 553                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
 554                print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
 555                # transmit file
 556                transmitfile($meta->{filehash});
 557            }
 558
 559            next;
 560        }
 561
 562        unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
 563        {
 564            print "E cvs add: nothing known about `$filename'\n";
 565            next;
 566        }
 567        # TODO : check we're not squashing an already existing file
 568        if ( defined ( $state->{entries}{$filename}{revision} ) )
 569        {
 570            print "E cvs add: `$filename' has already been entered\n";
 571            next;
 572        }
 573
 574        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
 575
 576        print "E cvs add: scheduling file `$filename' for addition\n";
 577
 578        print "Checked-in $dirpart\n";
 579        print "$filename\n";
 580        my $kopts = kopts_from_path($filename,"file",
 581                        $state->{entries}{$filename}{modified_filename});
 582        print "/$filepart/0//$kopts/\n";
 583
 584        my $requestedKopts = $state->{opt}{k};
 585        if(defined($requestedKopts))
 586        {
 587            $requestedKopts = "-k$requestedKopts";
 588        }
 589        else
 590        {
 591            $requestedKopts = "";
 592        }
 593        if( $kopts ne $requestedKopts )
 594        {
 595            $log->warn("Ignoring requested -k='$requestedKopts'"
 596                        . " for '$filename'; detected -k='$kopts' instead");
 597            #TODO: Also have option to send warning to user?
 598        }
 599
 600        $addcount++;
 601    }
 602
 603    if ( $addcount == 1 )
 604    {
 605        print "E cvs add: use `cvs commit' to add this file permanently\n";
 606    }
 607    elsif ( $addcount > 1 )
 608    {
 609        print "E cvs add: use `cvs commit' to add these files permanently\n";
 610    }
 611
 612    print "ok\n";
 613}
 614
 615# remove \n
 616#     Response expected: yes. Remove a file. This uses any previous Argument,
 617#     Directory, Entry, or Modified requests, if they have been sent. The last
 618#     Directory sent specifies the working directory at the time of the
 619#     operation. Note that this request does not actually do anything to the
 620#     repository; the only effect of a successful remove request is to supply
 621#     the client with a new entries line containing `-' to indicate a removed
 622#     file. In fact, the client probably could perform this operation without
 623#     contacting the server, although using remove may cause the server to
 624#     perform a few more checks. The client sends a subsequent ci request to
 625#     actually record the removal in the repository.
 626sub req_remove
 627{
 628    my ( $cmd, $data ) = @_;
 629
 630    argsplit("remove");
 631
 632    # Grab a handle to the SQLite db and do any necessary updates
 633    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
 634    $updater->update();
 635
 636    #$log->debug("add state : " . Dumper($state));
 637
 638    my $rmcount = 0;
 639
 640    foreach my $filename ( @{$state->{args}} )
 641    {
 642        $filename = filecleanup($filename);
 643
 644        if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
 645        {
 646            print "E cvs remove: file `$filename' still in working directory\n";
 647            next;
 648        }
 649
 650        my $meta = $updater->getmeta($filename);
 651        my $wrev = revparse($filename);
 652
 653        unless ( defined ( $wrev ) )
 654        {
 655            print "E cvs remove: nothing known about `$filename'\n";
 656            next;
 657        }
 658
 659        if ( defined($wrev) and $wrev < 0 )
 660        {
 661            print "E cvs remove: file `$filename' already scheduled for removal\n";
 662            next;
 663        }
 664
 665        unless ( $wrev == $meta->{revision} )
 666        {
 667            # TODO : not sure if the format of this message is quite correct.
 668            print "E cvs remove: Up to date check failed for `$filename'\n";
 669            next;
 670        }
 671
 672
 673        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
 674
 675        print "E cvs remove: scheduling `$filename' for removal\n";
 676
 677        print "Checked-in $dirpart\n";
 678        print "$filename\n";
 679        my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
 680        print "/$filepart/-1.$wrev//$kopts/\n";
 681
 682        $rmcount++;
 683    }
 684
 685    if ( $rmcount == 1 )
 686    {
 687        print "E cvs remove: use `cvs commit' to remove this file permanently\n";
 688    }
 689    elsif ( $rmcount > 1 )
 690    {
 691        print "E cvs remove: use `cvs commit' to remove these files permanently\n";
 692    }
 693
 694    print "ok\n";
 695}
 696
 697# Modified filename \n
 698#     Response expected: no. Additional data: mode, \n, file transmission. Send
 699#     the server a copy of one locally modified file. filename is a file within
 700#     the most recent directory sent with Directory; it must not contain `/'.
 701#     If the user is operating on only some files in a directory, only those
 702#     files need to be included. This can also be sent without Entry, if there
 703#     is no entry for the file.
 704sub req_Modified
 705{
 706    my ( $cmd, $data ) = @_;
 707
 708    my $mode = <STDIN>;
 709    defined $mode
 710        or (print "E end of file reading mode for $data\n"), return;
 711    chomp $mode;
 712    my $size = <STDIN>;
 713    defined $size
 714        or (print "E end of file reading size of $data\n"), return;
 715    chomp $size;
 716
 717    # Grab config information
 718    my $blocksize = 8192;
 719    my $bytesleft = $size;
 720    my $tmp;
 721
 722    # Get a filehandle/name to write it to
 723    my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
 724
 725    # Loop over file data writing out to temporary file.
 726    while ( $bytesleft )
 727    {
 728        $blocksize = $bytesleft if ( $bytesleft < $blocksize );
 729        read STDIN, $tmp, $blocksize;
 730        print $fh $tmp;
 731        $bytesleft -= $blocksize;
 732    }
 733
 734    close $fh
 735        or (print "E failed to write temporary, $filename: $!\n"), return;
 736
 737    # Ensure we have something sensible for the file mode
 738    if ( $mode =~ /u=(\w+)/ )
 739    {
 740        $mode = $1;
 741    } else {
 742        $mode = "rw";
 743    }
 744
 745    # Save the file data in $state
 746    $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
 747    $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
 748    $state->{entries}{$state->{directory}.$data}{modified_hash} = `git hash-object $filename`;
 749    $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
 750
 751    #$log->debug("req_Modified : file=$data mode=$mode size=$size");
 752}
 753
 754# Unchanged filename \n
 755#     Response expected: no. Tell the server that filename has not been
 756#     modified in the checked out directory. The filename is a file within the
 757#     most recent directory sent with Directory; it must not contain `/'.
 758sub req_Unchanged
 759{
 760    my ( $cmd, $data ) = @_;
 761
 762    $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
 763
 764    #$log->debug("req_Unchanged : $data");
 765}
 766
 767# Argument text \n
 768#     Response expected: no. Save argument for use in a subsequent command.
 769#     Arguments accumulate until an argument-using command is given, at which
 770#     point they are forgotten.
 771# Argumentx text \n
 772#     Response expected: no. Append \n followed by text to the current argument
 773#     being saved.
 774sub req_Argument
 775{
 776    my ( $cmd, $data ) = @_;
 777
 778    # Argumentx means: append to last Argument (with a newline in front)
 779
 780    $log->debug("$cmd : $data");
 781
 782    if ( $cmd eq 'Argumentx') {
 783        ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
 784    } else {
 785        push @{$state->{arguments}}, $data;
 786    }
 787}
 788
 789# expand-modules \n
 790#     Response expected: yes. Expand the modules which are specified in the
 791#     arguments. Returns the data in Module-expansion responses. Note that the
 792#     server can assume that this is checkout or export, not rtag or rdiff; the
 793#     latter do not access the working directory and thus have no need to
 794#     expand modules on the client side. Expand may not be the best word for
 795#     what this request does. It does not necessarily tell you all the files
 796#     contained in a module, for example. Basically it is a way of telling you
 797#     which working directories the server needs to know about in order to
 798#     handle a checkout of the specified modules. For example, suppose that the
 799#     server has a module defined by
 800#   aliasmodule -a 1dir
 801#     That is, one can check out aliasmodule and it will take 1dir in the
 802#     repository and check it out to 1dir in the working directory. Now suppose
 803#     the client already has this module checked out and is planning on using
 804#     the co request to update it. Without using expand-modules, the client
 805#     would have two bad choices: it could either send information about all
 806#     working directories under the current directory, which could be
 807#     unnecessarily slow, or it could be ignorant of the fact that aliasmodule
 808#     stands for 1dir, and neglect to send information for 1dir, which would
 809#     lead to incorrect operation. With expand-modules, the client would first
 810#     ask for the module to be expanded:
 811sub req_expandmodules
 812{
 813    my ( $cmd, $data ) = @_;
 814
 815    argsplit();
 816
 817    $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
 818
 819    unless ( ref $state->{arguments} eq "ARRAY" )
 820    {
 821        print "ok\n";
 822        return;
 823    }
 824
 825    foreach my $module ( @{$state->{arguments}} )
 826    {
 827        $log->debug("SEND : Module-expansion $module");
 828        print "Module-expansion $module\n";
 829    }
 830
 831    print "ok\n";
 832    statecleanup();
 833}
 834
 835# co \n
 836#     Response expected: yes. Get files from the repository. This uses any
 837#     previous Argument, Directory, Entry, or Modified requests, if they have
 838#     been sent. Arguments to this command are module names; the client cannot
 839#     know what directories they correspond to except by (1) just sending the
 840#     co request, and then seeing what directory names the server sends back in
 841#     its responses, and (2) the expand-modules request.
 842sub req_co
 843{
 844    my ( $cmd, $data ) = @_;
 845
 846    argsplit("co");
 847
 848    # Provide list of modules, if -c was used.
 849    if (exists $state->{opt}{c}) {
 850        my $showref = `git show-ref --heads`;
 851        for my $line (split '\n', $showref) {
 852            if ( $line =~ m% refs/heads/(.*)$% ) {
 853                print "M $1\t$1\n";
 854            }
 855        }
 856        print "ok\n";
 857        return 1;
 858    }
 859
 860    my $module = $state->{args}[0];
 861    $state->{module} = $module;
 862    my $checkout_path = $module;
 863
 864    # use the user specified directory if we're given it
 865    $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
 866
 867    $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
 868
 869    $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
 870
 871    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
 872
 873    # Grab a handle to the SQLite db and do any necessary updates
 874    my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
 875    $updater->update();
 876
 877    $checkout_path =~ s|/$||; # get rid of trailing slashes
 878
 879    # Eclipse seems to need the Clear-sticky command
 880    # to prepare the 'Entries' file for the new directory.
 881    print "Clear-sticky $checkout_path/\n";
 882    print $state->{CVSROOT} . "/$module/\n";
 883    print "Clear-static-directory $checkout_path/\n";
 884    print $state->{CVSROOT} . "/$module/\n";
 885    print "Clear-sticky $checkout_path/\n"; # yes, twice
 886    print $state->{CVSROOT} . "/$module/\n";
 887    print "Template $checkout_path/\n";
 888    print $state->{CVSROOT} . "/$module/\n";
 889    print "0\n";
 890
 891    # instruct the client that we're checking out to $checkout_path
 892    print "E cvs checkout: Updating $checkout_path\n";
 893
 894    my %seendirs = ();
 895    my $lastdir ='';
 896
 897    # recursive
 898    sub prepdir {
 899       my ($dir, $repodir, $remotedir, $seendirs) = @_;
 900       my $parent = dirname($dir);
 901       $dir       =~ s|/+$||;
 902       $repodir   =~ s|/+$||;
 903       $remotedir =~ s|/+$||;
 904       $parent    =~ s|/+$||;
 905       $log->debug("announcedir $dir, $repodir, $remotedir" );
 906
 907       if ($parent eq '.' || $parent eq './') {
 908           $parent = '';
 909       }
 910       # recurse to announce unseen parents first
 911       if (length($parent) && !exists($seendirs->{$parent})) {
 912           prepdir($parent, $repodir, $remotedir, $seendirs);
 913       }
 914       # Announce that we are going to modify at the parent level
 915       if ($parent) {
 916           print "E cvs checkout: Updating $remotedir/$parent\n";
 917       } else {
 918           print "E cvs checkout: Updating $remotedir\n";
 919       }
 920       print "Clear-sticky $remotedir/$parent/\n";
 921       print "$repodir/$parent/\n";
 922
 923       print "Clear-static-directory $remotedir/$dir/\n";
 924       print "$repodir/$dir/\n";
 925       print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
 926       print "$repodir/$parent/\n";
 927       print "Template $remotedir/$dir/\n";
 928       print "$repodir/$dir/\n";
 929       print "0\n";
 930
 931       $seendirs->{$dir} = 1;
 932    }
 933
 934    foreach my $git ( @{$updater->gethead} )
 935    {
 936        # Don't want to check out deleted files
 937        next if ( $git->{filehash} eq "deleted" );
 938
 939        my $fullName = $git->{name};
 940        ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
 941
 942       if (length($git->{dir}) && $git->{dir} ne './'
 943           && $git->{dir} ne $lastdir ) {
 944           unless (exists($seendirs{$git->{dir}})) {
 945               prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
 946                       $checkout_path, \%seendirs);
 947               $lastdir = $git->{dir};
 948               $seendirs{$git->{dir}} = 1;
 949           }
 950           print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
 951       }
 952
 953        # modification time of this file
 954        print "Mod-time $git->{modified}\n";
 955
 956        # print some information to the client
 957        if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
 958        {
 959            print "M U $checkout_path/$git->{dir}$git->{name}\n";
 960        } else {
 961            print "M U $checkout_path/$git->{name}\n";
 962        }
 963
 964       # instruct client we're sending a file to put in this path
 965       print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
 966
 967       print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
 968
 969        # this is an "entries" line
 970        my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
 971        print "/$git->{name}/1.$git->{revision}//$kopts/\n";
 972        # permissions
 973        print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
 974
 975        # transmit file
 976        transmitfile($git->{filehash});
 977    }
 978
 979    print "ok\n";
 980
 981    statecleanup();
 982}
 983
 984# update \n
 985#     Response expected: yes. Actually do a cvs update command. This uses any
 986#     previous Argument, Directory, Entry, or Modified requests, if they have
 987#     been sent. The last Directory sent specifies the working directory at the
 988#     time of the operation. The -I option is not used--files which the client
 989#     can decide whether to ignore are not mentioned and the client sends the
 990#     Questionable request for others.
 991sub req_update
 992{
 993    my ( $cmd, $data ) = @_;
 994
 995    $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
 996
 997    argsplit("update");
 998
 999    #
1000    # It may just be a client exploring the available heads/modules
1001    # in that case, list them as top level directories and leave it
1002    # at that. Eclipse uses this technique to offer you a list of
1003    # projects (heads in this case) to checkout.
1004    #
1005    if ($state->{module} eq '') {
1006        my $showref = `git show-ref --heads`;
1007        print "E cvs update: Updating .\n";
1008        for my $line (split '\n', $showref) {
1009            if ( $line =~ m% refs/heads/(.*)$% ) {
1010                print "E cvs update: New directory `$1'\n";
1011            }
1012        }
1013        print "ok\n";
1014        return 1;
1015    }
1016
1017
1018    # Grab a handle to the SQLite db and do any necessary updates
1019    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1020
1021    $updater->update();
1022
1023    argsfromdir($updater);
1024
1025    #$log->debug("update state : " . Dumper($state));
1026
1027    my $last_dirname = "///";
1028
1029    # foreach file specified on the command line ...
1030    foreach my $filename ( @{$state->{args}} )
1031    {
1032        $filename = filecleanup($filename);
1033
1034        $log->debug("Processing file $filename");
1035
1036        unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
1037        {
1038            my $cur_dirname = dirname($filename);
1039            if ( $cur_dirname ne $last_dirname )
1040            {
1041                $last_dirname = $cur_dirname;
1042                if ( $cur_dirname eq "" )
1043                {
1044                    $cur_dirname = ".";
1045                }
1046                print "E cvs update: Updating $cur_dirname\n";
1047            }
1048        }
1049
1050        # if we have a -C we should pretend we never saw modified stuff
1051        if ( exists ( $state->{opt}{C} ) )
1052        {
1053            delete $state->{entries}{$filename}{modified_hash};
1054            delete $state->{entries}{$filename}{modified_filename};
1055            $state->{entries}{$filename}{unchanged} = 1;
1056        }
1057
1058        my $meta;
1059        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
1060        {
1061            $meta = $updater->getmeta($filename, $1);
1062        } else {
1063            $meta = $updater->getmeta($filename);
1064        }
1065
1066        # If -p was given, "print" the contents of the requested revision.
1067        if ( exists ( $state->{opt}{p} ) ) {
1068            if ( defined ( $meta->{revision} ) ) {
1069                $log->info("Printing '$filename' revision " . $meta->{revision});
1070
1071                transmitfile($meta->{filehash}, { print => 1 });
1072            }
1073
1074            next;
1075        }
1076
1077        if ( ! defined $meta )
1078        {
1079            $meta = {
1080                name => $filename,
1081                revision => 0,
1082                filehash => 'added'
1083            };
1084        }
1085
1086        my $oldmeta = $meta;
1087
1088        my $wrev = revparse($filename);
1089
1090        # If the working copy is an old revision, lets get that version too for comparison.
1091        if ( defined($wrev) and $wrev != $meta->{revision} )
1092        {
1093            $oldmeta = $updater->getmeta($filename, $wrev);
1094        }
1095
1096        #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
1097
1098        # Files are up to date if the working copy and repo copy have the same revision,
1099        # and the working copy is unmodified _and_ the user hasn't specified -C
1100        next if ( defined ( $wrev )
1101                  and defined($meta->{revision})
1102                  and $wrev == $meta->{revision}
1103                  and $state->{entries}{$filename}{unchanged}
1104                  and not exists ( $state->{opt}{C} ) );
1105
1106        # If the working copy and repo copy have the same revision,
1107        # but the working copy is modified, tell the client it's modified
1108        if ( defined ( $wrev )
1109             and defined($meta->{revision})
1110             and $wrev == $meta->{revision}
1111             and defined($state->{entries}{$filename}{modified_hash})
1112             and not exists ( $state->{opt}{C} ) )
1113        {
1114            $log->info("Tell the client the file is modified");
1115            print "MT text M \n";
1116            print "MT fname $filename\n";
1117            print "MT newline\n";
1118            next;
1119        }
1120
1121        if ( $meta->{filehash} eq "deleted" )
1122        {
1123            my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1124
1125            $log->info("Removing '$filename' from working copy (no longer in the repo)");
1126
1127            print "E cvs update: `$filename' is no longer in the repository\n";
1128            # Don't want to actually _DO_ the update if -n specified
1129            unless ( $state->{globaloptions}{-n} ) {
1130                print "Removed $dirpart\n";
1131                print "$filepart\n";
1132            }
1133        }
1134        elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
1135                or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1136                or $meta->{filehash} eq 'added' )
1137        {
1138            # normal update, just send the new revision (either U=Update,
1139            # or A=Add, or R=Remove)
1140            if ( defined($wrev) && $wrev < 0 )
1141            {
1142                $log->info("Tell the client the file is scheduled for removal");
1143                print "MT text R \n";
1144                print "MT fname $filename\n";
1145                print "MT newline\n";
1146                next;
1147            }
1148            elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
1149            {
1150                $log->info("Tell the client the file is scheduled for addition");
1151                print "MT text A \n";
1152                print "MT fname $filename\n";
1153                print "MT newline\n";
1154                next;
1155
1156            }
1157            else {
1158                $log->info("Updating '$filename' to ".$meta->{revision});
1159                print "MT +updated\n";
1160                print "MT text U \n";
1161                print "MT fname $filename\n";
1162                print "MT newline\n";
1163                print "MT -updated\n";
1164            }
1165
1166            my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1167
1168            # Don't want to actually _DO_ the update if -n specified
1169            unless ( $state->{globaloptions}{-n} )
1170            {
1171                if ( defined ( $wrev ) )
1172                {
1173                    # instruct client we're sending a file to put in this path as a replacement
1174                    print "Update-existing $dirpart\n";
1175                    $log->debug("Updating existing file 'Update-existing $dirpart'");
1176                } else {
1177                    # instruct client we're sending a file to put in this path as a new file
1178                    print "Clear-static-directory $dirpart\n";
1179                    print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1180                    print "Clear-sticky $dirpart\n";
1181                    print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1182
1183                    $log->debug("Creating new file 'Created $dirpart'");
1184                    print "Created $dirpart\n";
1185                }
1186                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1187
1188                # this is an "entries" line
1189                my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1190                $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1191                print "/$filepart/1.$meta->{revision}//$kopts/\n";
1192
1193                # permissions
1194                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1195                print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1196
1197                # transmit file
1198                transmitfile($meta->{filehash});
1199            }
1200        } else {
1201            $log->info("Updating '$filename'");
1202            my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
1203
1204            my $mergeDir = setupTmpDir();
1205
1206            my $file_local = $filepart . ".mine";
1207            my $mergedFile = "$mergeDir/$file_local";
1208            system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1209            my $file_old = $filepart . "." . $oldmeta->{revision};
1210            transmitfile($oldmeta->{filehash}, { targetfile => $file_old });
1211            my $file_new = $filepart . "." . $meta->{revision};
1212            transmitfile($meta->{filehash}, { targetfile => $file_new });
1213
1214            # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1215            $log->info("Merging $file_local, $file_old, $file_new");
1216            print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
1217
1218            $log->debug("Temporary directory for merge is $mergeDir");
1219
1220            my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1221            $return >>= 8;
1222
1223            cleanupTmpDir();
1224
1225            if ( $return == 0 )
1226            {
1227                $log->info("Merged successfully");
1228                print "M M $filename\n";
1229                $log->debug("Merged $dirpart");
1230
1231                # Don't want to actually _DO_ the update if -n specified
1232                unless ( $state->{globaloptions}{-n} )
1233                {
1234                    print "Merged $dirpart\n";
1235                    $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1236                    print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1237                    my $kopts = kopts_from_path("$dirpart/$filepart",
1238                                                "file",$mergedFile);
1239                    $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1240                    print "/$filepart/1.$meta->{revision}//$kopts/\n";
1241                }
1242            }
1243            elsif ( $return == 1 )
1244            {
1245                $log->info("Merged with conflicts");
1246                print "E cvs update: conflicts found in $filename\n";
1247                print "M C $filename\n";
1248
1249                # Don't want to actually _DO_ the update if -n specified
1250                unless ( $state->{globaloptions}{-n} )
1251                {
1252                    print "Merged $dirpart\n";
1253                    print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1254                    my $kopts = kopts_from_path("$dirpart/$filepart",
1255                                                "file",$mergedFile);
1256                    print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
1257                }
1258            }
1259            else
1260            {
1261                $log->warn("Merge failed");
1262                next;
1263            }
1264
1265            # Don't want to actually _DO_ the update if -n specified
1266            unless ( $state->{globaloptions}{-n} )
1267            {
1268                # permissions
1269                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1270                print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1271
1272                # transmit file, format is single integer on a line by itself (file
1273                # size) followed by the file contents
1274                # TODO : we should copy files in blocks
1275                my $data = `cat $mergedFile`;
1276                $log->debug("File size : " . length($data));
1277                print length($data) . "\n";
1278                print $data;
1279            }
1280        }
1281
1282    }
1283
1284    print "ok\n";
1285}
1286
1287sub req_ci
1288{
1289    my ( $cmd, $data ) = @_;
1290
1291    argsplit("ci");
1292
1293    #$log->debug("State : " . Dumper($state));
1294
1295    $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1296
1297    if ( $state->{method} eq 'pserver' and $state->{user} eq 'anonymous' )
1298    {
1299        print "error 1 anonymous user cannot commit via pserver\n";
1300        cleanupWorkTree();
1301        exit;
1302    }
1303
1304    if ( -e $state->{CVSROOT} . "/index" )
1305    {
1306        $log->warn("file 'index' already exists in the git repository");
1307        print "error 1 Index already exists in git repo\n";
1308        cleanupWorkTree();
1309        exit;
1310    }
1311
1312    # Grab a handle to the SQLite db and do any necessary updates
1313    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1314    $updater->update();
1315
1316    # Remember where the head was at the beginning.
1317    my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1318    chomp $parenthash;
1319    if ($parenthash !~ /^[0-9a-f]{40}$/) {
1320            print "error 1 pserver cannot find the current HEAD of module";
1321            cleanupWorkTree();
1322            exit;
1323    }
1324
1325    setupWorkTree($parenthash);
1326
1327    $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1328
1329    $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
1330
1331    my @committedfiles = ();
1332    my %oldmeta;
1333
1334    # foreach file specified on the command line ...
1335    foreach my $filename ( @{$state->{args}} )
1336    {
1337        my $committedfile = $filename;
1338        $filename = filecleanup($filename);
1339
1340        next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1341
1342        my $meta = $updater->getmeta($filename);
1343        $oldmeta{$filename} = $meta;
1344
1345        my $wrev = revparse($filename);
1346
1347        my ( $filepart, $dirpart ) = filenamesplit($filename);
1348
1349        # do a checkout of the file if it is part of this tree
1350        if ($wrev) {
1351            system('git', 'checkout-index', '-f', '-u', $filename);
1352            unless ($? == 0) {
1353                die "Error running git-checkout-index -f -u $filename : $!";
1354            }
1355        }
1356
1357        my $addflag = 0;
1358        my $rmflag = 0;
1359        $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1360        $addflag = 1 unless ( -e $filename );
1361
1362        # Do up to date checking
1363        unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1364        {
1365            # fail everything if an up to date check fails
1366            print "error 1 Up to date check failed for $filename\n";
1367            cleanupWorkTree();
1368            exit;
1369        }
1370
1371        push @committedfiles, $committedfile;
1372        $log->info("Committing $filename");
1373
1374        system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1375
1376        unless ( $rmflag )
1377        {
1378            $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1379            rename $state->{entries}{$filename}{modified_filename},$filename;
1380
1381            # Calculate modes to remove
1382            my $invmode = "";
1383            foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1384
1385            $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1386            system("chmod","u+" .  $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1387        }
1388
1389        if ( $rmflag )
1390        {
1391            $log->info("Removing file '$filename'");
1392            unlink($filename);
1393            system("git", "update-index", "--remove", $filename);
1394        }
1395        elsif ( $addflag )
1396        {
1397            $log->info("Adding file '$filename'");
1398            system("git", "update-index", "--add", $filename);
1399        } else {
1400            $log->info("Updating file '$filename'");
1401            system("git", "update-index", $filename);
1402        }
1403    }
1404
1405    unless ( scalar(@committedfiles) > 0 )
1406    {
1407        print "E No files to commit\n";
1408        print "ok\n";
1409        cleanupWorkTree();
1410        return;
1411    }
1412
1413    my $treehash = `git write-tree`;
1414    chomp $treehash;
1415
1416    $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1417
1418    # write our commit message out if we have one ...
1419    my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1420    print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1421    if ( defined ( $cfg->{gitcvs}{commitmsgannotation} ) ) {
1422        if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/ ) {
1423            print $msg_fh "\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"
1424        }
1425    } else {
1426        print $msg_fh "\n\nvia git-CVS emulator\n";
1427    }
1428    close $msg_fh;
1429
1430    my $commithash = `git commit-tree $treehash -p $parenthash < $msg_filename`;
1431    chomp($commithash);
1432    $log->info("Commit hash : $commithash");
1433
1434    unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1435    {
1436        $log->warn("Commit failed (Invalid commit hash)");
1437        print "error 1 Commit failed (unknown reason)\n";
1438        cleanupWorkTree();
1439        exit;
1440    }
1441
1442        ### Emulate git-receive-pack by running hooks/update
1443        my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1444                        $parenthash, $commithash );
1445        if( -x $hook[0] ) {
1446                unless( system( @hook ) == 0 )
1447                {
1448                        $log->warn("Commit failed (update hook declined to update ref)");
1449                        print "error 1 Commit failed (update hook declined)\n";
1450                        cleanupWorkTree();
1451                        exit;
1452                }
1453        }
1454
1455        ### Update the ref
1456        if (system(qw(git update-ref -m), "cvsserver ci",
1457                        "refs/heads/$state->{module}", $commithash, $parenthash)) {
1458                $log->warn("update-ref for $state->{module} failed.");
1459                print "error 1 Cannot commit -- update first\n";
1460                cleanupWorkTree();
1461                exit;
1462        }
1463
1464        ### Emulate git-receive-pack by running hooks/post-receive
1465        my $hook = $ENV{GIT_DIR}.'hooks/post-receive';
1466        if( -x $hook ) {
1467                open(my $pipe, "| $hook") || die "can't fork $!";
1468
1469                local $SIG{PIPE} = sub { die 'pipe broke' };
1470
1471                print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
1472
1473                close $pipe || die "bad pipe: $! $?";
1474        }
1475
1476    $updater->update();
1477
1478        ### Then hooks/post-update
1479        $hook = $ENV{GIT_DIR}.'hooks/post-update';
1480        if (-x $hook) {
1481                system($hook, "refs/heads/$state->{module}");
1482        }
1483
1484    # foreach file specified on the command line ...
1485    foreach my $filename ( @committedfiles )
1486    {
1487        $filename = filecleanup($filename);
1488
1489        my $meta = $updater->getmeta($filename);
1490        unless (defined $meta->{revision}) {
1491          $meta->{revision} = 1;
1492        }
1493
1494        my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
1495
1496        $log->debug("Checked-in $dirpart : $filename");
1497
1498        print "M $state->{CVSROOT}/$state->{module}/$filename,v  <--  $dirpart$filepart\n";
1499        if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
1500        {
1501            print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
1502            print "Remove-entry $dirpart\n";
1503            print "$filename\n";
1504        } else {
1505            if ($meta->{revision} == 1) {
1506                print "M initial revision: 1.1\n";
1507            } else {
1508                print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
1509            }
1510            print "Checked-in $dirpart\n";
1511            print "$filename\n";
1512            my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1513            print "/$filepart/1.$meta->{revision}//$kopts/\n";
1514        }
1515    }
1516
1517    cleanupWorkTree();
1518    print "ok\n";
1519}
1520
1521sub req_status
1522{
1523    my ( $cmd, $data ) = @_;
1524
1525    argsplit("status");
1526
1527    $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1528    #$log->debug("status state : " . Dumper($state));
1529
1530    # Grab a handle to the SQLite db and do any necessary updates
1531    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1532    $updater->update();
1533
1534    # if no files were specified, we need to work out what files we should be providing status on ...
1535    argsfromdir($updater);
1536
1537    # foreach file specified on the command line ...
1538    foreach my $filename ( @{$state->{args}} )
1539    {
1540        $filename = filecleanup($filename);
1541
1542        next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0;
1543
1544        my $meta = $updater->getmeta($filename);
1545        my $oldmeta = $meta;
1546
1547        my $wrev = revparse($filename);
1548
1549        # If the working copy is an old revision, lets get that version too for comparison.
1550        if ( defined($wrev) and $wrev != $meta->{revision} )
1551        {
1552            $oldmeta = $updater->getmeta($filename, $wrev);
1553        }
1554
1555        # TODO : All possible statuses aren't yet implemented
1556        my $status;
1557        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1558        $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1559                                    and
1560                                    ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1561                                      or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1562                                   );
1563
1564        # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1565        $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1566                                          and
1567                                          ( $state->{entries}{$filename}{unchanged}
1568                                            or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1569                                        );
1570
1571        # Need checkout if it exists in the repo but doesn't have a working copy
1572        $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1573
1574        # Locally modified if working copy and repo copy have the same revision but there are local changes
1575        $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1576
1577        # Needs Merge if working copy revision is less than repo copy and there are local changes
1578        $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1579
1580        $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1581        $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1582        $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1583        $status ||= "File had conflicts on merge" if ( 0 );
1584
1585        $status ||= "Unknown";
1586
1587        my ($filepart) = filenamesplit($filename);
1588
1589        print "M ===================================================================\n";
1590        print "M File: $filepart\tStatus: $status\n";
1591        if ( defined($state->{entries}{$filename}{revision}) )
1592        {
1593            print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1594        } else {
1595            print "M Working revision:\tNo entry for $filename\n";
1596        }
1597        if ( defined($meta->{revision}) )
1598        {
1599            print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1600            print "M Sticky Tag:\t\t(none)\n";
1601            print "M Sticky Date:\t\t(none)\n";
1602            print "M Sticky Options:\t\t(none)\n";
1603        } else {
1604            print "M Repository revision:\tNo revision control file\n";
1605        }
1606        print "M\n";
1607    }
1608
1609    print "ok\n";
1610}
1611
1612sub req_diff
1613{
1614    my ( $cmd, $data ) = @_;
1615
1616    argsplit("diff");
1617
1618    $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1619    #$log->debug("status state : " . Dumper($state));
1620
1621    my ($revision1, $revision2);
1622    if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1623    {
1624        $revision1 = $state->{opt}{r}[0];
1625        $revision2 = $state->{opt}{r}[1];
1626    } else {
1627        $revision1 = $state->{opt}{r};
1628    }
1629
1630    $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1631    $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1632
1633    $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1634
1635    # Grab a handle to the SQLite db and do any necessary updates
1636    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1637    $updater->update();
1638
1639    # if no files were specified, we need to work out what files we should be providing status on ...
1640    argsfromdir($updater);
1641
1642    # foreach file specified on the command line ...
1643    foreach my $filename ( @{$state->{args}} )
1644    {
1645        $filename = filecleanup($filename);
1646
1647        my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1648
1649        my $wrev = revparse($filename);
1650
1651        # We need _something_ to diff against
1652        next unless ( defined ( $wrev ) );
1653
1654        # if we have a -r switch, use it
1655        if ( defined ( $revision1 ) )
1656        {
1657            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1658            $meta1 = $updater->getmeta($filename, $revision1);
1659            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1660            {
1661                print "E File $filename at revision 1.$revision1 doesn't exist\n";
1662                next;
1663            }
1664            transmitfile($meta1->{filehash}, { targetfile => $file1 });
1665        }
1666        # otherwise we just use the working copy revision
1667        else
1668        {
1669            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1670            $meta1 = $updater->getmeta($filename, $wrev);
1671            transmitfile($meta1->{filehash}, { targetfile => $file1 });
1672        }
1673
1674        # if we have a second -r switch, use it too
1675        if ( defined ( $revision2 ) )
1676        {
1677            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1678            $meta2 = $updater->getmeta($filename, $revision2);
1679
1680            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1681            {
1682                print "E File $filename at revision 1.$revision2 doesn't exist\n";
1683                next;
1684            }
1685
1686            transmitfile($meta2->{filehash}, { targetfile => $file2 });
1687        }
1688        # otherwise we just use the working copy
1689        else
1690        {
1691            $file2 = $state->{entries}{$filename}{modified_filename};
1692        }
1693
1694        # if we have been given -r, and we don't have a $file2 yet, lets get one
1695        if ( defined ( $revision1 ) and not defined ( $file2 ) )
1696        {
1697            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1698            $meta2 = $updater->getmeta($filename, $wrev);
1699            transmitfile($meta2->{filehash}, { targetfile => $file2 });
1700        }
1701
1702        # We need to have retrieved something useful
1703        next unless ( defined ( $meta1 ) );
1704
1705        # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1706        next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1707                  and
1708                   ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1709                     or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1710                  );
1711
1712        # Apparently we only show diffs for locally modified files
1713        next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1714
1715        print "M Index: $filename\n";
1716        print "M ===================================================================\n";
1717        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1718        print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1719        print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1720        print "M diff ";
1721        foreach my $opt ( keys %{$state->{opt}} )
1722        {
1723            if ( ref $state->{opt}{$opt} eq "ARRAY" )
1724            {
1725                foreach my $value ( @{$state->{opt}{$opt}} )
1726                {
1727                    print "-$opt $value ";
1728                }
1729            } else {
1730                print "-$opt ";
1731                print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1732            }
1733        }
1734        print "$filename\n";
1735
1736        $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1737
1738        ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1739
1740        if ( exists $state->{opt}{u} )
1741        {
1742            system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1743        } else {
1744            system("diff $file1 $file2 > $filediff");
1745        }
1746
1747        while ( <$fh> )
1748        {
1749            print "M $_";
1750        }
1751        close $fh;
1752    }
1753
1754    print "ok\n";
1755}
1756
1757sub req_log
1758{
1759    my ( $cmd, $data ) = @_;
1760
1761    argsplit("log");
1762
1763    $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1764    #$log->debug("log state : " . Dumper($state));
1765
1766    my ( $minrev, $maxrev );
1767    if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1768    {
1769        my $control = $2;
1770        $minrev = $1;
1771        $maxrev = $3;
1772        $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1773        $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1774        $minrev++ if ( defined($minrev) and $control eq "::" );
1775    }
1776
1777    # Grab a handle to the SQLite db and do any necessary updates
1778    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1779    $updater->update();
1780
1781    # if no files were specified, we need to work out what files we should be providing status on ...
1782    argsfromdir($updater);
1783
1784    # foreach file specified on the command line ...
1785    foreach my $filename ( @{$state->{args}} )
1786    {
1787        $filename = filecleanup($filename);
1788
1789        my $headmeta = $updater->getmeta($filename);
1790
1791        my $revisions = $updater->getlog($filename);
1792        my $totalrevisions = scalar(@$revisions);
1793
1794        if ( defined ( $minrev ) )
1795        {
1796            $log->debug("Removing revisions less than $minrev");
1797            while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1798            {
1799                pop @$revisions;
1800            }
1801        }
1802        if ( defined ( $maxrev ) )
1803        {
1804            $log->debug("Removing revisions greater than $maxrev");
1805            while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1806            {
1807                shift @$revisions;
1808            }
1809        }
1810
1811        next unless ( scalar(@$revisions) );
1812
1813        print "M \n";
1814        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1815        print "M Working file: $filename\n";
1816        print "M head: 1.$headmeta->{revision}\n";
1817        print "M branch:\n";
1818        print "M locks: strict\n";
1819        print "M access list:\n";
1820        print "M symbolic names:\n";
1821        print "M keyword substitution: kv\n";
1822        print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1823        print "M description:\n";
1824
1825        foreach my $revision ( @$revisions )
1826        {
1827            print "M ----------------------------\n";
1828            print "M revision 1.$revision->{revision}\n";
1829            # reformat the date for log output
1830            $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}) );
1831            $revision->{author} = cvs_author($revision->{author});
1832            print "M date: $revision->{modified};  author: $revision->{author};  state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . ";  lines: +2 -3\n";
1833            my $commitmessage = $updater->commitmessage($revision->{commithash});
1834            $commitmessage =~ s/^/M /mg;
1835            print $commitmessage . "\n";
1836        }
1837        print "M =============================================================================\n";
1838    }
1839
1840    print "ok\n";
1841}
1842
1843sub req_annotate
1844{
1845    my ( $cmd, $data ) = @_;
1846
1847    argsplit("annotate");
1848
1849    $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1850    #$log->debug("status state : " . Dumper($state));
1851
1852    # Grab a handle to the SQLite db and do any necessary updates
1853    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1854    $updater->update();
1855
1856    # if no files were specified, we need to work out what files we should be providing annotate on ...
1857    argsfromdir($updater);
1858
1859    # we'll need a temporary checkout dir
1860    setupWorkTree();
1861
1862    $log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");
1863
1864    # foreach file specified on the command line ...
1865    foreach my $filename ( @{$state->{args}} )
1866    {
1867        $filename = filecleanup($filename);
1868
1869        my $meta = $updater->getmeta($filename);
1870
1871        next unless ( $meta->{revision} );
1872
1873        # get all the commits that this file was in
1874        # in dense format -- aka skip dead revisions
1875        my $revisions   = $updater->gethistorydense($filename);
1876        my $lastseenin  = $revisions->[0][2];
1877
1878        # populate the temporary index based on the latest commit were we saw
1879        # the file -- but do it cheaply without checking out any files
1880        # TODO: if we got a revision from the client, use that instead
1881        # to look up the commithash in sqlite (still good to default to
1882        # the current head as we do now)
1883        system("git", "read-tree", $lastseenin);
1884        unless ($? == 0)
1885        {
1886            print "E error running git-read-tree $lastseenin $ENV{GIT_INDEX_FILE} $!\n";
1887            return;
1888        }
1889        $log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit $lastseenin - exit status $?");
1890
1891        # do a checkout of the file
1892        system('git', 'checkout-index', '-f', '-u', $filename);
1893        unless ($? == 0) {
1894            print "E error running git-checkout-index -f -u $filename : $!\n";
1895            return;
1896        }
1897
1898        $log->info("Annotate $filename");
1899
1900        # Prepare a file with the commits from the linearized
1901        # history that annotate should know about. This prevents
1902        # git-jsannotate telling us about commits we are hiding
1903        # from the client.
1904
1905        my $a_hints = "$work->{workDir}/.annotate_hints";
1906        if (!open(ANNOTATEHINTS, '>', $a_hints)) {
1907            print "E failed to open '$a_hints' for writing: $!\n";
1908            return;
1909        }
1910        for (my $i=0; $i < @$revisions; $i++)
1911        {
1912            print ANNOTATEHINTS $revisions->[$i][2];
1913            if ($i+1 < @$revisions) { # have we got a parent?
1914                print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1915            }
1916            print ANNOTATEHINTS "\n";
1917        }
1918
1919        print ANNOTATEHINTS "\n";
1920        close ANNOTATEHINTS
1921            or (print "E failed to write $a_hints: $!\n"), return;
1922
1923        my @cmd = (qw(git annotate -l -S), $a_hints, $filename);
1924        if (!open(ANNOTATE, "-|", @cmd)) {
1925            print "E error invoking ". join(' ',@cmd) .": $!\n";
1926            return;
1927        }
1928        my $metadata = {};
1929        print "E Annotations for $filename\n";
1930        print "E ***************\n";
1931        while ( <ANNOTATE> )
1932        {
1933            if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1934            {
1935                my $commithash = $1;
1936                my $data = $2;
1937                unless ( defined ( $metadata->{$commithash} ) )
1938                {
1939                    $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1940                    $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
1941                    $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1942                }
1943                printf("M 1.%-5d      (%-8s %10s): %s\n",
1944                    $metadata->{$commithash}{revision},
1945                    $metadata->{$commithash}{author},
1946                    $metadata->{$commithash}{modified},
1947                    $data
1948                );
1949            } else {
1950                $log->warn("Error in annotate output! LINE: $_");
1951                print "E Annotate error \n";
1952                next;
1953            }
1954        }
1955        close ANNOTATE;
1956    }
1957
1958    # done; get out of the tempdir
1959    cleanupWorkTree();
1960
1961    print "ok\n";
1962
1963}
1964
1965# This method takes the state->{arguments} array and produces two new arrays.
1966# The first is $state->{args} which is everything before the '--' argument, and
1967# the second is $state->{files} which is everything after it.
1968sub argsplit
1969{
1970    $state->{args} = [];
1971    $state->{files} = [];
1972    $state->{opt} = {};
1973
1974    return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1975
1976    my $type = shift;
1977
1978    if ( defined($type) )
1979    {
1980        my $opt = {};
1981        $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
1982        $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1983        $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
1984        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1985        $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1986        $opt = { k => 1, m => 1 } if ( $type eq "add" );
1987        $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1988        $opt = { l => 0, b => 0, h => 0, R => 0, t => 0, N => 0, S => 0, r => 1, d => 1, s => 1, w => 1 } if ( $type eq "log" );
1989
1990
1991        while ( scalar ( @{$state->{arguments}} ) > 0 )
1992        {
1993            my $arg = shift @{$state->{arguments}};
1994
1995            next if ( $arg eq "--" );
1996            next unless ( $arg =~ /\S/ );
1997
1998            # if the argument looks like a switch
1999            if ( $arg =~ /^-(\w)(.*)/ )
2000            {
2001                # if it's a switch that takes an argument
2002                if ( $opt->{$1} )
2003                {
2004                    # If this switch has already been provided
2005                    if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
2006                    {
2007                        $state->{opt}{$1} = [ $state->{opt}{$1} ];
2008                        if ( length($2) > 0 )
2009                        {
2010                            push @{$state->{opt}{$1}},$2;
2011                        } else {
2012                            push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
2013                        }
2014                    } else {
2015                        # if there's extra data in the arg, use that as the argument for the switch
2016                        if ( length($2) > 0 )
2017                        {
2018                            $state->{opt}{$1} = $2;
2019                        } else {
2020                            $state->{opt}{$1} = shift @{$state->{arguments}};
2021                        }
2022                    }
2023                } else {
2024                    $state->{opt}{$1} = undef;
2025                }
2026            }
2027            else
2028            {
2029                push @{$state->{args}}, $arg;
2030            }
2031        }
2032    }
2033    else
2034    {
2035        my $mode = 0;
2036
2037        foreach my $value ( @{$state->{arguments}} )
2038        {
2039            if ( $value eq "--" )
2040            {
2041                $mode++;
2042                next;
2043            }
2044            push @{$state->{args}}, $value if ( $mode == 0 );
2045            push @{$state->{files}}, $value if ( $mode == 1 );
2046        }
2047    }
2048}
2049
2050# This method uses $state->{directory} to populate $state->{args} with a list of filenames
2051sub argsfromdir
2052{
2053    my $updater = shift;
2054
2055    $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
2056
2057    return if ( scalar ( @{$state->{args}} ) > 1 );
2058
2059    my @gethead = @{$updater->gethead};
2060
2061    # push added files
2062    foreach my $file (keys %{$state->{entries}}) {
2063        if ( exists $state->{entries}{$file}{revision} &&
2064                $state->{entries}{$file}{revision} == 0 )
2065        {
2066            push @gethead, { name => $file, filehash => 'added' };
2067        }
2068    }
2069
2070    if ( scalar(@{$state->{args}}) == 1 )
2071    {
2072        my $arg = $state->{args}[0];
2073        $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
2074
2075        $log->info("Only one arg specified, checking for directory expansion on '$arg'");
2076
2077        foreach my $file ( @gethead )
2078        {
2079            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2080            next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg  );
2081            push @{$state->{args}}, $file->{name};
2082        }
2083
2084        shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
2085    } else {
2086        $log->info("Only one arg specified, populating file list automatically");
2087
2088        $state->{args} = [];
2089
2090        foreach my $file ( @gethead )
2091        {
2092            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2093            next unless ( $file->{name} =~ s/^$state->{prependdir}// );
2094            push @{$state->{args}}, $file->{name};
2095        }
2096    }
2097}
2098
2099# This method cleans up the $state variable after a command that uses arguments has run
2100sub statecleanup
2101{
2102    $state->{files} = [];
2103    $state->{args} = [];
2104    $state->{arguments} = [];
2105    $state->{entries} = {};
2106}
2107
2108sub revparse
2109{
2110    my $filename = shift;
2111
2112    return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
2113
2114    return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
2115    return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
2116
2117    return undef;
2118}
2119
2120# This method takes a file hash and does a CVS "file transfer".  Its
2121# exact behaviour depends on a second, optional hash table argument:
2122# - If $options->{targetfile}, dump the contents to that file;
2123# - If $options->{print}, use M/MT to transmit the contents one line
2124#   at a time;
2125# - Otherwise, transmit the size of the file, followed by the file
2126#   contents.
2127sub transmitfile
2128{
2129    my $filehash = shift;
2130    my $options = shift;
2131
2132    if ( defined ( $filehash ) and $filehash eq "deleted" )
2133    {
2134        $log->warn("filehash is 'deleted'");
2135        return;
2136    }
2137
2138    die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2139
2140    my $type = `git cat-file -t $filehash`;
2141    chomp $type;
2142
2143    die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2144
2145    my $size = `git cat-file -s $filehash`;
2146    chomp $size;
2147
2148    $log->debug("transmitfile($filehash) size=$size, type=$type");
2149
2150    if ( open my $fh, '-|', "git", "cat-file", "blob", $filehash )
2151    {
2152        if ( defined ( $options->{targetfile} ) )
2153        {
2154            my $targetfile = $options->{targetfile};
2155            open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2156            print NEWFILE $_ while ( <$fh> );
2157            close NEWFILE or die("Failed to write '$targetfile': $!");
2158        } elsif ( defined ( $options->{print} ) && $options->{print} ) {
2159            while ( <$fh> ) {
2160                if( /\n\z/ ) {
2161                    print 'M ', $_;
2162                } else {
2163                    print 'MT text ', $_, "\n";
2164                }
2165            }
2166        } else {
2167            print "$size\n";
2168            print while ( <$fh> );
2169        }
2170        close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
2171    } else {
2172        die("Couldn't execute git-cat-file");
2173    }
2174}
2175
2176# This method takes a file name, and returns ( $dirpart, $filepart ) which
2177# refers to the directory portion and the file portion of the filename
2178# respectively
2179sub filenamesplit
2180{
2181    my $filename = shift;
2182    my $fixforlocaldir = shift;
2183
2184    my ( $filepart, $dirpart ) = ( $filename, "." );
2185    ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2186    $dirpart .= "/";
2187
2188    if ( $fixforlocaldir )
2189    {
2190        $dirpart =~ s/^$state->{prependdir}//;
2191    }
2192
2193    return ( $filepart, $dirpart );
2194}
2195
2196sub filecleanup
2197{
2198    my $filename = shift;
2199
2200    return undef unless(defined($filename));
2201    if ( $filename =~ /^\// )
2202    {
2203        print "E absolute filenames '$filename' not supported by server\n";
2204        return undef;
2205    }
2206
2207    $filename =~ s/^\.\///g;
2208    $filename = $state->{prependdir} . $filename;
2209    return $filename;
2210}
2211
2212sub validateGitDir
2213{
2214    if( !defined($state->{CVSROOT}) )
2215    {
2216        print "error 1 CVSROOT not specified\n";
2217        cleanupWorkTree();
2218        exit;
2219    }
2220    if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') )
2221    {
2222        print "error 1 Internally inconsistent CVSROOT\n";
2223        cleanupWorkTree();
2224        exit;
2225    }
2226}
2227
2228# Setup working directory in a work tree with the requested version
2229# loaded in the index.
2230sub setupWorkTree
2231{
2232    my ($ver) = @_;
2233
2234    validateGitDir();
2235
2236    if( ( defined($work->{state}) && $work->{state} != 1 ) ||
2237        defined($work->{tmpDir}) )
2238    {
2239        $log->warn("Bad work tree state management");
2240        print "error 1 Internal setup multiple work trees without cleanup\n";
2241        cleanupWorkTree();
2242        exit;
2243    }
2244
2245    $work->{workDir} = tempdir ( DIR => $TEMP_DIR );
2246
2247    if( !defined($work->{index}) )
2248    {
2249        (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2250    }
2251
2252    chdir $work->{workDir} or
2253        die "Unable to chdir to $work->{workDir}\n";
2254
2255    $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");
2256
2257    $ENV{GIT_WORK_TREE} = ".";
2258    $ENV{GIT_INDEX_FILE} = $work->{index};
2259    $work->{state} = 2;
2260
2261    if($ver)
2262    {
2263        system("git","read-tree",$ver);
2264        unless ($? == 0)
2265        {
2266            $log->warn("Error running git-read-tree");
2267            die "Error running git-read-tree $ver in $work->{workDir} $!\n";
2268        }
2269    }
2270    # else # req_annotate reads tree for each file
2271}
2272
2273# Ensure current directory is in some kind of working directory,
2274# with a recent version loaded in the index.
2275sub ensureWorkTree
2276{
2277    if( defined($work->{tmpDir}) )
2278    {
2279        $log->warn("Bad work tree state management [ensureWorkTree()]");
2280        print "error 1 Internal setup multiple dirs without cleanup\n";
2281        cleanupWorkTree();
2282        exit;
2283    }
2284    if( $work->{state} )
2285    {
2286        return;
2287    }
2288
2289    validateGitDir();
2290
2291    if( !defined($work->{emptyDir}) )
2292    {
2293        $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0);
2294    }
2295    chdir $work->{emptyDir} or
2296        die "Unable to chdir to $work->{emptyDir}\n";
2297
2298    my $ver = `git show-ref -s refs/heads/$state->{module}`;
2299    chomp $ver;
2300    if ($ver !~ /^[0-9a-f]{40}$/)
2301    {
2302        $log->warn("Error from git show-ref -s refs/head$state->{module}");
2303        print "error 1 cannot find the current HEAD of module";
2304        cleanupWorkTree();
2305        exit;
2306    }
2307
2308    if( !defined($work->{index}) )
2309    {
2310        (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2311    }
2312
2313    $ENV{GIT_WORK_TREE} = ".";
2314    $ENV{GIT_INDEX_FILE} = $work->{index};
2315    $work->{state} = 1;
2316
2317    system("git","read-tree",$ver);
2318    unless ($? == 0)
2319    {
2320        die "Error running git-read-tree $ver $!\n";
2321    }
2322}
2323
2324# Cleanup working directory that is not needed any longer.
2325sub cleanupWorkTree
2326{
2327    if( ! $work->{state} )
2328    {
2329        return;
2330    }
2331
2332    chdir "/" or die "Unable to chdir '/'\n";
2333
2334    if( defined($work->{workDir}) )
2335    {
2336        rmtree( $work->{workDir} );
2337        undef $work->{workDir};
2338    }
2339    undef $work->{state};
2340}
2341
2342# Setup a temporary directory (not a working tree), typically for
2343# merging dirty state as in req_update.
2344sub setupTmpDir
2345{
2346    $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR );
2347    chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n";
2348
2349    return $work->{tmpDir};
2350}
2351
2352# Clean up a previously setupTmpDir.  Restore previous work tree if
2353# appropriate.
2354sub cleanupTmpDir
2355{
2356    if ( !defined($work->{tmpDir}) )
2357    {
2358        $log->warn("cleanup tmpdir that has not been setup");
2359        die "Cleanup tmpDir that has not been setup\n";
2360    }
2361    if( defined($work->{state}) )
2362    {
2363        if( $work->{state} == 1 )
2364        {
2365            chdir $work->{emptyDir} or
2366                die "Unable to chdir to $work->{emptyDir}\n";
2367        }
2368        elsif( $work->{state} == 2 )
2369        {
2370            chdir $work->{workDir} or
2371                die "Unable to chdir to $work->{emptyDir}\n";
2372        }
2373        else
2374        {
2375            $log->warn("Inconsistent work dir state");
2376            die "Inconsistent work dir state\n";
2377        }
2378    }
2379    else
2380    {
2381        chdir "/" or die "Unable to chdir '/'\n";
2382    }
2383}
2384
2385# Given a path, this function returns a string containing the kopts
2386# that should go into that path's Entries line.  For example, a binary
2387# file should get -kb.
2388sub kopts_from_path
2389{
2390    my ($path, $srcType, $name) = @_;
2391
2392    if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and
2393         $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i )
2394    {
2395        my ($val) = check_attr( "crlf", $path );
2396        if ( $val eq "set" )
2397        {
2398            return "";
2399        }
2400        elsif ( $val eq "unset" )
2401        {
2402            return "-kb"
2403        }
2404        else
2405        {
2406            $log->info("Unrecognized check_attr crlf $path : $val");
2407        }
2408    }
2409
2410    if ( defined ( $cfg->{gitcvs}{allbinary} ) )
2411    {
2412        if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) )
2413        {
2414            return "-kb";
2415        }
2416        elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
2417        {
2418            if( $srcType eq "sha1Or-k" &&
2419                !defined($name) )
2420            {
2421                my ($ret)=$state->{entries}{$path}{options};
2422                if( !defined($ret) )
2423                {
2424                    $ret=$state->{opt}{k};
2425                    if(defined($ret))
2426                    {
2427                        $ret="-k$ret";
2428                    }
2429                    else
2430                    {
2431                        $ret="";
2432                    }
2433                }
2434                if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) )
2435                {
2436                    print "E Bad -k option\n";
2437                    $log->warn("Bad -k option: $ret");
2438                    die "Error: Bad -k option: $ret\n";
2439                }
2440
2441                return $ret;
2442            }
2443            else
2444            {
2445                if( is_binary($srcType,$name) )
2446                {
2447                    $log->debug("... as binary");
2448                    return "-kb";
2449                }
2450                else
2451                {
2452                    $log->debug("... as text");
2453                }
2454            }
2455        }
2456    }
2457    # Return "" to give no special treatment to any path
2458    return "";
2459}
2460
2461sub check_attr
2462{
2463    my ($attr,$path) = @_;
2464    ensureWorkTree();
2465    if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path )
2466    {
2467        my $val = <$fh>;
2468        close $fh;
2469        $val =~ s/.*: ([^:\r\n]*)\s*$/$1/;
2470        return $val;
2471    }
2472    else
2473    {
2474        return undef;
2475    }
2476}
2477
2478# This should have the same heuristics as convert.c:is_binary() and related.
2479# Note that the bare CR test is done by callers in convert.c.
2480sub is_binary
2481{
2482    my ($srcType,$name) = @_;
2483    $log->debug("is_binary($srcType,$name)");
2484
2485    # Minimize amount of interpreted code run in the inner per-character
2486    # loop for large files, by totalling each character value and
2487    # then analyzing the totals.
2488    my @counts;
2489    my $i;
2490    for($i=0;$i<256;$i++)
2491    {
2492        $counts[$i]=0;
2493    }
2494
2495    my $fh = open_blob_or_die($srcType,$name);
2496    my $line;
2497    while( defined($line=<$fh>) )
2498    {
2499        # Any '\0' and bare CR are considered binary.
2500        if( $line =~ /\0|(\r[^\n])/ )
2501        {
2502            close($fh);
2503            return 1;
2504        }
2505
2506        # Count up each character in the line:
2507        my $len=length($line);
2508        for($i=0;$i<$len;$i++)
2509        {
2510            $counts[ord(substr($line,$i,1))]++;
2511        }
2512    }
2513    close $fh;
2514
2515    # Don't count CR and LF as either printable/nonprintable
2516    $counts[ord("\n")]=0;
2517    $counts[ord("\r")]=0;
2518
2519    # Categorize individual character count into printable and nonprintable:
2520    my $printable=0;
2521    my $nonprintable=0;
2522    for($i=0;$i<256;$i++)
2523    {
2524        if( $i < 32 &&
2525            $i != ord("\b") &&
2526            $i != ord("\t") &&
2527            $i != 033 &&       # ESC
2528            $i != 014 )        # FF
2529        {
2530            $nonprintable+=$counts[$i];
2531        }
2532        elsif( $i==127 )  # DEL
2533        {
2534            $nonprintable+=$counts[$i];
2535        }
2536        else
2537        {
2538            $printable+=$counts[$i];
2539        }
2540    }
2541
2542    return ($printable >> 7) < $nonprintable;
2543}
2544
2545# Returns open file handle.  Possible invocations:
2546#  - open_blob_or_die("file",$filename);
2547#  - open_blob_or_die("sha1",$filehash);
2548sub open_blob_or_die
2549{
2550    my ($srcType,$name) = @_;
2551    my ($fh);
2552    if( $srcType eq "file" )
2553    {
2554        if( !open $fh,"<",$name )
2555        {
2556            $log->warn("Unable to open file $name: $!");
2557            die "Unable to open file $name: $!\n";
2558        }
2559    }
2560    elsif( $srcType eq "sha1" || $srcType eq "sha1Or-k" )
2561    {
2562        unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
2563        {
2564            $log->warn("Need filehash");
2565            die "Need filehash\n";
2566        }
2567
2568        my $type = `git cat-file -t $name`;
2569        chomp $type;
2570
2571        unless ( defined ( $type ) and $type eq "blob" )
2572        {
2573            $log->warn("Invalid type '$type' for '$name'");
2574            die ( "Invalid type '$type' (expected 'blob')" )
2575        }
2576
2577        my $size = `git cat-file -s $name`;
2578        chomp $size;
2579
2580        $log->debug("open_blob_or_die($name) size=$size, type=$type");
2581
2582        unless( open $fh, '-|', "git", "cat-file", "blob", $name )
2583        {
2584            $log->warn("Unable to open sha1 $name");
2585            die "Unable to open sha1 $name\n";
2586        }
2587    }
2588    else
2589    {
2590        $log->warn("Unknown type of blob source: $srcType");
2591        die "Unknown type of blob source: $srcType\n";
2592    }
2593    return $fh;
2594}
2595
2596# Generate a CVS author name from Git author information, by taking the local
2597# part of the email address and replacing characters not in the Portable
2598# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS
2599# Login names are Unix login names, which should be restricted to this
2600# character set.
2601sub cvs_author
2602{
2603    my $author_line = shift;
2604    (my $author) = $author_line =~ /<([^@>]*)/;
2605
2606    $author =~ s/[^-a-zA-Z0-9_.]/_/g;
2607    $author =~ s/^-/_/;
2608
2609    $author;
2610}
2611
2612
2613sub descramble
2614{
2615    # This table is from src/scramble.c in the CVS source
2616    my @SHIFTS = (
2617        0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
2618        16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2619        114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87,
2620        111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105,
2621        41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35,
2622        125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56,
2623        36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48,
2624        58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223,
2625        225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,
2626        199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,
2627        174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,
2628        207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,
2629        192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,
2630        227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,
2631        182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,
2632        243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152
2633    );
2634    my ($str) = @_;
2635
2636    # This should never happen, the same password format (A) bas been
2637    # used by CVS since the beginning of time
2638    die "invalid password format $1" unless substr($str, 0, 1) eq 'A';
2639
2640    my @str = unpack "C*", substr($str, 1);
2641    my $ret = join '', map { chr $SHIFTS[$_] } @str;
2642    return $ret;
2643}
2644
2645
2646package GITCVS::log;
2647
2648####
2649#### Copyright The Open University UK - 2006.
2650####
2651#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
2652####          Martin Langhoff <martin@catalyst.net.nz>
2653####
2654####
2655
2656use strict;
2657use warnings;
2658
2659=head1 NAME
2660
2661GITCVS::log
2662
2663=head1 DESCRIPTION
2664
2665This module provides very crude logging with a similar interface to
2666Log::Log4perl
2667
2668=head1 METHODS
2669
2670=cut
2671
2672=head2 new
2673
2674Creates a new log object, optionally you can specify a filename here to
2675indicate the file to log to. If no log file is specified, you can specify one
2676later with method setfile, or indicate you no longer want logging with method
2677nofile.
2678
2679Until one of these methods is called, all log calls will buffer messages ready
2680to write out.
2681
2682=cut
2683sub new
2684{
2685    my $class = shift;
2686    my $filename = shift;
2687
2688    my $self = {};
2689
2690    bless $self, $class;
2691
2692    if ( defined ( $filename ) )
2693    {
2694        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2695    }
2696
2697    return $self;
2698}
2699
2700=head2 setfile
2701
2702This methods takes a filename, and attempts to open that file as the log file.
2703If successful, all buffered data is written out to the file, and any further
2704logging is written directly to the file.
2705
2706=cut
2707sub setfile
2708{
2709    my $self = shift;
2710    my $filename = shift;
2711
2712    if ( defined ( $filename ) )
2713    {
2714        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2715    }
2716
2717    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2718
2719    while ( my $line = shift @{$self->{buffer}} )
2720    {
2721        print {$self->{fh}} $line;
2722    }
2723}
2724
2725=head2 nofile
2726
2727This method indicates no logging is going to be used. It flushes any entries in
2728the internal buffer, and sets a flag to ensure no further data is put there.
2729
2730=cut
2731sub nofile
2732{
2733    my $self = shift;
2734
2735    $self->{nolog} = 1;
2736
2737    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2738
2739    $self->{buffer} = [];
2740}
2741
2742=head2 _logopen
2743
2744Internal method. Returns true if the log file is open, false otherwise.
2745
2746=cut
2747sub _logopen
2748{
2749    my $self = shift;
2750
2751    return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2752    return 0;
2753}
2754
2755=head2 debug info warn fatal
2756
2757These four methods are wrappers to _log. They provide the actual interface for
2758logging data.
2759
2760=cut
2761sub debug { my $self = shift; $self->_log("debug", @_); }
2762sub info  { my $self = shift; $self->_log("info" , @_); }
2763sub warn  { my $self = shift; $self->_log("warn" , @_); }
2764sub fatal { my $self = shift; $self->_log("fatal", @_); }
2765
2766=head2 _log
2767
2768This is an internal method called by the logging functions. It generates a
2769timestamp and pushes the logged line either to file, or internal buffer.
2770
2771=cut
2772sub _log
2773{
2774    my $self = shift;
2775    my $level = shift;
2776
2777    return if ( $self->{nolog} );
2778
2779    my @time = localtime;
2780    my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2781        $time[5] + 1900,
2782        $time[4] + 1,
2783        $time[3],
2784        $time[2],
2785        $time[1],
2786        $time[0],
2787        uc $level,
2788    );
2789
2790    if ( $self->_logopen )
2791    {
2792        print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2793    } else {
2794        push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2795    }
2796}
2797
2798=head2 DESTROY
2799
2800This method simply closes the file handle if one is open
2801
2802=cut
2803sub DESTROY
2804{
2805    my $self = shift;
2806
2807    if ( $self->_logopen )
2808    {
2809        close $self->{fh};
2810    }
2811}
2812
2813package GITCVS::updater;
2814
2815####
2816#### Copyright The Open University UK - 2006.
2817####
2818#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
2819####          Martin Langhoff <martin@catalyst.net.nz>
2820####
2821####
2822
2823use strict;
2824use warnings;
2825use DBI;
2826
2827=head1 METHODS
2828
2829=cut
2830
2831=head2 new
2832
2833=cut
2834sub new
2835{
2836    my $class = shift;
2837    my $config = shift;
2838    my $module = shift;
2839    my $log = shift;
2840
2841    die "Need to specify a git repository" unless ( defined($config) and -d $config );
2842    die "Need to specify a module" unless ( defined($module) );
2843
2844    $class = ref($class) || $class;
2845
2846    my $self = {};
2847
2848    bless $self, $class;
2849
2850    $self->{valid_tables} = {'revision' => 1,
2851                             'revision_ix1' => 1,
2852                             'revision_ix2' => 1,
2853                             'head' => 1,
2854                             'head_ix1' => 1,
2855                             'properties' => 1,
2856                             'commitmsgs' => 1};
2857
2858    $self->{module} = $module;
2859    $self->{git_path} = $config . "/";
2860
2861    $self->{log} = $log;
2862
2863    die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2864
2865    $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
2866        $cfg->{gitcvs}{dbdriver} || "SQLite";
2867    $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2868        $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2869    $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2870        $cfg->{gitcvs}{dbuser} || "";
2871    $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2872        $cfg->{gitcvs}{dbpass} || "";
2873    $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||
2874        $cfg->{gitcvs}{dbtablenameprefix} || "";
2875    my %mapping = ( m => $module,
2876                    a => $state->{method},
2877                    u => getlogin || getpwuid($<) || $<,
2878                    G => $self->{git_path},
2879                    g => mangle_dirname($self->{git_path}),
2880                    );
2881    $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2882    $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
2883    $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg;
2884    $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});
2885
2886    die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2887    die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2888    $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
2889                                $self->{dbuser},
2890                                $self->{dbpass});
2891    die "Error connecting to database\n" unless defined $self->{dbh};
2892
2893    $self->{tables} = {};
2894    foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
2895    {
2896        $self->{tables}{$table} = 1;
2897    }
2898
2899    # Construct the revision table if required
2900    unless ( $self->{tables}{$self->tablename("revision")} )
2901    {
2902        my $tablename = $self->tablename("revision");
2903        my $ix1name = $self->tablename("revision_ix1");
2904        my $ix2name = $self->tablename("revision_ix2");
2905        $self->{dbh}->do("
2906            CREATE TABLE $tablename (
2907                name       TEXT NOT NULL,
2908                revision   INTEGER NOT NULL,
2909                filehash   TEXT NOT NULL,
2910                commithash TEXT NOT NULL,
2911                author     TEXT NOT NULL,
2912                modified   TEXT NOT NULL,
2913                mode       TEXT NOT NULL
2914            )
2915        ");
2916        $self->{dbh}->do("
2917            CREATE INDEX $ix1name
2918            ON $tablename (name,revision)
2919        ");
2920        $self->{dbh}->do("
2921            CREATE INDEX $ix2name
2922            ON $tablename (name,commithash)
2923        ");
2924    }
2925
2926    # Construct the head table if required
2927    unless ( $self->{tables}{$self->tablename("head")} )
2928    {
2929        my $tablename = $self->tablename("head");
2930        my $ix1name = $self->tablename("head_ix1");
2931        $self->{dbh}->do("
2932            CREATE TABLE $tablename (
2933                name       TEXT NOT NULL,
2934                revision   INTEGER NOT NULL,
2935                filehash   TEXT NOT NULL,
2936                commithash TEXT NOT NULL,
2937                author     TEXT NOT NULL,
2938                modified   TEXT NOT NULL,
2939                mode       TEXT NOT NULL
2940            )
2941        ");
2942        $self->{dbh}->do("
2943            CREATE INDEX $ix1name
2944            ON $tablename (name)
2945        ");
2946    }
2947
2948    # Construct the properties table if required
2949    unless ( $self->{tables}{$self->tablename("properties")} )
2950    {
2951        my $tablename = $self->tablename("properties");
2952        $self->{dbh}->do("
2953            CREATE TABLE $tablename (
2954                key        TEXT NOT NULL PRIMARY KEY,
2955                value      TEXT
2956            )
2957        ");
2958    }
2959
2960    # Construct the commitmsgs table if required
2961    unless ( $self->{tables}{$self->tablename("commitmsgs")} )
2962    {
2963        my $tablename = $self->tablename("commitmsgs");
2964        $self->{dbh}->do("
2965            CREATE TABLE $tablename (
2966                key        TEXT NOT NULL PRIMARY KEY,
2967                value      TEXT
2968            )
2969        ");
2970    }
2971
2972    return $self;
2973}
2974
2975=head2 tablename
2976
2977=cut
2978sub tablename
2979{
2980    my $self = shift;
2981    my $name = shift;
2982
2983    if (exists $self->{valid_tables}{$name}) {
2984        return $self->{dbtablenameprefix} . $name;
2985    } else {
2986        return undef;
2987    }
2988}
2989
2990=head2 update
2991
2992=cut
2993sub update
2994{
2995    my $self = shift;
2996
2997    # first lets get the commit list
2998    $ENV{GIT_DIR} = $self->{git_path};
2999
3000    my $commitsha1 = `git rev-parse $self->{module}`;
3001    chomp $commitsha1;
3002
3003    my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
3004    unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
3005    {
3006        die("Invalid module '$self->{module}'");
3007    }
3008
3009
3010    my $git_log;
3011    my $lastcommit = $self->_get_prop("last_commit");
3012
3013    if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
3014         return 1;
3015    }
3016
3017    # Start exclusive lock here...
3018    $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
3019
3020    # TODO: log processing is memory bound
3021    # if we can parse into a 2nd file that is in reverse order
3022    # we can probably do something really efficient
3023    my @git_log_params = ('--pretty', '--parents', '--topo-order');
3024
3025    if (defined $lastcommit) {
3026        push @git_log_params, "$lastcommit..$self->{module}";
3027    } else {
3028        push @git_log_params, $self->{module};
3029    }
3030    # git-rev-list is the backend / plumbing version of git-log
3031    open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
3032
3033    my @commits;
3034
3035    my %commit = ();
3036
3037    while ( <GITLOG> )
3038    {
3039        chomp;
3040        if (m/^commit\s+(.*)$/) {
3041            # on ^commit lines put the just seen commit in the stack
3042            # and prime things for the next one
3043            if (keys %commit) {
3044                my %copy = %commit;
3045                unshift @commits, \%copy;
3046                %commit = ();
3047            }
3048            my @parents = split(m/\s+/, $1);
3049            $commit{hash} = shift @parents;
3050            $commit{parents} = \@parents;
3051        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
3052            # on rfc822-like lines seen before we see any message,
3053            # lowercase the entry and put it in the hash as key-value
3054            $commit{lc($1)} = $2;
3055        } else {
3056            # message lines - skip initial empty line
3057            # and trim whitespace
3058            if (!exists($commit{message}) && m/^\s*$/) {
3059                # define it to mark the end of headers
3060                $commit{message} = '';
3061                next;
3062            }
3063            s/^\s+//; s/\s+$//; # trim ws
3064            $commit{message} .= $_ . "\n";
3065        }
3066    }
3067    close GITLOG;
3068
3069    unshift @commits, \%commit if ( keys %commit );
3070
3071    # Now all the commits are in the @commits bucket
3072    # ordered by time DESC. for each commit that needs processing,
3073    # determine whether it's following the last head we've seen or if
3074    # it's on its own branch, grab a file list, and add whatever's changed
3075    # NOTE: $lastcommit refers to the last commit from previous run
3076    #       $lastpicked is the last commit we picked in this run
3077    my $lastpicked;
3078    my $head = {};
3079    if (defined $lastcommit) {
3080        $lastpicked = $lastcommit;
3081    }
3082
3083    my $committotal = scalar(@commits);
3084    my $commitcount = 0;
3085
3086    # Load the head table into $head (for cached lookups during the update process)
3087    foreach my $file ( @{$self->gethead()} )
3088    {
3089        $head->{$file->{name}} = $file;
3090    }
3091
3092    foreach my $commit ( @commits )
3093    {
3094        $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
3095        if (defined $lastpicked)
3096        {
3097            if (!in_array($lastpicked, @{$commit->{parents}}))
3098            {
3099                # skip, we'll see this delta
3100                # as part of a merge later
3101                # warn "skipping off-track  $commit->{hash}\n";
3102                next;
3103            } elsif (@{$commit->{parents}} > 1) {
3104                # it is a merge commit, for each parent that is
3105                # not $lastpicked, see if we can get a log
3106                # from the merge-base to that parent to put it
3107                # in the message as a merge summary.
3108                my @parents = @{$commit->{parents}};
3109                foreach my $parent (@parents) {
3110                    # git-merge-base can potentially (but rarely) throw
3111                    # several candidate merge bases. let's assume
3112                    # that the first one is the best one.
3113                    if ($parent eq $lastpicked) {
3114                        next;
3115                    }
3116                    my $base = eval {
3117                            safe_pipe_capture('git', 'merge-base',
3118                                                 $lastpicked, $parent);
3119                    };
3120                    # The two branches may not be related at all,
3121                    # in which case merge base simply fails to find
3122                    # any, but that's Ok.
3123                    next if ($@);
3124
3125                    chomp $base;
3126                    if ($base) {
3127                        my @merged;
3128                        # print "want to log between  $base $parent \n";
3129                        open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
3130                          or die "Cannot call git-log: $!";
3131                        my $mergedhash;
3132                        while (<GITLOG>) {
3133                            chomp;
3134                            if (!defined $mergedhash) {
3135                                if (m/^commit\s+(.+)$/) {
3136                                    $mergedhash = $1;
3137                                } else {
3138                                    next;
3139                                }
3140                            } else {
3141                                # grab the first line that looks non-rfc822
3142                                # aka has content after leading space
3143                                if (m/^\s+(\S.*)$/) {
3144                                    my $title = $1;
3145                                    $title = substr($title,0,100); # truncate
3146                                    unshift @merged, "$mergedhash $title";
3147                                    undef $mergedhash;
3148                                }
3149                            }
3150                        }
3151                        close GITLOG;
3152                        if (@merged) {
3153                            $commit->{mergemsg} = $commit->{message};
3154                            $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
3155                            foreach my $summary (@merged) {
3156                                $commit->{mergemsg} .= "\t$summary\n";
3157                            }
3158                            $commit->{mergemsg} .= "\n\n";
3159                            # print "Message for $commit->{hash} \n$commit->{mergemsg}";
3160                        }
3161                    }
3162                }
3163            }
3164        }
3165
3166        # convert the date to CVS-happy format
3167        $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
3168
3169        if ( defined ( $lastpicked ) )
3170        {
3171            my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
3172            local ($/) = "\0";
3173            while ( <FILELIST> )
3174            {
3175                chomp;
3176                unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
3177                {
3178                    die("Couldn't process git-diff-tree line : $_");
3179                }
3180                my ($mode, $hash, $change) = ($1, $2, $3);
3181                my $name = <FILELIST>;
3182                chomp($name);
3183
3184                # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3185
3186                my $git_perms = "";
3187                $git_perms .= "r" if ( $mode & 4 );
3188                $git_perms .= "w" if ( $mode & 2 );
3189                $git_perms .= "x" if ( $mode & 1 );
3190                $git_perms = "rw" if ( $git_perms eq "" );
3191
3192                if ( $change eq "D" )
3193                {
3194                    #$log->debug("DELETE   $name");
3195                    $head->{$name} = {
3196                        name => $name,
3197                        revision => $head->{$name}{revision} + 1,
3198                        filehash => "deleted",
3199                        commithash => $commit->{hash},
3200                        modified => $commit->{date},
3201                        author => $commit->{author},
3202                        mode => $git_perms,
3203                    };
3204                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3205                }
3206                elsif ( $change eq "M" || $change eq "T" )
3207                {
3208                    #$log->debug("MODIFIED $name");
3209                    $head->{$name} = {
3210                        name => $name,
3211                        revision => $head->{$name}{revision} + 1,
3212                        filehash => $hash,
3213                        commithash => $commit->{hash},
3214                        modified => $commit->{date},
3215                        author => $commit->{author},
3216                        mode => $git_perms,
3217                    };
3218                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3219                }
3220                elsif ( $change eq "A" )
3221                {
3222                    #$log->debug("ADDED    $name");
3223                    $head->{$name} = {
3224                        name => $name,
3225                        revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
3226                        filehash => $hash,
3227                        commithash => $commit->{hash},
3228                        modified => $commit->{date},
3229                        author => $commit->{author},
3230                        mode => $git_perms,
3231                    };
3232                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3233                }
3234                else
3235                {
3236                    $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3237                    die;
3238                }
3239            }
3240            close FILELIST;
3241        } else {
3242            # this is used to detect files removed from the repo
3243            my $seen_files = {};
3244
3245            my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
3246            local $/ = "\0";
3247            while ( <FILELIST> )
3248            {
3249                chomp;
3250                unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3251                {
3252                    die("Couldn't process git-ls-tree line : $_");
3253                }
3254
3255                my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
3256
3257                $seen_files->{$git_filename} = 1;
3258
3259                my ( $oldhash, $oldrevision, $oldmode ) = (
3260                    $head->{$git_filename}{filehash},
3261                    $head->{$git_filename}{revision},
3262                    $head->{$git_filename}{mode}
3263                );
3264
3265                if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
3266                {
3267                    $git_perms = "";
3268                    $git_perms .= "r" if ( $1 & 4 );
3269                    $git_perms .= "w" if ( $1 & 2 );
3270                    $git_perms .= "x" if ( $1 & 1 );
3271                } else {
3272                    $git_perms = "rw";
3273                }
3274
3275                # unless the file exists with the same hash, we need to update it ...
3276                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
3277                {
3278                    my $newrevision = ( $oldrevision or 0 ) + 1;
3279
3280                    $head->{$git_filename} = {
3281                        name => $git_filename,
3282                        revision => $newrevision,
3283                        filehash => $git_hash,
3284                        commithash => $commit->{hash},
3285                        modified => $commit->{date},
3286                        author => $commit->{author},
3287                        mode => $git_perms,
3288                    };
3289
3290
3291                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3292                }
3293            }
3294            close FILELIST;
3295
3296            # Detect deleted files
3297            foreach my $file ( keys %$head )
3298            {
3299                unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
3300                {
3301                    $head->{$file}{revision}++;
3302                    $head->{$file}{filehash} = "deleted";
3303                    $head->{$file}{commithash} = $commit->{hash};
3304                    $head->{$file}{modified} = $commit->{date};
3305                    $head->{$file}{author} = $commit->{author};
3306
3307                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
3308                }
3309            }
3310            # END : "Detect deleted files"
3311        }
3312
3313
3314        if (exists $commit->{mergemsg})
3315        {
3316            $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3317        }
3318
3319        $lastpicked = $commit->{hash};
3320
3321        $self->_set_prop("last_commit", $commit->{hash});
3322    }
3323
3324    $self->delete_head();
3325    foreach my $file ( keys %$head )
3326    {
3327        $self->insert_head(
3328            $file,
3329            $head->{$file}{revision},
3330            $head->{$file}{filehash},
3331            $head->{$file}{commithash},
3332            $head->{$file}{modified},
3333            $head->{$file}{author},
3334            $head->{$file}{mode},
3335        );
3336    }
3337    # invalidate the gethead cache
3338    $self->{gethead_cache} = undef;
3339
3340
3341    # Ending exclusive lock here
3342    $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
3343}
3344
3345sub insert_rev
3346{
3347    my $self = shift;
3348    my $name = shift;
3349    my $revision = shift;
3350    my $filehash = shift;
3351    my $commithash = shift;
3352    my $modified = shift;
3353    my $author = shift;
3354    my $mode = shift;
3355    my $tablename = $self->tablename("revision");
3356
3357    my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3358    $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3359}
3360
3361sub insert_mergelog
3362{
3363    my $self = shift;
3364    my $key = shift;
3365    my $value = shift;
3366    my $tablename = $self->tablename("commitmsgs");
3367
3368    my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3369    $insert_mergelog->execute($key, $value);
3370}
3371
3372sub delete_head
3373{
3374    my $self = shift;
3375    my $tablename = $self->tablename("head");
3376
3377    my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1);
3378    $delete_head->execute();
3379}
3380
3381sub insert_head
3382{
3383    my $self = shift;
3384    my $name = shift;
3385    my $revision = shift;
3386    my $filehash = shift;
3387    my $commithash = shift;
3388    my $modified = shift;
3389    my $author = shift;
3390    my $mode = shift;
3391    my $tablename = $self->tablename("head");
3392
3393    my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3394    $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3395}
3396
3397sub _headrev
3398{
3399    my $self = shift;
3400    my $filename = shift;
3401    my $tablename = $self->tablename("head");
3402
3403    my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1);
3404    $db_query->execute($filename);
3405    my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
3406
3407    return ( $hash, $revision, $mode );
3408}
3409
3410sub _get_prop
3411{
3412    my $self = shift;
3413    my $key = shift;
3414    my $tablename = $self->tablename("properties");
3415
3416    my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3417    $db_query->execute($key);
3418    my ( $value ) = $db_query->fetchrow_array;
3419
3420    return $value;
3421}
3422
3423sub _set_prop
3424{
3425    my $self = shift;
3426    my $key = shift;
3427    my $value = shift;
3428    my $tablename = $self->tablename("properties");
3429
3430    my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1);
3431    $db_query->execute($value, $key);
3432
3433    unless ( $db_query->rows )
3434    {
3435        $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3436        $db_query->execute($key, $value);
3437    }
3438
3439    return $value;
3440}
3441
3442=head2 gethead
3443
3444=cut
3445
3446sub gethead
3447{
3448    my $self = shift;
3449    my $tablename = $self->tablename("head");
3450
3451    return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
3452
3453    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1);
3454    $db_query->execute();
3455
3456    my $tree = [];
3457    while ( my $file = $db_query->fetchrow_hashref )
3458    {
3459        push @$tree, $file;
3460    }
3461
3462    $self->{gethead_cache} = $tree;
3463
3464    return $tree;
3465}
3466
3467=head2 getlog
3468
3469=cut
3470
3471sub getlog
3472{
3473    my $self = shift;
3474    my $filename = shift;
3475    my $tablename = $self->tablename("revision");
3476
3477    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3478    $db_query->execute($filename);
3479
3480    my $tree = [];
3481    while ( my $file = $db_query->fetchrow_hashref )
3482    {
3483        push @$tree, $file;
3484    }
3485
3486    return $tree;
3487}
3488
3489=head2 getmeta
3490
3491This function takes a filename (with path) argument and returns a hashref of
3492metadata for that file.
3493
3494=cut
3495
3496sub getmeta
3497{
3498    my $self = shift;
3499    my $filename = shift;
3500    my $revision = shift;
3501    my $tablename_rev = $self->tablename("revision");
3502    my $tablename_head = $self->tablename("head");
3503
3504    my $db_query;
3505    if ( defined($revision) and $revision =~ /^\d+$/ )
3506    {
3507        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
3508        $db_query->execute($filename, $revision);
3509    }
3510    elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
3511    {
3512        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
3513        $db_query->execute($filename, $revision);
3514    } else {
3515        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
3516        $db_query->execute($filename);
3517    }
3518
3519    return $db_query->fetchrow_hashref;
3520}
3521
3522=head2 commitmessage
3523
3524this function takes a commithash and returns the commit message for that commit
3525
3526=cut
3527sub commitmessage
3528{
3529    my $self = shift;
3530    my $commithash = shift;
3531    my $tablename = $self->tablename("commitmsgs");
3532
3533    die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
3534
3535    my $db_query;
3536    $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3537    $db_query->execute($commithash);
3538
3539    my ( $message ) = $db_query->fetchrow_array;
3540
3541    if ( defined ( $message ) )
3542    {
3543        $message .= " " if ( $message =~ /\n$/ );
3544        return $message;
3545    }
3546
3547    my @lines = safe_pipe_capture("git", "cat-file", "commit", $commithash);
3548    shift @lines while ( $lines[0] =~ /\S/ );
3549    $message = join("",@lines);
3550    $message .= " " if ( $message =~ /\n$/ );
3551    return $message;
3552}
3553
3554=head2 gethistory
3555
3556This function takes a filename (with path) argument and returns an arrayofarrays
3557containing revision,filehash,commithash ordered by revision descending
3558
3559=cut
3560sub gethistory
3561{
3562    my $self = shift;
3563    my $filename = shift;
3564    my $tablename = $self->tablename("revision");
3565
3566    my $db_query;
3567    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3568    $db_query->execute($filename);
3569
3570    return $db_query->fetchall_arrayref;
3571}
3572
3573=head2 gethistorydense
3574
3575This function takes a filename (with path) argument and returns an arrayofarrays
3576containing revision,filehash,commithash ordered by revision descending.
3577
3578This version of gethistory skips deleted entries -- so it is useful for annotate.
3579The 'dense' part is a reference to a '--dense' option available for git-rev-list
3580and other git tools that depend on it.
3581
3582=cut
3583sub gethistorydense
3584{
3585    my $self = shift;
3586    my $filename = shift;
3587    my $tablename = $self->tablename("revision");
3588
3589    my $db_query;
3590    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
3591    $db_query->execute($filename);
3592
3593    return $db_query->fetchall_arrayref;
3594}
3595
3596=head2 in_array()
3597
3598from Array::PAT - mimics the in_array() function
3599found in PHP. Yuck but works for small arrays.
3600
3601=cut
3602sub in_array
3603{
3604    my ($check, @array) = @_;
3605    my $retval = 0;
3606    foreach my $test (@array){
3607        if($check eq $test){
3608            $retval =  1;
3609        }
3610    }
3611    return $retval;
3612}
3613
3614=head2 safe_pipe_capture
3615
3616an alternative to `command` that allows input to be passed as an array
3617to work around shell problems with weird characters in arguments
3618
3619=cut
3620sub safe_pipe_capture {
3621
3622    my @output;
3623
3624    if (my $pid = open my $child, '-|') {
3625        @output = (<$child>);
3626        close $child or die join(' ',@_).": $! $?";
3627    } else {
3628        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3629    }
3630    return wantarray ? @output : join('',@output);
3631}
3632
3633=head2 mangle_dirname
3634
3635create a string from a directory name that is suitable to use as
3636part of a filename, mainly by converting all chars except \w.- to _
3637
3638=cut
3639sub mangle_dirname {
3640    my $dirname = shift;
3641    return unless defined $dirname;
3642
3643    $dirname =~ s/[^\w.-]/_/g;
3644
3645    return $dirname;
3646}
3647
3648=head2 mangle_tablename
3649
3650create a string from a that is suitable to use as part of an SQL table
3651name, mainly by converting all chars except \w to _
3652
3653=cut
3654sub mangle_tablename {
3655    my $tablename = shift;
3656    return unless defined $tablename;
3657
3658    $tablename =~ s/[^\w_]/_/g;
3659
3660    return $tablename;
3661}
3662
36631;