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