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