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