Code

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