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