Code

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