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