git-cvsserver.perlon commit Added logged warnings for CVS error returns (568907f)
   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;
  20
  21use Fcntl;
  22use File::Temp qw/tempdir tempfile/;
  23use File::Basename;
  24
  25my $log = GITCVS::log->new();
  26my $cfg;
  27
  28my $DATE_LIST = {
  29    Jan => "01",
  30    Feb => "02",
  31    Mar => "03",
  32    Apr => "04",
  33    May => "05",
  34    Jun => "06",
  35    Jul => "07",
  36    Aug => "08",
  37    Sep => "09",
  38    Oct => "10",
  39    Nov => "11",
  40    Dec => "12",
  41};
  42
  43# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
  44$| = 1;
  45
  46#### Definition and mappings of functions ####
  47
  48my $methods = {
  49    'Root'            => \&req_Root,
  50    'Valid-responses' => \&req_Validresponses,
  51    'valid-requests'  => \&req_validrequests,
  52    'Directory'       => \&req_Directory,
  53    'Entry'           => \&req_Entry,
  54    'Modified'        => \&req_Modified,
  55    'Unchanged'       => \&req_Unchanged,
  56    'Questionable'    => \&req_Questionable,
  57    'Argument'        => \&req_Argument,
  58    'Argumentx'       => \&req_Argument,
  59    'expand-modules'  => \&req_expandmodules,
  60    'add'             => \&req_add,
  61    'remove'          => \&req_remove,
  62    'co'              => \&req_co,
  63    'update'          => \&req_update,
  64    'ci'              => \&req_ci,
  65    'diff'            => \&req_diff,
  66    'log'             => \&req_log,
  67    'rlog'            => \&req_log,
  68    'tag'             => \&req_CATCHALL,
  69    'status'          => \&req_status,
  70    'admin'           => \&req_CATCHALL,
  71    'history'         => \&req_CATCHALL,
  72    'watchers'        => \&req_CATCHALL,
  73    'editors'         => \&req_CATCHALL,
  74    'annotate'        => \&req_annotate,
  75    'Global_option'   => \&req_Globaloption,
  76    #'annotate'        => \&req_CATCHALL,
  77};
  78
  79##############################################
  80
  81
  82# $state holds all the bits of information the clients sends us that could
  83# potentially be useful when it comes to actually _doing_ something.
  84my $state = {};
  85$log->info("--------------- STARTING -----------------");
  86
  87my $TEMP_DIR = tempdir( CLEANUP => 1 );
  88$log->debug("Temporary directory is '$TEMP_DIR'");
  89
  90# if we are called with a pserver argument,
  91# deal with the authentication cat before entering the
  92# main loop
  93if (@ARGV && $ARGV[0] eq 'pserver') {
  94    my $line = <STDIN>; chomp $line;
  95    unless( $line eq 'BEGIN AUTH REQUEST') {
  96       die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
  97    }
  98    $line = <STDIN>; chomp $line;
  99    req_Root('root', $line) # reuse Root
 100       or die "E Invalid root $line \n";
 101    $line = <STDIN>; chomp $line;
 102    unless ($line eq 'anonymous') {
 103       print "E Only anonymous user allowed via pserver\n";
 104       print "I HATE YOU\n";
 105    }
 106    $line = <STDIN>; chomp $line;    # validate the password?
 107    $line = <STDIN>; chomp $line;
 108    unless ($line eq 'END AUTH REQUEST') {
 109       die "E Do not understand $line -- expecting END AUTH REQUEST\n";
 110    }
 111    print "I LOVE YOU\n";
 112    # and now back to our regular programme...
 113}
 114
 115# Keep going until the client closes the connection
 116while (<STDIN>)
 117{
 118    chomp;
 119
 120    # Check to see if we've seen this method, and call appropriate function.
 121    if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
 122    {
 123        # use the $methods hash to call the appropriate sub for this command
 124        #$log->info("Method : $1");
 125        &{$methods->{$1}}($1,$2);
 126    } else {
 127        # log fatal because we don't understand this function. If this happens
 128        # we're fairly screwed because we don't know if the client is expecting
 129        # a response. If it is, the client will hang, we'll hang, and the whole
 130        # thing will be custard.
 131        $log->fatal("Don't understand command $_\n");
 132        die("Unknown command $_");
 133    }
 134}
 135
 136$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
 137$log->info("--------------- FINISH -----------------");
 138
 139# Magic catchall method.
 140#    This is the method that will handle all commands we haven't yet
 141#    implemented. It simply sends a warning to the log file indicating a
 142#    command that hasn't been implemented has been invoked.
 143sub req_CATCHALL
 144{
 145    my ( $cmd, $data ) = @_;
 146    $log->warn("Unhandled command : req_$cmd : $data");
 147}
 148
 149
 150# Root pathname \n
 151#     Response expected: no. Tell the server which CVSROOT to use. Note that
 152#     pathname is a local directory and not a fully qualified CVSROOT variable.
 153#     pathname must already exist; if creating a new root, use the init
 154#     request, not Root. pathname does not include the hostname of the server,
 155#     how to access the server, etc.; by the time the CVS protocol is in use,
 156#     connection, authentication, etc., are already taken care of. The Root
 157#     request must be sent only once, and it must be sent before any requests
 158#     other than Valid-responses, valid-requests, UseUnchanged, Set or init.
 159sub req_Root
 160{
 161    my ( $cmd, $data ) = @_;
 162    $log->debug("req_Root : $data");
 163
 164    $state->{CVSROOT} = $data;
 165
 166    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
 167    unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
 168       print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
 169        print "E \n";
 170        print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
 171       return 0;
 172    }
 173
 174    my @gitvars = `git-repo-config -l`;
 175    if ($?) {
 176       print "E problems executing git-repo-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
 177        print "E \n";
 178        print "error 1 - problem executing git-repo-config\n";
 179       return 0;
 180    }
 181    foreach my $line ( @gitvars )
 182    {
 183        next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ );
 184        $cfg->{$1}{$2} = $3;
 185    }
 186
 187    unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i )
 188    {
 189        print "E GITCVS emulation needs to be enabled on this repo\n";
 190        print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
 191        print "E \n";
 192        print "error 1 GITCVS emulation disabled\n";
 193        return 0;
 194    }
 195
 196    if ( defined ( $cfg->{gitcvs}{logfile} ) )
 197    {
 198        $log->setfile($cfg->{gitcvs}{logfile});
 199    } else {
 200        $log->nofile();
 201    }
 202
 203    return 1;
 204}
 205
 206# Global_option option \n
 207#     Response expected: no. Transmit one of the global options `-q', `-Q',
 208#     `-l', `-t', `-r', or `-n'. option must be one of those strings, no
 209#     variations (such as combining of options) are allowed. For graceful
 210#     handling of valid-requests, it is probably better to make new global
 211#     options separate requests, rather than trying to add them to this
 212#     request.
 213sub req_Globaloption
 214{
 215    my ( $cmd, $data ) = @_;
 216    $log->debug("req_Globaloption : $data");
 217
 218    # TODO : is this data useful ???
 219}
 220
 221# Valid-responses request-list \n
 222#     Response expected: no. Tell the server what responses the client will
 223#     accept. request-list is a space separated list of tokens.
 224sub req_Validresponses
 225{
 226    my ( $cmd, $data ) = @_;
 227    $log->debug("req_Validresponses : $data");
 228
 229    # TODO : re-enable this, currently it's not particularly useful
 230    #$state->{validresponses} = [ split /\s+/, $data ];
 231}
 232
 233# valid-requests \n
 234#     Response expected: yes. Ask the server to send back a Valid-requests
 235#     response.
 236sub req_validrequests
 237{
 238    my ( $cmd, $data ) = @_;
 239
 240    $log->debug("req_validrequests");
 241
 242    $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
 243    $log->debug("SEND : ok");
 244
 245    print "Valid-requests " . join(" ",keys %$methods) . "\n";
 246    print "ok\n";
 247}
 248
 249# Directory local-directory \n
 250#     Additional data: repository \n. Response expected: no. Tell the server
 251#     what directory to use. The repository should be a directory name from a
 252#     previous server response. Note that this both gives a default for Entry
 253#     and Modified and also for ci and the other commands; normal usage is to
 254#     send Directory for each directory in which there will be an Entry or
 255#     Modified, and then a final Directory for the original directory, then the
 256#     command. The local-directory is relative to the top level at which the
 257#     command is occurring (i.e. the last Directory which is sent before the
 258#     command); to indicate that top level, `.' should be sent for
 259#     local-directory.
 260sub req_Directory
 261{
 262    my ( $cmd, $data ) = @_;
 263
 264    my $repository = <STDIN>;
 265    chomp $repository;
 266
 267
 268    $state->{localdir} = $data;
 269    $state->{repository} = $repository;
 270    $state->{directory} = $repository;
 271    $state->{directory} =~ s/^$state->{CVSROOT}\///;
 272    $state->{module} = $1 if ($state->{directory} =~ s/^(.*?)(\/|$)//);
 273    $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
 274
 275    $log->debug("req_Directory : localdir=$data repository=$repository directory=$state->{directory} module=$state->{module}");
 276}
 277
 278# Entry entry-line \n
 279#     Response expected: no. Tell the server what version of a file is on the
 280#     local machine. The name in entry-line is a name relative to the directory
 281#     most recently specified with Directory. If the user is operating on only
 282#     some files in a directory, Entry requests for only those files need be
 283#     included. If an Entry request is sent without Modified, Is-modified, or
 284#     Unchanged, it means the file is lost (does not exist in the working
 285#     directory). If both Entry and one of Modified, Is-modified, or Unchanged
 286#     are sent for the same file, Entry must be sent first. For a given file,
 287#     one can send Modified, Is-modified, or Unchanged, but not more than one
 288#     of these three.
 289sub req_Entry
 290{
 291    my ( $cmd, $data ) = @_;
 292
 293    $log->debug("req_Entry : $data");
 294
 295    my @data = split(/\//, $data);
 296
 297    $state->{entries}{$state->{directory}.$data[1]} = {
 298        revision    => $data[2],
 299        conflict    => $data[3],
 300        options     => $data[4],
 301        tag_or_date => $data[5],
 302    };
 303}
 304
 305# add \n
 306#     Response expected: yes. Add a file or directory. This uses any previous
 307#     Argument, Directory, Entry, or Modified requests, if they have been sent.
 308#     The last Directory sent specifies the working directory at the time of
 309#     the operation. To add a directory, send the directory to be added using
 310#     Directory and Argument requests.
 311sub req_add
 312{
 313    my ( $cmd, $data ) = @_;
 314
 315    argsplit("add");
 316
 317    my $addcount = 0;
 318
 319    foreach my $filename ( @{$state->{args}} )
 320    {
 321        $filename = filecleanup($filename);
 322
 323        unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
 324        {
 325            print "E cvs add: nothing known about `$filename'\n";
 326            next;
 327        }
 328        # TODO : check we're not squashing an already existing file
 329        if ( defined ( $state->{entries}{$filename}{revision} ) )
 330        {
 331            print "E cvs add: `$filename' has already been entered\n";
 332            next;
 333        }
 334
 335
 336        my ( $filepart, $dirpart ) = filenamesplit($filename);
 337
 338        print "E cvs add: scheduling file `$filename' for addition\n";
 339
 340        print "Checked-in $dirpart\n";
 341        print "$filename\n";
 342        print "/$filepart/0///\n";
 343
 344        $addcount++;
 345    }
 346
 347    if ( $addcount == 1 )
 348    {
 349        print "E cvs add: use `cvs commit' to add this file permanently\n";
 350    }
 351    elsif ( $addcount > 1 )
 352    {
 353        print "E cvs add: use `cvs commit' to add these files permanently\n";
 354    }
 355
 356    print "ok\n";
 357}
 358
 359# remove \n
 360#     Response expected: yes. Remove a file. This uses any previous Argument,
 361#     Directory, Entry, or Modified requests, if they have been sent. The last
 362#     Directory sent specifies the working directory at the time of the
 363#     operation. Note that this request does not actually do anything to the
 364#     repository; the only effect of a successful remove request is to supply
 365#     the client with a new entries line containing `-' to indicate a removed
 366#     file. In fact, the client probably could perform this operation without
 367#     contacting the server, although using remove may cause the server to
 368#     perform a few more checks. The client sends a subsequent ci request to
 369#     actually record the removal in the repository.
 370sub req_remove
 371{
 372    my ( $cmd, $data ) = @_;
 373
 374    argsplit("remove");
 375
 376    # Grab a handle to the SQLite db and do any necessary updates
 377    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
 378    $updater->update();
 379
 380    #$log->debug("add state : " . Dumper($state));
 381
 382    my $rmcount = 0;
 383
 384    foreach my $filename ( @{$state->{args}} )
 385    {
 386        $filename = filecleanup($filename);
 387
 388        if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
 389        {
 390            print "E cvs remove: file `$filename' still in working directory\n";
 391            next;
 392        }
 393
 394        my $meta = $updater->getmeta($filename);
 395        my $wrev = revparse($filename);
 396
 397        unless ( defined ( $wrev ) )
 398        {
 399            print "E cvs remove: nothing known about `$filename'\n";
 400            next;
 401        }
 402
 403        if ( defined($wrev) and $wrev < 0 )
 404        {
 405            print "E cvs remove: file `$filename' already scheduled for removal\n";
 406            next;
 407        }
 408
 409        unless ( $wrev == $meta->{revision} )
 410        {
 411            # TODO : not sure if the format of this message is quite correct.
 412            print "E cvs remove: Up to date check failed for `$filename'\n";
 413            next;
 414        }
 415
 416
 417        my ( $filepart, $dirpart ) = filenamesplit($filename);
 418
 419        print "E cvs remove: scheduling `$filename' for removal\n";
 420
 421        print "Checked-in $dirpart\n";
 422        print "$filename\n";
 423        print "/$filepart/-1.$wrev///\n";
 424
 425        $rmcount++;
 426    }
 427
 428    if ( $rmcount == 1 )
 429    {
 430        print "E cvs remove: use `cvs commit' to remove this file permanently\n";
 431    }
 432    elsif ( $rmcount > 1 )
 433    {
 434        print "E cvs remove: use `cvs commit' to remove these files permanently\n";
 435    }
 436
 437    print "ok\n";
 438}
 439
 440# Modified filename \n
 441#     Response expected: no. Additional data: mode, \n, file transmission. Send
 442#     the server a copy of one locally modified file. filename is a file within
 443#     the most recent directory sent with Directory; it must not contain `/'.
 444#     If the user is operating on only some files in a directory, only those
 445#     files need to be included. This can also be sent without Entry, if there
 446#     is no entry for the file.
 447sub req_Modified
 448{
 449    my ( $cmd, $data ) = @_;
 450
 451    my $mode = <STDIN>;
 452    chomp $mode;
 453    my $size = <STDIN>;
 454    chomp $size;
 455
 456    # Grab config information
 457    my $blocksize = 8192;
 458    my $bytesleft = $size;
 459    my $tmp;
 460
 461    # Get a filehandle/name to write it to
 462    my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
 463
 464    # Loop over file data writing out to temporary file.
 465    while ( $bytesleft )
 466    {
 467        $blocksize = $bytesleft if ( $bytesleft < $blocksize );
 468        read STDIN, $tmp, $blocksize;
 469        print $fh $tmp;
 470        $bytesleft -= $blocksize;
 471    }
 472
 473    close $fh;
 474
 475    # Ensure we have something sensible for the file mode
 476    if ( $mode =~ /u=(\w+)/ )
 477    {
 478        $mode = $1;
 479    } else {
 480        $mode = "rw";
 481    }
 482
 483    # Save the file data in $state
 484    $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
 485    $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
 486    $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
 487    $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
 488
 489    #$log->debug("req_Modified : file=$data mode=$mode size=$size");
 490}
 491
 492# Unchanged filename \n
 493#     Response expected: no. Tell the server that filename has not been
 494#     modified in the checked out directory. The filename is a file within the
 495#     most recent directory sent with Directory; it must not contain `/'.
 496sub req_Unchanged
 497{
 498    my ( $cmd, $data ) = @_;
 499
 500    $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
 501
 502    #$log->debug("req_Unchanged : $data");
 503}
 504
 505# Questionable filename \n
 506#     Response expected: no. Additional data: no.
 507#     Tell the server to check whether filename should be ignored,
 508#     and if not, next time the server sends responses, send (in
 509#     a M response) `?' followed by the directory and filename.
 510#     filename must not contain `/'; it needs to be a file in the
 511#     directory named by the most recent Directory request.
 512sub req_Questionable
 513{
 514    my ( $cmd, $data ) = @_;
 515
 516    $state->{entries}{$state->{directory}.$data}{questionable} = 1;
 517
 518    #$log->debug("req_Questionable : $data");
 519}
 520
 521# Argument text \n
 522#     Response expected: no. Save argument for use in a subsequent command.
 523#     Arguments accumulate until an argument-using command is given, at which
 524#     point they are forgotten.
 525# Argumentx text \n
 526#     Response expected: no. Append \n followed by text to the current argument
 527#     being saved.
 528sub req_Argument
 529{
 530    my ( $cmd, $data ) = @_;
 531
 532    # TODO :  Not quite sure how Argument and Argumentx differ, but I assume
 533    # it's for multi-line arguments ... somehow ...
 534
 535    $log->debug("$cmd : $data");
 536
 537    push @{$state->{arguments}}, $data;
 538}
 539
 540# expand-modules \n
 541#     Response expected: yes. Expand the modules which are specified in the
 542#     arguments. Returns the data in Module-expansion responses. Note that the
 543#     server can assume that this is checkout or export, not rtag or rdiff; the
 544#     latter do not access the working directory and thus have no need to
 545#     expand modules on the client side. Expand may not be the best word for
 546#     what this request does. It does not necessarily tell you all the files
 547#     contained in a module, for example. Basically it is a way of telling you
 548#     which working directories the server needs to know about in order to
 549#     handle a checkout of the specified modules. For example, suppose that the
 550#     server has a module defined by
 551#   aliasmodule -a 1dir
 552#     That is, one can check out aliasmodule and it will take 1dir in the
 553#     repository and check it out to 1dir in the working directory. Now suppose
 554#     the client already has this module checked out and is planning on using
 555#     the co request to update it. Without using expand-modules, the client
 556#     would have two bad choices: it could either send information about all
 557#     working directories under the current directory, which could be
 558#     unnecessarily slow, or it could be ignorant of the fact that aliasmodule
 559#     stands for 1dir, and neglect to send information for 1dir, which would
 560#     lead to incorrect operation. With expand-modules, the client would first
 561#     ask for the module to be expanded:
 562sub req_expandmodules
 563{
 564    my ( $cmd, $data ) = @_;
 565
 566    argsplit();
 567
 568    $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
 569
 570    unless ( ref $state->{arguments} eq "ARRAY" )
 571    {
 572        print "ok\n";
 573        return;
 574    }
 575
 576    foreach my $module ( @{$state->{arguments}} )
 577    {
 578        $log->debug("SEND : Module-expansion $module");
 579        print "Module-expansion $module\n";
 580    }
 581
 582    print "ok\n";
 583    statecleanup();
 584}
 585
 586# co \n
 587#     Response expected: yes. Get files from the repository. This uses any
 588#     previous Argument, Directory, Entry, or Modified requests, if they have
 589#     been sent. Arguments to this command are module names; the client cannot
 590#     know what directories they correspond to except by (1) just sending the
 591#     co request, and then seeing what directory names the server sends back in
 592#     its responses, and (2) the expand-modules request.
 593sub req_co
 594{
 595    my ( $cmd, $data ) = @_;
 596
 597    argsplit("co");
 598
 599    my $module = $state->{args}[0];
 600    my $checkout_path = $module;
 601
 602    # use the user specified directory if we're given it
 603    $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
 604
 605    $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
 606
 607    $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
 608
 609    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
 610
 611    # Grab a handle to the SQLite db and do any necessary updates
 612    my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
 613    $updater->update();
 614
 615    $checkout_path =~ s|/$||; # get rid of trailing slashes
 616
 617    # Eclipse seems to need the Clear-sticky command
 618    # to prepare the 'Entries' file for the new directory.
 619    print "Clear-sticky $checkout_path/\n";
 620    print $state->{CVSROOT} . "/$module/\n";
 621    print "Clear-static-directory $checkout_path/\n";
 622    print $state->{CVSROOT} . "/$module/\n";
 623    print "Clear-sticky $checkout_path/\n"; # yes, twice
 624    print $state->{CVSROOT} . "/$module/\n";
 625    print "Template $checkout_path/\n";
 626    print $state->{CVSROOT} . "/$module/\n";
 627    print "0\n";
 628
 629    # instruct the client that we're checking out to $checkout_path
 630    print "E cvs checkout: Updating $checkout_path\n";
 631
 632    my %seendirs = ();
 633    my $lastdir ='';
 634
 635    # recursive
 636    sub prepdir {
 637       my ($dir, $repodir, $remotedir, $seendirs) = @_;
 638       my $parent = dirname($dir);
 639       $dir       =~ s|/+$||;
 640       $repodir   =~ s|/+$||;
 641       $remotedir =~ s|/+$||;
 642       $parent    =~ s|/+$||;
 643       $log->debug("announcedir $dir, $repodir, $remotedir" );
 644
 645       if ($parent eq '.' || $parent eq './') {
 646           $parent = '';
 647       }
 648       # recurse to announce unseen parents first
 649       if (length($parent) && !exists($seendirs->{$parent})) {
 650           prepdir($parent, $repodir, $remotedir, $seendirs);
 651       }
 652       # Announce that we are going to modify at the parent level
 653       if ($parent) {
 654           print "E cvs checkout: Updating $remotedir/$parent\n";
 655       } else {
 656           print "E cvs checkout: Updating $remotedir\n";
 657       }
 658       print "Clear-sticky $remotedir/$parent/\n";
 659       print "$repodir/$parent/\n";
 660
 661       print "Clear-static-directory $remotedir/$dir/\n";
 662       print "$repodir/$dir/\n";
 663       print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
 664       print "$repodir/$parent/\n";
 665       print "Template $remotedir/$dir/\n";
 666       print "$repodir/$dir/\n";
 667       print "0\n";
 668
 669       $seendirs->{$dir} = 1;
 670    }
 671
 672    foreach my $git ( @{$updater->gethead} )
 673    {
 674        # Don't want to check out deleted files
 675        next if ( $git->{filehash} eq "deleted" );
 676
 677        ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
 678
 679       if (length($git->{dir}) && $git->{dir} ne './'
 680           && $git->{dir} ne $lastdir ) {
 681           unless (exists($seendirs{$git->{dir}})) {
 682               prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
 683                       $checkout_path, \%seendirs);
 684               $lastdir = $git->{dir};
 685               $seendirs{$git->{dir}} = 1;
 686           }
 687           print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
 688       }
 689
 690        # modification time of this file
 691        print "Mod-time $git->{modified}\n";
 692
 693        # print some information to the client
 694        if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
 695        {
 696            print "M U $checkout_path/$git->{dir}$git->{name}\n";
 697        } else {
 698            print "M U $checkout_path/$git->{name}\n";
 699        }
 700
 701       # instruct client we're sending a file to put in this path
 702       print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
 703
 704       print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
 705
 706        # this is an "entries" line
 707        print "/$git->{name}/1.$git->{revision}///\n";
 708        # permissions
 709        print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
 710
 711        # transmit file
 712        transmitfile($git->{filehash});
 713    }
 714
 715    print "ok\n";
 716
 717    statecleanup();
 718}
 719
 720# update \n
 721#     Response expected: yes. Actually do a cvs update command. This uses any
 722#     previous Argument, Directory, Entry, or Modified requests, if they have
 723#     been sent. The last Directory sent specifies the working directory at the
 724#     time of the operation. The -I option is not used--files which the client
 725#     can decide whether to ignore are not mentioned and the client sends the
 726#     Questionable request for others.
 727sub req_update
 728{
 729    my ( $cmd, $data ) = @_;
 730
 731    $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
 732
 733    argsplit("update");
 734
 735    #
 736    # It may just be a client exploring the available heads/modules
 737    # in that case, list them as top level directories and leave it
 738    # at that. Eclipse uses this technique to offer you a list of
 739    # projects (heads in this case) to checkout.
 740    #
 741    if ($state->{module} eq '') {
 742        print "E cvs update: Updating .\n";
 743        opendir HEADS, $state->{CVSROOT} . '/refs/heads';
 744        while (my $head = readdir(HEADS)) {
 745            if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
 746                print "E cvs update: New directory `$head'\n";
 747            }
 748        }
 749        closedir HEADS;
 750        print "ok\n";
 751        return 1;
 752    }
 753
 754
 755    # Grab a handle to the SQLite db and do any necessary updates
 756    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
 757
 758    $updater->update();
 759
 760    # if no files were specified, we need to work out what files we should be providing status on ...
 761    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
 762
 763    #$log->debug("update state : " . Dumper($state));
 764
 765    # foreach file specified on the commandline ...
 766    foreach my $filename ( @{$state->{args}} )
 767    {
 768        $filename = filecleanup($filename);
 769
 770        # if we have a -C we should pretend we never saw modified stuff
 771        if ( exists ( $state->{opt}{C} ) )
 772        {
 773            delete $state->{entries}{$filename}{modified_hash};
 774            delete $state->{entries}{$filename}{modified_filename};
 775            $state->{entries}{$filename}{unchanged} = 1;
 776        }
 777
 778        my $meta;
 779        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
 780        {
 781            $meta = $updater->getmeta($filename, $1);
 782        } else {
 783            $meta = $updater->getmeta($filename);
 784        }
 785
 786        next unless ( $meta->{revision} );
 787
 788        my $oldmeta = $meta;
 789
 790        my $wrev = revparse($filename);
 791
 792        # If the working copy is an old revision, lets get that version too for comparison.
 793        if ( defined($wrev) and $wrev != $meta->{revision} )
 794        {
 795            $oldmeta = $updater->getmeta($filename, $wrev);
 796        }
 797
 798        #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
 799
 800        # Files are up to date if the working copy and repo copy have the same revision,
 801        # and the working copy is unmodified _and_ the user hasn't specified -C
 802        next if ( defined ( $wrev )
 803                  and defined($meta->{revision})
 804                  and $wrev == $meta->{revision}
 805                  and $state->{entries}{$filename}{unchanged}
 806                  and not exists ( $state->{opt}{C} ) );
 807
 808        # If the working copy and repo copy have the same revision,
 809        # but the working copy is modified, tell the client it's modified
 810        if ( defined ( $wrev )
 811             and defined($meta->{revision})
 812             and $wrev == $meta->{revision}
 813             and not exists ( $state->{opt}{C} ) )
 814        {
 815            $log->info("Tell the client the file is modified");
 816            print "MT text U\n";
 817            print "MT fname $filename\n";
 818            print "MT newline\n";
 819            next;
 820        }
 821
 822        if ( $meta->{filehash} eq "deleted" )
 823        {
 824            my ( $filepart, $dirpart ) = filenamesplit($filename);
 825
 826            $log->info("Removing '$filename' from working copy (no longer in the repo)");
 827
 828            print "E cvs update: `$filename' is no longer in the repository\n";
 829            print "Removed $dirpart\n";
 830            print "$filepart\n";
 831        }
 832        elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
 833                or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} )
 834        {
 835            $log->info("Updating '$filename'");
 836            # normal update, just send the new revision (either U=Update, or A=Add, or R=Remove)
 837            print "MT +updated\n";
 838            print "MT text U\n";
 839            print "MT fname $filename\n";
 840            print "MT newline\n";
 841            print "MT -updated\n";
 842
 843            my ( $filepart, $dirpart ) = filenamesplit($filename);
 844            $dirpart =~ s/^$state->{directory}//;
 845
 846            if ( defined ( $wrev ) )
 847            {
 848                # instruct client we're sending a file to put in this path as a replacement
 849                print "Update-existing $dirpart\n";
 850                $log->debug("Updating existing file 'Update-existing $dirpart'");
 851            } else {
 852                # instruct client we're sending a file to put in this path as a new file
 853                print "Created $dirpart\n";
 854                $log->debug("Creating new file 'Created $dirpart'");
 855            }
 856            print $state->{CVSROOT} . "/$state->{module}/$filename\n";
 857
 858            # this is an "entries" line
 859            $log->debug("/$filepart/1.$meta->{revision}///");
 860            print "/$filepart/1.$meta->{revision}///\n";
 861
 862            # permissions
 863            $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
 864            print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
 865
 866            # transmit file
 867            transmitfile($meta->{filehash});
 868        } else {
 869            $log->info("Updating '$filename'");
 870            my ( $filepart, $dirpart ) = filenamesplit($meta->{name});
 871
 872            my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
 873
 874            chdir $dir;
 875            my $file_local = $filepart . ".mine";
 876            system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
 877            my $file_old = $filepart . "." . $oldmeta->{revision};
 878            transmitfile($oldmeta->{filehash}, $file_old);
 879            my $file_new = $filepart . "." . $meta->{revision};
 880            transmitfile($meta->{filehash}, $file_new);
 881
 882            # we need to merge with the local changes ( M=successful merge, C=conflict merge )
 883            $log->info("Merging $file_local, $file_old, $file_new");
 884
 885            $log->debug("Temporary directory for merge is $dir");
 886
 887            my $return = system("merge", $file_local, $file_old, $file_new);
 888            $return >>= 8;
 889
 890            if ( $return == 0 )
 891            {
 892                $log->info("Merged successfully");
 893                print "M M $filename\n";
 894                $log->debug("Update-existing $dirpart");
 895                print "Update-existing $dirpart\n";
 896                $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
 897                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
 898                $log->debug("/$filepart/1.$meta->{revision}///");
 899                print "/$filepart/1.$meta->{revision}///\n";
 900            }
 901            elsif ( $return == 1 )
 902            {
 903                $log->info("Merged with conflicts");
 904                print "M C $filename\n";
 905                print "Update-existing $dirpart\n";
 906                print $state->{CVSROOT} . "/$state->{module}/$filename\n";
 907                print "/$filepart/1.$meta->{revision}/+//\n";
 908            }
 909            else
 910            {
 911                $log->warn("Merge failed");
 912                next;
 913            }
 914
 915            # permissions
 916            $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
 917            print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
 918
 919            # transmit file, format is single integer on a line by itself (file
 920            # size) followed by the file contents
 921            # TODO : we should copy files in blocks
 922            my $data = `cat $file_local`;
 923            $log->debug("File size : " . length($data));
 924            print length($data) . "\n";
 925            print $data;
 926
 927            chdir "/";
 928        }
 929
 930    }
 931
 932    print "ok\n";
 933}
 934
 935sub req_ci
 936{
 937    my ( $cmd, $data ) = @_;
 938
 939    argsplit("ci");
 940
 941    #$log->debug("State : " . Dumper($state));
 942
 943    $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
 944
 945    if ( @ARGV && $ARGV[0] eq 'pserver')
 946    {
 947        print "error 1 pserver access cannot commit\n";
 948        exit;
 949    }
 950
 951    if ( -e $state->{CVSROOT} . "/index" )
 952    {
 953        $log->warn("file 'index' already exists in the git repository");
 954        print "error 1 Index already exists in git repo\n";
 955        exit;
 956    }
 957
 958    my $lockfile = "$state->{CVSROOT}/refs/heads/$state->{module}.lock";
 959    unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) )
 960    {
 961        $log->warn("lockfile '$lockfile' already exists, please try again");
 962        print "error 1 Lock file '$lockfile' already exists, please try again\n";
 963        exit;
 964    }
 965
 966    # Grab a handle to the SQLite db and do any necessary updates
 967    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
 968    $updater->update();
 969
 970    my $tmpdir = tempdir ( DIR => $TEMP_DIR );
 971    my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
 972    $log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'");
 973
 974    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
 975    $ENV{GIT_INDEX_FILE} = $file_index;
 976
 977    chdir $tmpdir;
 978
 979    # populate the temporary index based
 980    system("git-read-tree", $state->{module});
 981    unless ($? == 0)
 982    {
 983        die "Error running git-read-tree $state->{module} $file_index $!";
 984    }
 985    $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
 986
 987
 988    my @committedfiles = ();
 989
 990    # foreach file specified on the commandline ...
 991    foreach my $filename ( @{$state->{args}} )
 992    {
 993        $filename = filecleanup($filename);
 994
 995        next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
 996
 997        my $meta = $updater->getmeta($filename);
 998
 999        my $wrev = revparse($filename);
1000
1001        my ( $filepart, $dirpart ) = filenamesplit($filename);
1002
1003        # do a checkout of the file if it part of this tree
1004        if ($wrev) {
1005            system('git-checkout-index', '-f', '-u', $filename);
1006            unless ($? == 0) {
1007                die "Error running git-checkout-index -f -u $filename : $!";
1008            }
1009        }
1010
1011        my $addflag = 0;
1012        my $rmflag = 0;
1013        $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1014        $addflag = 1 unless ( -e $filename );
1015
1016        # Do up to date checking
1017        unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1018        {
1019            # fail everything if an up to date check fails
1020            print "error 1 Up to date check failed for $filename\n";
1021            close LOCKFILE;
1022            unlink($lockfile);
1023            chdir "/";
1024            exit;
1025        }
1026
1027        push @committedfiles, $filename;
1028        $log->info("Committing $filename");
1029
1030        system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1031
1032        unless ( $rmflag )
1033        {
1034            $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1035            rename $state->{entries}{$filename}{modified_filename},$filename;
1036
1037            # Calculate modes to remove
1038            my $invmode = "";
1039            foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1040
1041            $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1042            system("chmod","u+" .  $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1043        }
1044
1045        if ( $rmflag )
1046        {
1047            $log->info("Removing file '$filename'");
1048            unlink($filename);
1049            system("git-update-index", "--remove", $filename);
1050        }
1051        elsif ( $addflag )
1052        {
1053            $log->info("Adding file '$filename'");
1054            system("git-update-index", "--add", $filename);
1055        } else {
1056            $log->info("Updating file '$filename'");
1057            system("git-update-index", $filename);
1058        }
1059    }
1060
1061    unless ( scalar(@committedfiles) > 0 )
1062    {
1063        print "E No files to commit\n";
1064        print "ok\n";
1065        close LOCKFILE;
1066        unlink($lockfile);
1067        chdir "/";
1068        return;
1069    }
1070
1071    my $treehash = `git-write-tree`;
1072    my $parenthash = `cat $ENV{GIT_DIR}refs/heads/$state->{module}`;
1073    chomp $treehash;
1074    chomp $parenthash;
1075
1076    $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1077
1078    # write our commit message out if we have one ...
1079    my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1080    print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1081    print $msg_fh "\n\nvia git-CVS emulator\n";
1082    close $msg_fh;
1083
1084    my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1085    $log->info("Commit hash : $commithash");
1086
1087    unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1088    {
1089        $log->warn("Commit failed (Invalid commit hash)");
1090        print "error 1 Commit failed (unknown reason)\n";
1091        close LOCKFILE;
1092        unlink($lockfile);
1093        chdir "/";
1094        exit;
1095    }
1096
1097    open FILE, ">", "$ENV{GIT_DIR}refs/heads/$state->{module}";
1098    print FILE $commithash;
1099    close FILE;
1100
1101    $updater->update();
1102
1103    # foreach file specified on the commandline ...
1104    foreach my $filename ( @committedfiles )
1105    {
1106        $filename = filecleanup($filename);
1107
1108        my $meta = $updater->getmeta($filename);
1109
1110        my ( $filepart, $dirpart ) = filenamesplit($filename);
1111
1112        $log->debug("Checked-in $dirpart : $filename");
1113
1114        if ( $meta->{filehash} eq "deleted" )
1115        {
1116            print "Remove-entry $dirpart\n";
1117            print "$filename\n";
1118        } else {
1119            print "Checked-in $dirpart\n";
1120            print "$filename\n";
1121            print "/$filepart/1.$meta->{revision}///\n";
1122        }
1123    }
1124
1125    close LOCKFILE;
1126    unlink($lockfile);
1127    chdir "/";
1128
1129    print "ok\n";
1130}
1131
1132sub req_status
1133{
1134    my ( $cmd, $data ) = @_;
1135
1136    argsplit("status");
1137
1138    $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1139    #$log->debug("status state : " . Dumper($state));
1140
1141    # Grab a handle to the SQLite db and do any necessary updates
1142    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1143    $updater->update();
1144
1145    # if no files were specified, we need to work out what files we should be providing status on ...
1146    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1147
1148    # foreach file specified on the commandline ...
1149    foreach my $filename ( @{$state->{args}} )
1150    {
1151        $filename = filecleanup($filename);
1152
1153        my $meta = $updater->getmeta($filename);
1154        my $oldmeta = $meta;
1155
1156        my $wrev = revparse($filename);
1157
1158        # If the working copy is an old revision, lets get that version too for comparison.
1159        if ( defined($wrev) and $wrev != $meta->{revision} )
1160        {
1161            $oldmeta = $updater->getmeta($filename, $wrev);
1162        }
1163
1164        # TODO : All possible statuses aren't yet implemented
1165        my $status;
1166        # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1167        $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1168                                    and
1169                                    ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1170                                      or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1171                                   );
1172
1173        # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1174        $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1175                                          and
1176                                          ( $state->{entries}{$filename}{unchanged}
1177                                            or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1178                                        );
1179
1180        # Need checkout if it exists in the repo but doesn't have a working copy
1181        $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1182
1183        # Locally modified if working copy and repo copy have the same revision but there are local changes
1184        $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1185
1186        # Needs Merge if working copy revision is less than repo copy and there are local changes
1187        $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1188
1189        $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1190        $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1191        $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1192        $status ||= "File had conflicts on merge" if ( 0 );
1193
1194        $status ||= "Unknown";
1195
1196        print "M ===================================================================\n";
1197        print "M File: $filename\tStatus: $status\n";
1198        if ( defined($state->{entries}{$filename}{revision}) )
1199        {
1200            print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1201        } else {
1202            print "M Working revision:\tNo entry for $filename\n";
1203        }
1204        if ( defined($meta->{revision}) )
1205        {
1206            print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{repository}/$filename,v\n";
1207            print "M Sticky Tag:\t\t(none)\n";
1208            print "M Sticky Date:\t\t(none)\n";
1209            print "M Sticky Options:\t\t(none)\n";
1210        } else {
1211            print "M Repository revision:\tNo revision control file\n";
1212        }
1213        print "M\n";
1214    }
1215
1216    print "ok\n";
1217}
1218
1219sub req_diff
1220{
1221    my ( $cmd, $data ) = @_;
1222
1223    argsplit("diff");
1224
1225    $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1226    #$log->debug("status state : " . Dumper($state));
1227
1228    my ($revision1, $revision2);
1229    if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1230    {
1231        $revision1 = $state->{opt}{r}[0];
1232        $revision2 = $state->{opt}{r}[1];
1233    } else {
1234        $revision1 = $state->{opt}{r};
1235    }
1236
1237    $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1238    $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1239
1240    $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1241
1242    # Grab a handle to the SQLite db and do any necessary updates
1243    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1244    $updater->update();
1245
1246    # if no files were specified, we need to work out what files we should be providing status on ...
1247    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1248
1249    # foreach file specified on the commandline ...
1250    foreach my $filename ( @{$state->{args}} )
1251    {
1252        $filename = filecleanup($filename);
1253
1254        my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1255
1256        my $wrev = revparse($filename);
1257
1258        # We need _something_ to diff against
1259        next unless ( defined ( $wrev ) );
1260
1261        # if we have a -r switch, use it
1262        if ( defined ( $revision1 ) )
1263        {
1264            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1265            $meta1 = $updater->getmeta($filename, $revision1);
1266            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1267            {
1268                print "E File $filename at revision 1.$revision1 doesn't exist\n";
1269                next;
1270            }
1271            transmitfile($meta1->{filehash}, $file1);
1272        }
1273        # otherwise we just use the working copy revision
1274        else
1275        {
1276            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1277            $meta1 = $updater->getmeta($filename, $wrev);
1278            transmitfile($meta1->{filehash}, $file1);
1279        }
1280
1281        # if we have a second -r switch, use it too
1282        if ( defined ( $revision2 ) )
1283        {
1284            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1285            $meta2 = $updater->getmeta($filename, $revision2);
1286
1287            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1288            {
1289                print "E File $filename at revision 1.$revision2 doesn't exist\n";
1290                next;
1291            }
1292
1293            transmitfile($meta2->{filehash}, $file2);
1294        }
1295        # otherwise we just use the working copy
1296        else
1297        {
1298            $file2 = $state->{entries}{$filename}{modified_filename};
1299        }
1300
1301        # if we have been given -r, and we don't have a $file2 yet, lets get one
1302        if ( defined ( $revision1 ) and not defined ( $file2 ) )
1303        {
1304            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1305            $meta2 = $updater->getmeta($filename, $wrev);
1306            transmitfile($meta2->{filehash}, $file2);
1307        }
1308
1309        # We need to have retrieved something useful
1310        next unless ( defined ( $meta1 ) );
1311
1312        # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1313        next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1314                  and
1315                   ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1316                     or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1317                  );
1318
1319        # Apparently we only show diffs for locally modified files
1320        next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1321
1322        print "M Index: $filename\n";
1323        print "M ===================================================================\n";
1324        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1325        print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1326        print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1327        print "M diff ";
1328        foreach my $opt ( keys %{$state->{opt}} )
1329        {
1330            if ( ref $state->{opt}{$opt} eq "ARRAY" )
1331            {
1332                foreach my $value ( @{$state->{opt}{$opt}} )
1333                {
1334                    print "-$opt $value ";
1335                }
1336            } else {
1337                print "-$opt ";
1338                print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1339            }
1340        }
1341        print "$filename\n";
1342
1343        $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1344
1345        ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1346
1347        if ( exists $state->{opt}{u} )
1348        {
1349            system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1350        } else {
1351            system("diff $file1 $file2 > $filediff");
1352        }
1353
1354        while ( <$fh> )
1355        {
1356            print "M $_";
1357        }
1358        close $fh;
1359    }
1360
1361    print "ok\n";
1362}
1363
1364sub req_log
1365{
1366    my ( $cmd, $data ) = @_;
1367
1368    argsplit("log");
1369
1370    $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1371    #$log->debug("log state : " . Dumper($state));
1372
1373    my ( $minrev, $maxrev );
1374    if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1375    {
1376        my $control = $2;
1377        $minrev = $1;
1378        $maxrev = $3;
1379        $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1380        $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1381        $minrev++ if ( defined($minrev) and $control eq "::" );
1382    }
1383
1384    # Grab a handle to the SQLite db and do any necessary updates
1385    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1386    $updater->update();
1387
1388    # if no files were specified, we need to work out what files we should be providing status on ...
1389    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1390
1391    # foreach file specified on the commandline ...
1392    foreach my $filename ( @{$state->{args}} )
1393    {
1394        $filename = filecleanup($filename);
1395
1396        my $headmeta = $updater->getmeta($filename);
1397
1398        my $revisions = $updater->getlog($filename);
1399        my $totalrevisions = scalar(@$revisions);
1400
1401        if ( defined ( $minrev ) )
1402        {
1403            $log->debug("Removing revisions less than $minrev");
1404            while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1405            {
1406                pop @$revisions;
1407            }
1408        }
1409        if ( defined ( $maxrev ) )
1410        {
1411            $log->debug("Removing revisions greater than $maxrev");
1412            while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1413            {
1414                shift @$revisions;
1415            }
1416        }
1417
1418        next unless ( scalar(@$revisions) );
1419
1420        print "M \n";
1421        print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1422        print "M Working file: $filename\n";
1423        print "M head: 1.$headmeta->{revision}\n";
1424        print "M branch:\n";
1425        print "M locks: strict\n";
1426        print "M access list:\n";
1427        print "M symbolic names:\n";
1428        print "M keyword substitution: kv\n";
1429        print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1430        print "M description:\n";
1431
1432        foreach my $revision ( @$revisions )
1433        {
1434            print "M ----------------------------\n";
1435            print "M revision 1.$revision->{revision}\n";
1436            # reformat the date for log output
1437            $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}) );
1438            $revision->{author} =~ s/\s+.*//;
1439            $revision->{author} =~ s/^(.{8}).*/$1/;
1440            print "M date: $revision->{modified};  author: $revision->{author};  state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . ";  lines: +2 -3\n";
1441            my $commitmessage = $updater->commitmessage($revision->{commithash});
1442            $commitmessage =~ s/^/M /mg;
1443            print $commitmessage . "\n";
1444        }
1445        print "M =============================================================================\n";
1446    }
1447
1448    print "ok\n";
1449}
1450
1451sub req_annotate
1452{
1453    my ( $cmd, $data ) = @_;
1454
1455    argsplit("annotate");
1456
1457    $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1458    #$log->debug("status state : " . Dumper($state));
1459
1460    # Grab a handle to the SQLite db and do any necessary updates
1461    my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1462    $updater->update();
1463
1464    # if no files were specified, we need to work out what files we should be providing annotate on ...
1465    argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1466
1467    # we'll need a temporary checkout dir
1468    my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1469    my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1470    $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1471
1472    $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1473    $ENV{GIT_INDEX_FILE} = $file_index;
1474
1475    chdir $tmpdir;
1476
1477    # foreach file specified on the commandline ...
1478    foreach my $filename ( @{$state->{args}} )
1479    {
1480        $filename = filecleanup($filename);
1481
1482        my $meta = $updater->getmeta($filename);
1483
1484        next unless ( $meta->{revision} );
1485
1486        # get all the commits that this file was in
1487        # in dense format -- aka skip dead revisions
1488        my $revisions   = $updater->gethistorydense($filename);
1489        my $lastseenin  = $revisions->[0][2];
1490
1491        # populate the temporary index based on the latest commit were we saw
1492        # the file -- but do it cheaply without checking out any files
1493        # TODO: if we got a revision from the client, use that instead
1494        # to look up the commithash in sqlite (still good to default to
1495        # the current head as we do now)
1496        system("git-read-tree", $lastseenin);
1497        unless ($? == 0)
1498        {
1499            die "Error running git-read-tree $lastseenin $file_index $!";
1500        }
1501        $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1502
1503        # do a checkout of the file
1504        system('git-checkout-index', '-f', '-u', $filename);
1505        unless ($? == 0) {
1506            die "Error running git-checkout-index -f -u $filename : $!";
1507        }
1508
1509        $log->info("Annotate $filename");
1510
1511        # Prepare a file with the commits from the linearized
1512        # history that annotate should know about. This prevents
1513        # git-jsannotate telling us about commits we are hiding
1514        # from the client.
1515
1516        open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
1517        for (my $i=0; $i < @$revisions; $i++)
1518        {
1519            print ANNOTATEHINTS $revisions->[$i][2];
1520            if ($i+1 < @$revisions) { # have we got a parent?
1521                print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1522            }
1523            print ANNOTATEHINTS "\n";
1524        }
1525
1526        print ANNOTATEHINTS "\n";
1527        close ANNOTATEHINTS;
1528
1529        my $annotatecmd = 'git-annotate';
1530        open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
1531            or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
1532        my $metadata = {};
1533        print "E Annotations for $filename\n";
1534        print "E ***************\n";
1535        while ( <ANNOTATE> )
1536        {
1537            if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1538            {
1539                my $commithash = $1;
1540                my $data = $2;
1541                unless ( defined ( $metadata->{$commithash} ) )
1542                {
1543                    $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1544                    $metadata->{$commithash}{author} =~ s/\s+.*//;
1545                    $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
1546                    $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1547                }
1548                printf("M 1.%-5d      (%-8s %10s): %s\n",
1549                    $metadata->{$commithash}{revision},
1550                    $metadata->{$commithash}{author},
1551                    $metadata->{$commithash}{modified},
1552                    $data
1553                );
1554            } else {
1555                $log->warn("Error in annotate output! LINE: $_");
1556                print "E Annotate error \n";
1557                next;
1558            }
1559        }
1560        close ANNOTATE;
1561    }
1562
1563    # done; get out of the tempdir
1564    chdir "/";
1565
1566    print "ok\n";
1567
1568}
1569
1570# This method takes the state->{arguments} array and produces two new arrays.
1571# The first is $state->{args} which is everything before the '--' argument, and
1572# the second is $state->{files} which is everything after it.
1573sub argsplit
1574{
1575    return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1576
1577    my $type = shift;
1578
1579    $state->{args} = [];
1580    $state->{files} = [];
1581    $state->{opt} = {};
1582
1583    if ( defined($type) )
1584    {
1585        my $opt = {};
1586        $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" );
1587        $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1588        $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" );
1589        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1590        $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1591        $opt = { k => 1, m => 1 } if ( $type eq "add" );
1592        $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1593        $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" );
1594
1595
1596        while ( scalar ( @{$state->{arguments}} ) > 0 )
1597        {
1598            my $arg = shift @{$state->{arguments}};
1599
1600            next if ( $arg eq "--" );
1601            next unless ( $arg =~ /\S/ );
1602
1603            # if the argument looks like a switch
1604            if ( $arg =~ /^-(\w)(.*)/ )
1605            {
1606                # if it's a switch that takes an argument
1607                if ( $opt->{$1} )
1608                {
1609                    # If this switch has already been provided
1610                    if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1611                    {
1612                        $state->{opt}{$1} = [ $state->{opt}{$1} ];
1613                        if ( length($2) > 0 )
1614                        {
1615                            push @{$state->{opt}{$1}},$2;
1616                        } else {
1617                            push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1618                        }
1619                    } else {
1620                        # if there's extra data in the arg, use that as the argument for the switch
1621                        if ( length($2) > 0 )
1622                        {
1623                            $state->{opt}{$1} = $2;
1624                        } else {
1625                            $state->{opt}{$1} = shift @{$state->{arguments}};
1626                        }
1627                    }
1628                } else {
1629                    $state->{opt}{$1} = undef;
1630                }
1631            }
1632            else
1633            {
1634                push @{$state->{args}}, $arg;
1635            }
1636        }
1637    }
1638    else
1639    {
1640        my $mode = 0;
1641
1642        foreach my $value ( @{$state->{arguments}} )
1643        {
1644            if ( $value eq "--" )
1645            {
1646                $mode++;
1647                next;
1648            }
1649            push @{$state->{args}}, $value if ( $mode == 0 );
1650            push @{$state->{files}}, $value if ( $mode == 1 );
1651        }
1652    }
1653}
1654
1655# This method uses $state->{directory} to populate $state->{args} with a list of filenames
1656sub argsfromdir
1657{
1658    my $updater = shift;
1659
1660    $state->{args} = [];
1661
1662    foreach my $file ( @{$updater->gethead} )
1663    {
1664        next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1665        next unless ( $file->{name} =~ s/^$state->{directory}// );
1666        push @{$state->{args}}, $file->{name};
1667    }
1668}
1669
1670# This method cleans up the $state variable after a command that uses arguments has run
1671sub statecleanup
1672{
1673    $state->{files} = [];
1674    $state->{args} = [];
1675    $state->{arguments} = [];
1676    $state->{entries} = {};
1677}
1678
1679sub revparse
1680{
1681    my $filename = shift;
1682
1683    return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
1684
1685    return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
1686    return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
1687
1688    return undef;
1689}
1690
1691# This method takes a file hash and does a CVS "file transfer" which transmits the
1692# size of the file, and then the file contents.
1693# If a second argument $targetfile is given, the file is instead written out to
1694# a file by the name of $targetfile
1695sub transmitfile
1696{
1697    my $filehash = shift;
1698    my $targetfile = shift;
1699
1700    if ( defined ( $filehash ) and $filehash eq "deleted" )
1701    {
1702        $log->warn("filehash is 'deleted'");
1703        return;
1704    }
1705
1706    die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
1707
1708    my $type = `git-cat-file -t $filehash`;
1709    chomp $type;
1710
1711    die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
1712
1713    my $size = `git-cat-file -s $filehash`;
1714    chomp $size;
1715
1716    $log->debug("transmitfile($filehash) size=$size, type=$type");
1717
1718    if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
1719    {
1720        if ( defined ( $targetfile ) )
1721        {
1722            open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
1723            print NEWFILE $_ while ( <$fh> );
1724            close NEWFILE;
1725        } else {
1726            print "$size\n";
1727            print while ( <$fh> );
1728        }
1729        close $fh or die ("Couldn't close filehandle for transmitfile()");
1730    } else {
1731        die("Couldn't execute git-cat-file");
1732    }
1733}
1734
1735# This method takes a file name, and returns ( $dirpart, $filepart ) which
1736# refers to the directory portion and the file portion of the filename
1737# respectively
1738sub filenamesplit
1739{
1740    my $filename = shift;
1741
1742    my ( $filepart, $dirpart ) = ( $filename, "." );
1743    ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
1744    $dirpart .= "/";
1745
1746    return ( $filepart, $dirpart );
1747}
1748
1749sub filecleanup
1750{
1751    my $filename = shift;
1752
1753    return undef unless(defined($filename));
1754    if ( $filename =~ /^\// )
1755    {
1756        print "E absolute filenames '$filename' not supported by server\n";
1757        return undef;
1758    }
1759
1760    $filename =~ s/^\.\///g;
1761    $filename = $state->{directory} . $filename;
1762
1763    return $filename;
1764}
1765
1766package GITCVS::log;
1767
1768####
1769#### Copyright The Open University UK - 2006.
1770####
1771#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
1772####          Martin Langhoff <martin@catalyst.net.nz>
1773####
1774####
1775
1776use strict;
1777use warnings;
1778
1779=head1 NAME
1780
1781GITCVS::log
1782
1783=head1 DESCRIPTION
1784
1785This module provides very crude logging with a similar interface to
1786Log::Log4perl
1787
1788=head1 METHODS
1789
1790=cut
1791
1792=head2 new
1793
1794Creates a new log object, optionally you can specify a filename here to
1795indicate the file to log to. If no log file is specified, you can specify one
1796later with method setfile, or indicate you no longer want logging with method
1797nofile.
1798
1799Until one of these methods is called, all log calls will buffer messages ready
1800to write out.
1801
1802=cut
1803sub new
1804{
1805    my $class = shift;
1806    my $filename = shift;
1807
1808    my $self = {};
1809
1810    bless $self, $class;
1811
1812    if ( defined ( $filename ) )
1813    {
1814        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
1815    }
1816
1817    return $self;
1818}
1819
1820=head2 setfile
1821
1822This methods takes a filename, and attempts to open that file as the log file.
1823If successful, all buffered data is written out to the file, and any further
1824logging is written directly to the file.
1825
1826=cut
1827sub setfile
1828{
1829    my $self = shift;
1830    my $filename = shift;
1831
1832    if ( defined ( $filename ) )
1833    {
1834        open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
1835    }
1836
1837    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
1838
1839    while ( my $line = shift @{$self->{buffer}} )
1840    {
1841        print {$self->{fh}} $line;
1842    }
1843}
1844
1845=head2 nofile
1846
1847This method indicates no logging is going to be used. It flushes any entries in
1848the internal buffer, and sets a flag to ensure no further data is put there.
1849
1850=cut
1851sub nofile
1852{
1853    my $self = shift;
1854
1855    $self->{nolog} = 1;
1856
1857    return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
1858
1859    $self->{buffer} = [];
1860}
1861
1862=head2 _logopen
1863
1864Internal method. Returns true if the log file is open, false otherwise.
1865
1866=cut
1867sub _logopen
1868{
1869    my $self = shift;
1870
1871    return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
1872    return 0;
1873}
1874
1875=head2 debug info warn fatal
1876
1877These four methods are wrappers to _log. They provide the actual interface for
1878logging data.
1879
1880=cut
1881sub debug { my $self = shift; $self->_log("debug", @_); }
1882sub info  { my $self = shift; $self->_log("info" , @_); }
1883sub warn  { my $self = shift; $self->_log("warn" , @_); }
1884sub fatal { my $self = shift; $self->_log("fatal", @_); }
1885
1886=head2 _log
1887
1888This is an internal method called by the logging functions. It generates a
1889timestamp and pushes the logged line either to file, or internal buffer.
1890
1891=cut
1892sub _log
1893{
1894    my $self = shift;
1895    my $level = shift;
1896
1897    return if ( $self->{nolog} );
1898
1899    my @time = localtime;
1900    my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
1901        $time[5] + 1900,
1902        $time[4] + 1,
1903        $time[3],
1904        $time[2],
1905        $time[1],
1906        $time[0],
1907        uc $level,
1908    );
1909
1910    if ( $self->_logopen )
1911    {
1912        print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
1913    } else {
1914        push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
1915    }
1916}
1917
1918=head2 DESTROY
1919
1920This method simply closes the file handle if one is open
1921
1922=cut
1923sub DESTROY
1924{
1925    my $self = shift;
1926
1927    if ( $self->_logopen )
1928    {
1929        close $self->{fh};
1930    }
1931}
1932
1933package GITCVS::updater;
1934
1935####
1936#### Copyright The Open University UK - 2006.
1937####
1938#### Authors: Martyn Smith    <martyn@catalyst.net.nz>
1939####          Martin Langhoff <martin@catalyst.net.nz>
1940####
1941####
1942
1943use strict;
1944use warnings;
1945use DBI;
1946
1947=head1 METHODS
1948
1949=cut
1950
1951=head2 new
1952
1953=cut
1954sub new
1955{
1956    my $class = shift;
1957    my $config = shift;
1958    my $module = shift;
1959    my $log = shift;
1960
1961    die "Need to specify a git repository" unless ( defined($config) and -d $config );
1962    die "Need to specify a module" unless ( defined($module) );
1963
1964    $class = ref($class) || $class;
1965
1966    my $self = {};
1967
1968    bless $self, $class;
1969
1970    $self->{dbdir} = $config . "/";
1971    die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} );
1972
1973    $self->{module} = $module;
1974    $self->{file} = $self->{dbdir} . "/gitcvs.$module.sqlite";
1975
1976    $self->{git_path} = $config . "/";
1977
1978    $self->{log} = $log;
1979
1980    die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
1981
1982    $self->{dbh} = DBI->connect("dbi:SQLite:dbname=" . $self->{file},"","");
1983
1984    $self->{tables} = {};
1985    foreach my $table ( $self->{dbh}->tables )
1986    {
1987        $table =~ s/^"//;
1988        $table =~ s/"$//;
1989        $self->{tables}{$table} = 1;
1990    }
1991
1992    # Construct the revision table if required
1993    unless ( $self->{tables}{revision} )
1994    {
1995        $self->{dbh}->do("
1996            CREATE TABLE revision (
1997                name       TEXT NOT NULL,
1998                revision   INTEGER NOT NULL,
1999                filehash   TEXT NOT NULL,
2000                commithash TEXT NOT NULL,
2001                author     TEXT NOT NULL,
2002                modified   TEXT NOT NULL,
2003                mode       TEXT NOT NULL
2004            )
2005        ");
2006    }
2007
2008    # Construct the revision table if required
2009    unless ( $self->{tables}{head} )
2010    {
2011        $self->{dbh}->do("
2012            CREATE TABLE head (
2013                name       TEXT NOT NULL,
2014                revision   INTEGER NOT NULL,
2015                filehash   TEXT NOT NULL,
2016                commithash TEXT NOT NULL,
2017                author     TEXT NOT NULL,
2018                modified   TEXT NOT NULL,
2019                mode       TEXT NOT NULL
2020            )
2021        ");
2022    }
2023
2024    # Construct the properties table if required
2025    unless ( $self->{tables}{properties} )
2026    {
2027        $self->{dbh}->do("
2028            CREATE TABLE properties (
2029                key        TEXT NOT NULL PRIMARY KEY,
2030                value      TEXT
2031            )
2032        ");
2033    }
2034
2035    # Construct the commitmsgs table if required
2036    unless ( $self->{tables}{commitmsgs} )
2037    {
2038        $self->{dbh}->do("
2039            CREATE TABLE commitmsgs (
2040                key        TEXT NOT NULL PRIMARY KEY,
2041                value      TEXT
2042            )
2043        ");
2044    }
2045
2046    return $self;
2047}
2048
2049=head2 update
2050
2051=cut
2052sub update
2053{
2054    my $self = shift;
2055
2056    # first lets get the commit list
2057    $ENV{GIT_DIR} = $self->{git_path};
2058
2059    # prepare database queries
2060    my $db_insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2061    my $db_insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2062    my $db_delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
2063    my $db_insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2064
2065    my $commitinfo = `git-cat-file commit $self->{module} 2>&1`;
2066    unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2067    {
2068        die("Invalid module '$self->{module}'");
2069    }
2070
2071
2072    my $git_log;
2073    my $lastcommit = $self->_get_prop("last_commit");
2074
2075    # Start exclusive lock here...
2076    $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2077
2078    # TODO: log processing is memory bound
2079    # if we can parse into a 2nd file that is in reverse order
2080    # we can probably do something really efficient
2081    my @git_log_params = ('--pretty', '--parents', '--topo-order');
2082
2083    if (defined $lastcommit) {
2084        push @git_log_params, "$lastcommit..$self->{module}";
2085    } else {
2086        push @git_log_params, $self->{module};
2087    }
2088    # git-rev-list is the backend / plumbing version of git-log
2089    open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
2090
2091    my @commits;
2092
2093    my %commit = ();
2094
2095    while ( <GITLOG> )
2096    {
2097        chomp;
2098        if (m/^commit\s+(.*)$/) {
2099            # on ^commit lines put the just seen commit in the stack
2100            # and prime things for the next one
2101            if (keys %commit) {
2102                my %copy = %commit;
2103                unshift @commits, \%copy;
2104                %commit = ();
2105            }
2106            my @parents = split(m/\s+/, $1);
2107            $commit{hash} = shift @parents;
2108            $commit{parents} = \@parents;
2109        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2110            # on rfc822-like lines seen before we see any message,
2111            # lowercase the entry and put it in the hash as key-value
2112            $commit{lc($1)} = $2;
2113        } else {
2114            # message lines - skip initial empty line
2115            # and trim whitespace
2116            if (!exists($commit{message}) && m/^\s*$/) {
2117                # define it to mark the end of headers
2118                $commit{message} = '';
2119                next;
2120            }
2121            s/^\s+//; s/\s+$//; # trim ws
2122            $commit{message} .= $_ . "\n";
2123        }
2124    }
2125    close GITLOG;
2126
2127    unshift @commits, \%commit if ( keys %commit );
2128
2129    # Now all the commits are in the @commits bucket
2130    # ordered by time DESC. for each commit that needs processing,
2131    # determine whether it's following the last head we've seen or if
2132    # it's on its own branch, grab a file list, and add whatever's changed
2133    # NOTE: $lastcommit refers to the last commit from previous run
2134    #       $lastpicked is the last commit we picked in this run
2135    my $lastpicked;
2136    my $head = {};
2137    if (defined $lastcommit) {
2138        $lastpicked = $lastcommit;
2139    }
2140
2141    my $committotal = scalar(@commits);
2142    my $commitcount = 0;
2143
2144    # Load the head table into $head (for cached lookups during the update process)
2145    foreach my $file ( @{$self->gethead()} )
2146    {
2147        $head->{$file->{name}} = $file;
2148    }
2149
2150    foreach my $commit ( @commits )
2151    {
2152        $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2153        if (defined $lastpicked)
2154        {
2155            if (!in_array($lastpicked, @{$commit->{parents}}))
2156            {
2157                # skip, we'll see this delta
2158                # as part of a merge later
2159                # warn "skipping off-track  $commit->{hash}\n";
2160                next;
2161            } elsif (@{$commit->{parents}} > 1) {
2162                # it is a merge commit, for each parent that is
2163                # not $lastpicked, see if we can get a log
2164                # from the merge-base to that parent to put it
2165                # in the message as a merge summary.
2166                my @parents = @{$commit->{parents}};
2167                foreach my $parent (@parents) {
2168                    # git-merge-base can potentially (but rarely) throw
2169                    # several candidate merge bases. let's assume
2170                    # that the first one is the best one.
2171                    if ($parent eq $lastpicked) {
2172                        next;
2173                    }
2174                    open my $p, 'git-merge-base '. $lastpicked . ' '
2175                    . $parent . '|';
2176                    my @output = (<$p>);
2177                    close $p;
2178                    my $base = join('', @output);
2179                    chomp $base;
2180                    if ($base) {
2181                        my @merged;
2182                        # print "want to log between  $base $parent \n";
2183                        open(GITLOG, '-|', 'git-log', "$base..$parent")
2184                        or die "Cannot call git-log: $!";
2185                        my $mergedhash;
2186                        while (<GITLOG>) {
2187                            chomp;
2188                            if (!defined $mergedhash) {
2189                                if (m/^commit\s+(.+)$/) {
2190                                    $mergedhash = $1;
2191                                } else {
2192                                    next;
2193                                }
2194                            } else {
2195                                # grab the first line that looks non-rfc822
2196                                # aka has content after leading space
2197                                if (m/^\s+(\S.*)$/) {
2198                                    my $title = $1;
2199                                    $title = substr($title,0,100); # truncate
2200                                    unshift @merged, "$mergedhash $title";
2201                                    undef $mergedhash;
2202                                }
2203                            }
2204                        }
2205                        close GITLOG;
2206                        if (@merged) {
2207                            $commit->{mergemsg} = $commit->{message};
2208                            $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
2209                            foreach my $summary (@merged) {
2210                                $commit->{mergemsg} .= "\t$summary\n";
2211                            }
2212                            $commit->{mergemsg} .= "\n\n";
2213                            # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2214                        }
2215                    }
2216                }
2217            }
2218        }
2219
2220        # convert the date to CVS-happy format
2221        $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2222
2223        if ( defined ( $lastpicked ) )
2224        {
2225            my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
2226            while ( <FILELIST> )
2227            {
2228                unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o )
2229                {
2230                    die("Couldn't process git-diff-tree line : $_");
2231                }
2232
2233                # $log->debug("File mode=$1, hash=$2, change=$3, name=$4");
2234
2235                my $git_perms = "";
2236                $git_perms .= "r" if ( $1 & 4 );
2237                $git_perms .= "w" if ( $1 & 2 );
2238                $git_perms .= "x" if ( $1 & 1 );
2239                $git_perms = "rw" if ( $git_perms eq "" );
2240
2241                if ( $3 eq "D" )
2242                {
2243                    #$log->debug("DELETE   $4");
2244                    $head->{$4} = {
2245                        name => $4,
2246                        revision => $head->{$4}{revision} + 1,
2247                        filehash => "deleted",
2248                        commithash => $commit->{hash},
2249                        modified => $commit->{date},
2250                        author => $commit->{author},
2251                        mode => $git_perms,
2252                    };
2253                    $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2254                }
2255                elsif ( $3 eq "M" )
2256                {
2257                    #$log->debug("MODIFIED $4");
2258                    $head->{$4} = {
2259                        name => $4,
2260                        revision => $head->{$4}{revision} + 1,
2261                        filehash => $2,
2262                        commithash => $commit->{hash},
2263                        modified => $commit->{date},
2264                        author => $commit->{author},
2265                        mode => $git_perms,
2266                    };
2267                    $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2268                }
2269                elsif ( $3 eq "A" )
2270                {
2271                    #$log->debug("ADDED    $4");
2272                    $head->{$4} = {
2273                        name => $4,
2274                        revision => 1,
2275                        filehash => $2,
2276                        commithash => $commit->{hash},
2277                        modified => $commit->{date},
2278                        author => $commit->{author},
2279                        mode => $git_perms,
2280                    };
2281                    $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2282                }
2283                else
2284                {
2285                    $log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");
2286                    die;
2287                }
2288            }
2289            close FILELIST;
2290        } else {
2291            # this is used to detect files removed from the repo
2292            my $seen_files = {};
2293
2294            my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
2295            while ( <FILELIST> )
2296            {
2297                unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o )
2298                {
2299                    die("Couldn't process git-ls-tree line : $_");
2300                }
2301
2302                my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2303
2304                $seen_files->{$git_filename} = 1;
2305
2306                my ( $oldhash, $oldrevision, $oldmode ) = (
2307                    $head->{$git_filename}{filehash},
2308                    $head->{$git_filename}{revision},
2309                    $head->{$git_filename}{mode}
2310                );
2311
2312                if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2313                {
2314                    $git_perms = "";
2315                    $git_perms .= "r" if ( $1 & 4 );
2316                    $git_perms .= "w" if ( $1 & 2 );
2317                    $git_perms .= "x" if ( $1 & 1 );
2318                } else {
2319                    $git_perms = "rw";
2320                }
2321
2322                # unless the file exists with the same hash, we need to update it ...
2323                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2324                {
2325                    my $newrevision = ( $oldrevision or 0 ) + 1;
2326
2327                    $head->{$git_filename} = {
2328                        name => $git_filename,
2329                        revision => $newrevision,
2330                        filehash => $git_hash,
2331                        commithash => $commit->{hash},
2332                        modified => $commit->{date},
2333                        author => $commit->{author},
2334                        mode => $git_perms,
2335                    };
2336
2337
2338                    $db_insert_rev->execute($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2339                }
2340            }
2341            close FILELIST;
2342
2343            # Detect deleted files
2344            foreach my $file ( keys %$head )
2345            {
2346                unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
2347                {
2348                    $head->{$file}{revision}++;
2349                    $head->{$file}{filehash} = "deleted";
2350                    $head->{$file}{commithash} = $commit->{hash};
2351                    $head->{$file}{modified} = $commit->{date};
2352                    $head->{$file}{author} = $commit->{author};
2353
2354                    $db_insert_rev->execute($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
2355                }
2356            }
2357            # END : "Detect deleted files"
2358        }
2359
2360
2361        if (exists $commit->{mergemsg})
2362        {
2363            $db_insert_mergelog->execute($commit->{hash}, $commit->{mergemsg});
2364        }
2365
2366        $lastpicked = $commit->{hash};
2367
2368        $self->_set_prop("last_commit", $commit->{hash});
2369    }
2370
2371    $db_delete_head->execute();
2372    foreach my $file ( keys %$head )
2373    {
2374        $db_insert_head->execute(
2375            $file,
2376            $head->{$file}{revision},
2377            $head->{$file}{filehash},
2378            $head->{$file}{commithash},
2379            $head->{$file}{modified},
2380            $head->{$file}{author},
2381            $head->{$file}{mode},
2382        );
2383    }
2384    # invalidate the gethead cache
2385    $self->{gethead_cache} = undef;
2386
2387
2388    # Ending exclusive lock here
2389    $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
2390}
2391
2392sub _headrev
2393{
2394    my $self = shift;
2395    my $filename = shift;
2396
2397    my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2398    $db_query->execute($filename);
2399    my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2400
2401    return ( $hash, $revision, $mode );
2402}
2403
2404sub _get_prop
2405{
2406    my $self = shift;
2407    my $key = shift;
2408
2409    my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2410    $db_query->execute($key);
2411    my ( $value ) = $db_query->fetchrow_array;
2412
2413    return $value;
2414}
2415
2416sub _set_prop
2417{
2418    my $self = shift;
2419    my $key = shift;
2420    my $value = shift;
2421
2422    my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2423    $db_query->execute($value, $key);
2424
2425    unless ( $db_query->rows )
2426    {
2427        $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2428        $db_query->execute($key, $value);
2429    }
2430
2431    return $value;
2432}
2433
2434=head2 gethead
2435
2436=cut
2437
2438sub gethead
2439{
2440    my $self = shift;
2441
2442    return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
2443
2444    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
2445    $db_query->execute();
2446
2447    my $tree = [];
2448    while ( my $file = $db_query->fetchrow_hashref )
2449    {
2450        push @$tree, $file;
2451    }
2452
2453    $self->{gethead_cache} = $tree;
2454
2455    return $tree;
2456}
2457
2458=head2 getlog
2459
2460=cut
2461
2462sub getlog
2463{
2464    my $self = shift;
2465    my $filename = shift;
2466
2467    my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2468    $db_query->execute($filename);
2469
2470    my $tree = [];
2471    while ( my $file = $db_query->fetchrow_hashref )
2472    {
2473        push @$tree, $file;
2474    }
2475
2476    return $tree;
2477}
2478
2479=head2 getmeta
2480
2481This function takes a filename (with path) argument and returns a hashref of
2482metadata for that file.
2483
2484=cut
2485
2486sub getmeta
2487{
2488    my $self = shift;
2489    my $filename = shift;
2490    my $revision = shift;
2491
2492    my $db_query;
2493    if ( defined($revision) and $revision =~ /^\d+$/ )
2494    {
2495        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2496        $db_query->execute($filename, $revision);
2497    }
2498    elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2499    {
2500        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2501        $db_query->execute($filename, $revision);
2502    } else {
2503        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2504        $db_query->execute($filename);
2505    }
2506
2507    return $db_query->fetchrow_hashref;
2508}
2509
2510=head2 commitmessage
2511
2512this function takes a commithash and returns the commit message for that commit
2513
2514=cut
2515sub commitmessage
2516{
2517    my $self = shift;
2518    my $commithash = shift;
2519
2520    die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2521
2522    my $db_query;
2523    $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2524    $db_query->execute($commithash);
2525
2526    my ( $message ) = $db_query->fetchrow_array;
2527
2528    if ( defined ( $message ) )
2529    {
2530        $message .= " " if ( $message =~ /\n$/ );
2531        return $message;
2532    }
2533
2534    my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
2535    shift @lines while ( $lines[0] =~ /\S/ );
2536    $message = join("",@lines);
2537    $message .= " " if ( $message =~ /\n$/ );
2538    return $message;
2539}
2540
2541=head2 gethistory
2542
2543This function takes a filename (with path) argument and returns an arrayofarrays
2544containing revision,filehash,commithash ordered by revision descending
2545
2546=cut
2547sub gethistory
2548{
2549    my $self = shift;
2550    my $filename = shift;
2551
2552    my $db_query;
2553    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2554    $db_query->execute($filename);
2555
2556    return $db_query->fetchall_arrayref;
2557}
2558
2559=head2 gethistorydense
2560
2561This function takes a filename (with path) argument and returns an arrayofarrays
2562containing revision,filehash,commithash ordered by revision descending.
2563
2564This version of gethistory skips deleted entries -- so it is useful for annotate.
2565The 'dense' part is a reference to a '--dense' option available for git-rev-list
2566and other git tools that depend on it.
2567
2568=cut
2569sub gethistorydense
2570{
2571    my $self = shift;
2572    my $filename = shift;
2573
2574    my $db_query;
2575    $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2576    $db_query->execute($filename);
2577
2578    return $db_query->fetchall_arrayref;
2579}
2580
2581=head2 in_array()
2582
2583from Array::PAT - mimics the in_array() function
2584found in PHP. Yuck but works for small arrays.
2585
2586=cut
2587sub in_array
2588{
2589    my ($check, @array) = @_;
2590    my $retval = 0;
2591    foreach my $test (@array){
2592        if($check eq $test){
2593            $retval =  1;
2594        }
2595    }
2596    return $retval;
2597}
2598
2599=head2 safe_pipe_capture
2600
2601an alternative to `command` that allows input to be passed as an array
2602to work around shell problems with weird characters in arguments
2603
2604=cut
2605sub safe_pipe_capture {
2606
2607    my @output;
2608
2609    if (my $pid = open my $child, '-|') {
2610        @output = (<$child>);
2611        close $child or die join(' ',@_).": $! $?";
2612    } else {
2613        exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
2614    }
2615    return wantarray ? @output : join('',@output);
2616}
2617
2618
26191;