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