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