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.
8 #### Copyright The Open University UK - 2006.
10 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
11 #### Martin Langhoff <martin@catalyst.net.nz>
14 #### Released under the GNU Public License, version 2.
23 use File::Temp qw/tempdir tempfile/;
25 use Getopt::Long qw(:config require_order no_ignore_case);
27 my $VERSION = '@@GIT_VERSION@@';
29 my $log = GITCVS::log->new();
47 # Enable autoflush for STDOUT (otherwise the whole thing falls apart)
50 #### Definition and mappings of functions ####
54 'Valid-responses' => \&req_Validresponses,
55 'valid-requests' => \&req_validrequests,
56 'Directory' => \&req_Directory,
57 'Entry' => \&req_Entry,
58 'Modified' => \&req_Modified,
59 'Unchanged' => \&req_Unchanged,
60 'Questionable' => \&req_Questionable,
61 'Argument' => \&req_Argument,
62 'Argumentx' => \&req_Argument,
63 'expand-modules' => \&req_expandmodules,
65 'remove' => \&req_remove,
67 'update' => \&req_update,
72 'tag' => \&req_CATCHALL,
73 'status' => \&req_status,
74 'admin' => \&req_CATCHALL,
75 'history' => \&req_CATCHALL,
76 'watchers' => \&req_CATCHALL,
77 'editors' => \&req_CATCHALL,
78 'annotate' => \&req_annotate,
79 'Global_option' => \&req_Globaloption,
80 #'annotate' => \&req_CATCHALL,
83 ##############################################
86 # $state holds all the bits of information the clients sends us that could
87 # potentially be useful when it comes to actually _doing_ something.
88 my $state = { prependdir => '' };
89 $log->info("--------------- STARTING -----------------");
92 "Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n".
93 " --base-path <path> : Prepend to requested CVSROOT\n".
94 " --strict-paths : Don't allow recursing into subdirectories\n".
95 " --export-all : Don't check for gitcvs.enabled in config\n".
96 " --version, -V : Print version information and exit\n".
97 " --help, -h, -H : Print usage information and exit\n".
99 "<directory> ... is a list of allowed directories. If no directories\n".
100 "are given, all are allowed. This is an additional restriction, gitcvs\n".
101 "access still needs to be enabled by the gitcvs.enabled config option.\n";
103 my @opts = ( 'help|h|H', 'version|V',
104 'base-path=s', 'strict-paths', 'export-all' );
105 GetOptions( $state, @opts )
108 if ($state->{version}) {
109 print "git-cvsserver version $VERSION\n";
112 if ($state->{help}) {
117 my $TEMP_DIR = tempdir( CLEANUP => 1 );
118 $log->debug("Temporary directory is '$TEMP_DIR'");
120 $state->{method} = 'ext';
122 if ($ARGV[0] eq 'pserver') {
123 $state->{method} = 'pserver';
125 } elsif ($ARGV[0] eq 'server') {
130 # everything else is a directory
131 $state->{allowed_roots} = [ @ARGV ];
133 # if we are called with a pserver argument,
134 # deal with the authentication cat before entering the
136 if ($state->{method} eq 'pserver') {
137 my $line = <STDIN>; chomp $line;
138 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
139 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
142 $line = <STDIN>; chomp $line;
143 req_Root('root', $line) # reuse Root
144 or die "E Invalid root $line \n";
145 $line = <STDIN>; chomp $line;
146 unless ($line eq 'anonymous') {
147 print "E Only anonymous user allowed via pserver\n";
148 print "I HATE YOU\n";
151 $line = <STDIN>; chomp $line; # validate the password?
152 $line = <STDIN>; chomp $line;
153 unless ($line eq "END $request REQUEST") {
154 die "E Do not understand $line -- expecting END $request REQUEST\n";
156 print "I LOVE YOU\n";
157 exit if $request eq 'VERIFICATION'; # cvs login
158 # and now back to our regular programme...
161 # Keep going until the client closes the connection
166 # Check to see if we've seen this method, and call appropriate function.
167 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
169 # use the $methods hash to call the appropriate sub for this command
170 #$log->info("Method : $1");
171 &{$methods->{$1}}($1,$2);
173 # log fatal because we don't understand this function. If this happens
174 # we're fairly screwed because we don't know if the client is expecting
175 # a response. If it is, the client will hang, we'll hang, and the whole
176 # thing will be custard.
177 $log->fatal("Don't understand command $_\n");
178 die("Unknown command $_");
182 $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
183 $log->info("--------------- FINISH -----------------");
185 # Magic catchall method.
186 # This is the method that will handle all commands we haven't yet
187 # implemented. It simply sends a warning to the log file indicating a
188 # command that hasn't been implemented has been invoked.
191 my ( $cmd, $data ) = @_;
192 $log->warn("Unhandled command : req_$cmd : $data");
197 # Response expected: no. Tell the server which CVSROOT to use. Note that
198 # pathname is a local directory and not a fully qualified CVSROOT variable.
199 # pathname must already exist; if creating a new root, use the init
200 # request, not Root. pathname does not include the hostname of the server,
201 # how to access the server, etc.; by the time the CVS protocol is in use,
202 # connection, authentication, etc., are already taken care of. The Root
203 # request must be sent only once, and it must be sent before any requests
204 # other than Valid-responses, valid-requests, UseUnchanged, Set or init.
207 my ( $cmd, $data ) = @_;
208 $log->debug("req_Root : $data");
210 unless ($data =~ m#^/#) {
211 print "error 1 Root must be an absolute pathname\n";
215 my $cvsroot = $state->{'base-path'} || '';
219 if ($state->{CVSROOT}
220 && ($state->{CVSROOT} ne $cvsroot)) {
221 print "error 1 Conflicting roots specified\n";
225 $state->{CVSROOT} = $cvsroot;
227 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
229 if (@{$state->{allowed_roots}}) {
231 foreach my $dir (@{$state->{allowed_roots}}) {
232 next unless $dir =~ m#^/#;
234 if ($state->{'strict-paths'}) {
235 if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
239 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
246 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
248 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
253 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
254 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
256 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
260 my @gitvars = `git-config -l`;
262 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
264 print "error 1 - problem executing git-config\n";
267 foreach my $line ( @gitvars )
269 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
273 $cfg->{$1}{$2}{$3} = $4;
277 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
278 || $cfg->{gitcvs}{enabled});
279 unless ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i) {
280 print "E GITCVS emulation needs to be enabled on this repo\n";
281 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
283 print "error 1 GITCVS emulation disabled\n";
287 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
290 $log->setfile($logfile);
298 # Global_option option \n
299 # Response expected: no. Transmit one of the global options `-q', `-Q',
300 # `-l', `-t', `-r', or `-n'. option must be one of those strings, no
301 # variations (such as combining of options) are allowed. For graceful
302 # handling of valid-requests, it is probably better to make new global
303 # options separate requests, rather than trying to add them to this
307 my ( $cmd, $data ) = @_;
308 $log->debug("req_Globaloption : $data");
309 $state->{globaloptions}{$data} = 1;
312 # Valid-responses request-list \n
313 # Response expected: no. Tell the server what responses the client will
314 # accept. request-list is a space separated list of tokens.
315 sub req_Validresponses
317 my ( $cmd, $data ) = @_;
318 $log->debug("req_Validresponses : $data");
320 # TODO : re-enable this, currently it's not particularly useful
321 #$state->{validresponses} = [ split /\s+/, $data ];
325 # Response expected: yes. Ask the server to send back a Valid-requests
327 sub req_validrequests
329 my ( $cmd, $data ) = @_;
331 $log->debug("req_validrequests");
333 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
334 $log->debug("SEND : ok");
336 print "Valid-requests " . join(" ",keys %$methods) . "\n";
340 # Directory local-directory \n
341 # Additional data: repository \n. Response expected: no. Tell the server
342 # what directory to use. The repository should be a directory name from a
343 # previous server response. Note that this both gives a default for Entry
344 # and Modified and also for ci and the other commands; normal usage is to
345 # send Directory for each directory in which there will be an Entry or
346 # Modified, and then a final Directory for the original directory, then the
347 # command. The local-directory is relative to the top level at which the
348 # command is occurring (i.e. the last Directory which is sent before the
349 # command); to indicate that top level, `.' should be sent for
353 my ( $cmd, $data ) = @_;
355 my $repository = <STDIN>;
359 $state->{localdir} = $data;
360 $state->{repository} = $repository;
361 $state->{path} = $repository;
362 $state->{path} =~ s/^$state->{CVSROOT}\///;
363 $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
364 $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
366 $state->{directory} = $state->{localdir};
367 $state->{directory} = "" if ( $state->{directory} eq "." );
368 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
370 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
372 $log->info("Setting prepend to '$state->{path}'");
373 $state->{prependdir} = $state->{path};
374 foreach my $entry ( keys %{$state->{entries}} )
376 $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
377 delete $state->{entries}{$entry};
381 if ( defined ( $state->{prependdir} ) )
383 $log->debug("Prepending '$state->{prependdir}' to state|directory");
384 $state->{directory} = $state->{prependdir} . $state->{directory}
386 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
389 # Entry entry-line \n
390 # Response expected: no. Tell the server what version of a file is on the
391 # local machine. The name in entry-line is a name relative to the directory
392 # most recently specified with Directory. If the user is operating on only
393 # some files in a directory, Entry requests for only those files need be
394 # included. If an Entry request is sent without Modified, Is-modified, or
395 # Unchanged, it means the file is lost (does not exist in the working
396 # directory). If both Entry and one of Modified, Is-modified, or Unchanged
397 # are sent for the same file, Entry must be sent first. For a given file,
398 # one can send Modified, Is-modified, or Unchanged, but not more than one
402 my ( $cmd, $data ) = @_;
404 #$log->debug("req_Entry : $data");
406 my @data = split(/\//, $data);
408 $state->{entries}{$state->{directory}.$data[1]} = {
409 revision => $data[2],
410 conflict => $data[3],
412 tag_or_date => $data[5],
415 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
418 # Questionable filename \n
419 # Response expected: no. Additional data: no. Tell the server to check
420 # whether filename should be ignored, and if not, next time the server
421 # sends responses, send (in a M response) `?' followed by the directory and
422 # filename. filename must not contain `/'; it needs to be a file in the
423 # directory named by the most recent Directory request.
426 my ( $cmd, $data ) = @_;
428 $log->debug("req_Questionable : $data");
429 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
433 # Response expected: yes. Add a file or directory. This uses any previous
434 # Argument, Directory, Entry, or Modified requests, if they have been sent.
435 # The last Directory sent specifies the working directory at the time of
436 # the operation. To add a directory, send the directory to be added using
437 # Directory and Argument requests.
440 my ( $cmd, $data ) = @_;
444 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
447 argsfromdir($updater);
451 foreach my $filename ( @{$state->{args}} )
453 $filename = filecleanup($filename);
455 my $meta = $updater->getmeta($filename);
456 my $wrev = revparse($filename);
458 if ($wrev && $meta && ($wrev < 0))
460 # previously removed file, add back
461 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
463 print "MT +updated\n";
464 print "MT text U \n";
465 print "MT fname $filename\n";
466 print "MT newline\n";
467 print "MT -updated\n";
469 unless ( $state->{globaloptions}{-n} )
471 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
473 print "Created $dirpart\n";
474 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
476 # this is an "entries" line
477 my $kopts = kopts_from_path($filepart);
478 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
479 print "/$filepart/1.$meta->{revision}//$kopts/\n";
481 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
482 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
484 transmitfile($meta->{filehash});
490 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
492 print "E cvs add: nothing known about `$filename'\n";
495 # TODO : check we're not squashing an already existing file
496 if ( defined ( $state->{entries}{$filename}{revision} ) )
498 print "E cvs add: `$filename' has already been entered\n";
502 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
504 print "E cvs add: scheduling file `$filename' for addition\n";
506 print "Checked-in $dirpart\n";
508 my $kopts = kopts_from_path($filepart);
509 print "/$filepart/0//$kopts/\n";
514 if ( $addcount == 1 )
516 print "E cvs add: use `cvs commit' to add this file permanently\n";
518 elsif ( $addcount > 1 )
520 print "E cvs add: use `cvs commit' to add these files permanently\n";
527 # Response expected: yes. Remove a file. This uses any previous Argument,
528 # Directory, Entry, or Modified requests, if they have been sent. The last
529 # Directory sent specifies the working directory at the time of the
530 # operation. Note that this request does not actually do anything to the
531 # repository; the only effect of a successful remove request is to supply
532 # the client with a new entries line containing `-' to indicate a removed
533 # file. In fact, the client probably could perform this operation without
534 # contacting the server, although using remove may cause the server to
535 # perform a few more checks. The client sends a subsequent ci request to
536 # actually record the removal in the repository.
539 my ( $cmd, $data ) = @_;
543 # Grab a handle to the SQLite db and do any necessary updates
544 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
547 #$log->debug("add state : " . Dumper($state));
551 foreach my $filename ( @{$state->{args}} )
553 $filename = filecleanup($filename);
555 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
557 print "E cvs remove: file `$filename' still in working directory\n";
561 my $meta = $updater->getmeta($filename);
562 my $wrev = revparse($filename);
564 unless ( defined ( $wrev ) )
566 print "E cvs remove: nothing known about `$filename'\n";
570 if ( defined($wrev) and $wrev < 0 )
572 print "E cvs remove: file `$filename' already scheduled for removal\n";
576 unless ( $wrev == $meta->{revision} )
578 # TODO : not sure if the format of this message is quite correct.
579 print "E cvs remove: Up to date check failed for `$filename'\n";
584 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
586 print "E cvs remove: scheduling `$filename' for removal\n";
588 print "Checked-in $dirpart\n";
590 my $kopts = kopts_from_path($filepart);
591 print "/$filepart/-1.$wrev//$kopts/\n";
598 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
600 elsif ( $rmcount > 1 )
602 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
608 # Modified filename \n
609 # Response expected: no. Additional data: mode, \n, file transmission. Send
610 # the server a copy of one locally modified file. filename is a file within
611 # the most recent directory sent with Directory; it must not contain `/'.
612 # If the user is operating on only some files in a directory, only those
613 # files need to be included. This can also be sent without Entry, if there
614 # is no entry for the file.
617 my ( $cmd, $data ) = @_;
624 # Grab config information
625 my $blocksize = 8192;
626 my $bytesleft = $size;
629 # Get a filehandle/name to write it to
630 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
632 # Loop over file data writing out to temporary file.
635 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
636 read STDIN, $tmp, $blocksize;
638 $bytesleft -= $blocksize;
643 # Ensure we have something sensible for the file mode
644 if ( $mode =~ /u=(\w+)/ )
651 # Save the file data in $state
652 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
653 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
654 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
655 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
657 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
660 # Unchanged filename \n
661 # Response expected: no. Tell the server that filename has not been
662 # modified in the checked out directory. The filename is a file within the
663 # most recent directory sent with Directory; it must not contain `/'.
666 my ( $cmd, $data ) = @_;
668 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
670 #$log->debug("req_Unchanged : $data");
674 # Response expected: no. Save argument for use in a subsequent command.
675 # Arguments accumulate until an argument-using command is given, at which
676 # point they are forgotten.
678 # Response expected: no. Append \n followed by text to the current argument
682 my ( $cmd, $data ) = @_;
684 # Argumentx means: append to last Argument (with a newline in front)
686 $log->debug("$cmd : $data");
688 if ( $cmd eq 'Argumentx') {
689 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
691 push @{$state->{arguments}}, $data;
696 # Response expected: yes. Expand the modules which are specified in the
697 # arguments. Returns the data in Module-expansion responses. Note that the
698 # server can assume that this is checkout or export, not rtag or rdiff; the
699 # latter do not access the working directory and thus have no need to
700 # expand modules on the client side. Expand may not be the best word for
701 # what this request does. It does not necessarily tell you all the files
702 # contained in a module, for example. Basically it is a way of telling you
703 # which working directories the server needs to know about in order to
704 # handle a checkout of the specified modules. For example, suppose that the
705 # server has a module defined by
706 # aliasmodule -a 1dir
707 # That is, one can check out aliasmodule and it will take 1dir in the
708 # repository and check it out to 1dir in the working directory. Now suppose
709 # the client already has this module checked out and is planning on using
710 # the co request to update it. Without using expand-modules, the client
711 # would have two bad choices: it could either send information about all
712 # working directories under the current directory, which could be
713 # unnecessarily slow, or it could be ignorant of the fact that aliasmodule
714 # stands for 1dir, and neglect to send information for 1dir, which would
715 # lead to incorrect operation. With expand-modules, the client would first
716 # ask for the module to be expanded:
717 sub req_expandmodules
719 my ( $cmd, $data ) = @_;
723 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
725 unless ( ref $state->{arguments} eq "ARRAY" )
731 foreach my $module ( @{$state->{arguments}} )
733 $log->debug("SEND : Module-expansion $module");
734 print "Module-expansion $module\n";
742 # Response expected: yes. Get files from the repository. This uses any
743 # previous Argument, Directory, Entry, or Modified requests, if they have
744 # been sent. Arguments to this command are module names; the client cannot
745 # know what directories they correspond to except by (1) just sending the
746 # co request, and then seeing what directory names the server sends back in
747 # its responses, and (2) the expand-modules request.
750 my ( $cmd, $data ) = @_;
754 my $module = $state->{args}[0];
755 my $checkout_path = $module;
757 # use the user specified directory if we're given it
758 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
760 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
762 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
764 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
766 # Grab a handle to the SQLite db and do any necessary updates
767 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
770 $checkout_path =~ s|/$||; # get rid of trailing slashes
772 # Eclipse seems to need the Clear-sticky command
773 # to prepare the 'Entries' file for the new directory.
774 print "Clear-sticky $checkout_path/\n";
775 print $state->{CVSROOT} . "/$module/\n";
776 print "Clear-static-directory $checkout_path/\n";
777 print $state->{CVSROOT} . "/$module/\n";
778 print "Clear-sticky $checkout_path/\n"; # yes, twice
779 print $state->{CVSROOT} . "/$module/\n";
780 print "Template $checkout_path/\n";
781 print $state->{CVSROOT} . "/$module/\n";
784 # instruct the client that we're checking out to $checkout_path
785 print "E cvs checkout: Updating $checkout_path\n";
792 my ($dir, $repodir, $remotedir, $seendirs) = @_;
793 my $parent = dirname($dir);
796 $remotedir =~ s|/+$||;
798 $log->debug("announcedir $dir, $repodir, $remotedir" );
800 if ($parent eq '.' || $parent eq './') {
803 # recurse to announce unseen parents first
804 if (length($parent) && !exists($seendirs->{$parent})) {
805 prepdir($parent, $repodir, $remotedir, $seendirs);
807 # Announce that we are going to modify at the parent level
809 print "E cvs checkout: Updating $remotedir/$parent\n";
811 print "E cvs checkout: Updating $remotedir\n";
813 print "Clear-sticky $remotedir/$parent/\n";
814 print "$repodir/$parent/\n";
816 print "Clear-static-directory $remotedir/$dir/\n";
817 print "$repodir/$dir/\n";
818 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
819 print "$repodir/$parent/\n";
820 print "Template $remotedir/$dir/\n";
821 print "$repodir/$dir/\n";
824 $seendirs->{$dir} = 1;
827 foreach my $git ( @{$updater->gethead} )
829 # Don't want to check out deleted files
830 next if ( $git->{filehash} eq "deleted" );
832 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
834 if (length($git->{dir}) && $git->{dir} ne './'
835 && $git->{dir} ne $lastdir ) {
836 unless (exists($seendirs{$git->{dir}})) {
837 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
838 $checkout_path, \%seendirs);
839 $lastdir = $git->{dir};
840 $seendirs{$git->{dir}} = 1;
842 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
845 # modification time of this file
846 print "Mod-time $git->{modified}\n";
848 # print some information to the client
849 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
851 print "M U $checkout_path/$git->{dir}$git->{name}\n";
853 print "M U $checkout_path/$git->{name}\n";
856 # instruct client we're sending a file to put in this path
857 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
859 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
861 # this is an "entries" line
862 my $kopts = kopts_from_path($git->{name});
863 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
865 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
868 transmitfile($git->{filehash});
877 # Response expected: yes. Actually do a cvs update command. This uses any
878 # previous Argument, Directory, Entry, or Modified requests, if they have
879 # been sent. The last Directory sent specifies the working directory at the
880 # time of the operation. The -I option is not used--files which the client
881 # can decide whether to ignore are not mentioned and the client sends the
882 # Questionable request for others.
885 my ( $cmd, $data ) = @_;
887 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
892 # It may just be a client exploring the available heads/modules
893 # in that case, list them as top level directories and leave it
894 # at that. Eclipse uses this technique to offer you a list of
895 # projects (heads in this case) to checkout.
897 if ($state->{module} eq '') {
898 print "E cvs update: Updating .\n";
899 opendir HEADS, $state->{CVSROOT} . '/refs/heads';
900 while (my $head = readdir(HEADS)) {
901 if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
902 print "E cvs update: New directory `$head'\n";
911 # Grab a handle to the SQLite db and do any necessary updates
912 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
916 argsfromdir($updater);
918 #$log->debug("update state : " . Dumper($state));
920 # foreach file specified on the command line ...
921 foreach my $filename ( @{$state->{args}} )
923 $filename = filecleanup($filename);
925 $log->debug("Processing file $filename");
927 # if we have a -C we should pretend we never saw modified stuff
928 if ( exists ( $state->{opt}{C} ) )
930 delete $state->{entries}{$filename}{modified_hash};
931 delete $state->{entries}{$filename}{modified_filename};
932 $state->{entries}{$filename}{unchanged} = 1;
936 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
938 $meta = $updater->getmeta($filename, $1);
940 $meta = $updater->getmeta($filename);
943 if ( ! defined $meta )
954 my $wrev = revparse($filename);
956 # If the working copy is an old revision, lets get that version too for comparison.
957 if ( defined($wrev) and $wrev != $meta->{revision} )
959 $oldmeta = $updater->getmeta($filename, $wrev);
962 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
964 # Files are up to date if the working copy and repo copy have the same revision,
965 # and the working copy is unmodified _and_ the user hasn't specified -C
966 next if ( defined ( $wrev )
967 and defined($meta->{revision})
968 and $wrev == $meta->{revision}
969 and $state->{entries}{$filename}{unchanged}
970 and not exists ( $state->{opt}{C} ) );
972 # If the working copy and repo copy have the same revision,
973 # but the working copy is modified, tell the client it's modified
974 if ( defined ( $wrev )
975 and defined($meta->{revision})
976 and $wrev == $meta->{revision}
977 and defined($state->{entries}{$filename}{modified_hash})
978 and not exists ( $state->{opt}{C} ) )
980 $log->info("Tell the client the file is modified");
981 print "MT text M \n";
982 print "MT fname $filename\n";
983 print "MT newline\n";
987 if ( $meta->{filehash} eq "deleted" )
989 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
991 $log->info("Removing '$filename' from working copy (no longer in the repo)");
993 print "E cvs update: `$filename' is no longer in the repository\n";
994 # Don't want to actually _DO_ the update if -n specified
995 unless ( $state->{globaloptions}{-n} ) {
996 print "Removed $dirpart\n";
1000 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
1001 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1002 or $meta->{filehash} eq 'added' )
1004 # normal update, just send the new revision (either U=Update,
1005 # or A=Add, or R=Remove)
1006 if ( defined($wrev) && $wrev < 0 )
1008 $log->info("Tell the client the file is scheduled for removal");
1009 print "MT text R \n";
1010 print "MT fname $filename\n";
1011 print "MT newline\n";
1014 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
1016 $log->info("Tell the client the file is scheduled for addition");
1017 print "MT text A \n";
1018 print "MT fname $filename\n";
1019 print "MT newline\n";
1024 $log->info("Updating '$filename' to ".$meta->{revision});
1025 print "MT +updated\n";
1026 print "MT text U \n";
1027 print "MT fname $filename\n";
1028 print "MT newline\n";
1029 print "MT -updated\n";
1032 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1034 # Don't want to actually _DO_ the update if -n specified
1035 unless ( $state->{globaloptions}{-n} )
1037 if ( defined ( $wrev ) )
1039 # instruct client we're sending a file to put in this path as a replacement
1040 print "Update-existing $dirpart\n";
1041 $log->debug("Updating existing file 'Update-existing $dirpart'");
1043 # instruct client we're sending a file to put in this path as a new file
1044 print "Clear-static-directory $dirpart\n";
1045 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1046 print "Clear-sticky $dirpart\n";
1047 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1049 $log->debug("Creating new file 'Created $dirpart'");
1050 print "Created $dirpart\n";
1052 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1054 # this is an "entries" line
1055 my $kopts = kopts_from_path($filepart);
1056 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1057 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1060 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1061 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1064 transmitfile($meta->{filehash});
1067 $log->info("Updating '$filename'");
1068 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
1070 my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
1073 my $file_local = $filepart . ".mine";
1074 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1075 my $file_old = $filepart . "." . $oldmeta->{revision};
1076 transmitfile($oldmeta->{filehash}, $file_old);
1077 my $file_new = $filepart . "." . $meta->{revision};
1078 transmitfile($meta->{filehash}, $file_new);
1080 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1081 $log->info("Merging $file_local, $file_old, $file_new");
1082 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
1084 $log->debug("Temporary directory for merge is $dir");
1086 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1091 $log->info("Merged successfully");
1092 print "M M $filename\n";
1093 $log->debug("Merged $dirpart");
1095 # Don't want to actually _DO_ the update if -n specified
1096 unless ( $state->{globaloptions}{-n} )
1098 print "Merged $dirpart\n";
1099 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1100 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1101 my $kopts = kopts_from_path($filepart);
1102 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1103 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1106 elsif ( $return == 1 )
1108 $log->info("Merged with conflicts");
1109 print "E cvs update: conflicts found in $filename\n";
1110 print "M C $filename\n";
1112 # Don't want to actually _DO_ the update if -n specified
1113 unless ( $state->{globaloptions}{-n} )
1115 print "Merged $dirpart\n";
1116 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1117 my $kopts = kopts_from_path($filepart);
1118 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
1123 $log->warn("Merge failed");
1127 # Don't want to actually _DO_ the update if -n specified
1128 unless ( $state->{globaloptions}{-n} )
1131 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1132 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1134 # transmit file, format is single integer on a line by itself (file
1135 # size) followed by the file contents
1136 # TODO : we should copy files in blocks
1137 my $data = `cat $file_local`;
1138 $log->debug("File size : " . length($data));
1139 print length($data) . "\n";
1153 my ( $cmd, $data ) = @_;
1157 #$log->debug("State : " . Dumper($state));
1159 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1161 if ( $state->{method} eq 'pserver')
1163 print "error 1 pserver access cannot commit\n";
1167 if ( -e $state->{CVSROOT} . "/index" )
1169 $log->warn("file 'index' already exists in the git repository");
1170 print "error 1 Index already exists in git repo\n";
1174 # Grab a handle to the SQLite db and do any necessary updates
1175 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1178 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1179 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1180 $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");
1182 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1183 $ENV{GIT_INDEX_FILE} = $file_index;
1185 # Remember where the head was at the beginning.
1186 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1188 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1189 print "error 1 pserver cannot find the current HEAD of module";
1195 # populate the temporary index based
1196 system("git-read-tree", $parenthash);
1199 die "Error running git-read-tree $state->{module} $file_index $!";
1201 $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
1203 my @committedfiles = ();
1206 # foreach file specified on the command line ...
1207 foreach my $filename ( @{$state->{args}} )
1209 my $committedfile = $filename;
1210 $filename = filecleanup($filename);
1212 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1214 my $meta = $updater->getmeta($filename);
1215 $oldmeta{$filename} = $meta;
1217 my $wrev = revparse($filename);
1219 my ( $filepart, $dirpart ) = filenamesplit($filename);
1221 # do a checkout of the file if it part of this tree
1223 system('git-checkout-index', '-f', '-u', $filename);
1225 die "Error running git-checkout-index -f -u $filename : $!";
1231 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1232 $addflag = 1 unless ( -e $filename );
1234 # Do up to date checking
1235 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1237 # fail everything if an up to date check fails
1238 print "error 1 Up to date check failed for $filename\n";
1243 push @committedfiles, $committedfile;
1244 $log->info("Committing $filename");
1246 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1250 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1251 rename $state->{entries}{$filename}{modified_filename},$filename;
1253 # Calculate modes to remove
1255 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1257 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1258 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1263 $log->info("Removing file '$filename'");
1265 system("git-update-index", "--remove", $filename);
1269 $log->info("Adding file '$filename'");
1270 system("git-update-index", "--add", $filename);
1272 $log->info("Updating file '$filename'");
1273 system("git-update-index", $filename);
1277 unless ( scalar(@committedfiles) > 0 )
1279 print "E No files to commit\n";
1285 my $treehash = `git-write-tree`;
1288 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1290 # write our commit message out if we have one ...
1291 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1292 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1293 print $msg_fh "\n\nvia git-CVS emulator\n";
1296 my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1298 $log->info("Commit hash : $commithash");
1300 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1302 $log->warn("Commit failed (Invalid commit hash)");
1303 print "error 1 Commit failed (unknown reason)\n";
1308 # Check that this is allowed, just as we would with a receive-pack
1309 my @cmd = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1310 $parenthash, $commithash );
1312 unless( system( @cmd ) == 0 )
1314 $log->warn("Commit failed (update hook declined to update ref)");
1315 print "error 1 Commit failed (update hook declined)\n";
1321 if (system(qw(git update-ref -m), "cvsserver ci",
1322 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1323 $log->warn("update-ref for $state->{module} failed.");
1324 print "error 1 Cannot commit -- update first\n";
1330 # foreach file specified on the command line ...
1331 foreach my $filename ( @committedfiles )
1333 $filename = filecleanup($filename);
1335 my $meta = $updater->getmeta($filename);
1336 unless (defined $meta->{revision}) {
1337 $meta->{revision} = 1;
1340 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
1342 $log->debug("Checked-in $dirpart : $filename");
1344 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
1345 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
1347 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
1348 print "Remove-entry $dirpart\n";
1349 print "$filename\n";
1351 if ($meta->{revision} == 1) {
1352 print "M initial revision: 1.1\n";
1354 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
1356 print "Checked-in $dirpart\n";
1357 print "$filename\n";
1358 my $kopts = kopts_from_path($filepart);
1359 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1369 my ( $cmd, $data ) = @_;
1373 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1374 #$log->debug("status state : " . Dumper($state));
1376 # Grab a handle to the SQLite db and do any necessary updates
1377 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1380 # if no files were specified, we need to work out what files we should be providing status on ...
1381 argsfromdir($updater);
1383 # foreach file specified on the command line ...
1384 foreach my $filename ( @{$state->{args}} )
1386 $filename = filecleanup($filename);
1388 my $meta = $updater->getmeta($filename);
1389 my $oldmeta = $meta;
1391 my $wrev = revparse($filename);
1393 # If the working copy is an old revision, lets get that version too for comparison.
1394 if ( defined($wrev) and $wrev != $meta->{revision} )
1396 $oldmeta = $updater->getmeta($filename, $wrev);
1399 # TODO : All possible statuses aren't yet implemented
1401 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1402 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1404 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1405 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1408 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1409 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1411 ( $state->{entries}{$filename}{unchanged}
1412 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1415 # Need checkout if it exists in the repo but doesn't have a working copy
1416 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1418 # Locally modified if working copy and repo copy have the same revision but there are local changes
1419 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1421 # Needs Merge if working copy revision is less than repo copy and there are local changes
1422 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1424 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1425 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1426 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1427 $status ||= "File had conflicts on merge" if ( 0 );
1429 $status ||= "Unknown";
1431 print "M ===================================================================\n";
1432 print "M File: $filename\tStatus: $status\n";
1433 if ( defined($state->{entries}{$filename}{revision}) )
1435 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1437 print "M Working revision:\tNo entry for $filename\n";
1439 if ( defined($meta->{revision}) )
1441 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1442 print "M Sticky Tag:\t\t(none)\n";
1443 print "M Sticky Date:\t\t(none)\n";
1444 print "M Sticky Options:\t\t(none)\n";
1446 print "M Repository revision:\tNo revision control file\n";
1456 my ( $cmd, $data ) = @_;
1460 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1461 #$log->debug("status state : " . Dumper($state));
1463 my ($revision1, $revision2);
1464 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1466 $revision1 = $state->{opt}{r}[0];
1467 $revision2 = $state->{opt}{r}[1];
1469 $revision1 = $state->{opt}{r};
1472 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1473 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1475 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1477 # Grab a handle to the SQLite db and do any necessary updates
1478 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1481 # if no files were specified, we need to work out what files we should be providing status on ...
1482 argsfromdir($updater);
1484 # foreach file specified on the command line ...
1485 foreach my $filename ( @{$state->{args}} )
1487 $filename = filecleanup($filename);
1489 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1491 my $wrev = revparse($filename);
1493 # We need _something_ to diff against
1494 next unless ( defined ( $wrev ) );
1496 # if we have a -r switch, use it
1497 if ( defined ( $revision1 ) )
1499 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1500 $meta1 = $updater->getmeta($filename, $revision1);
1501 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1503 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1506 transmitfile($meta1->{filehash}, $file1);
1508 # otherwise we just use the working copy revision
1511 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1512 $meta1 = $updater->getmeta($filename, $wrev);
1513 transmitfile($meta1->{filehash}, $file1);
1516 # if we have a second -r switch, use it too
1517 if ( defined ( $revision2 ) )
1519 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1520 $meta2 = $updater->getmeta($filename, $revision2);
1522 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1524 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1528 transmitfile($meta2->{filehash}, $file2);
1530 # otherwise we just use the working copy
1533 $file2 = $state->{entries}{$filename}{modified_filename};
1536 # if we have been given -r, and we don't have a $file2 yet, lets get one
1537 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1539 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1540 $meta2 = $updater->getmeta($filename, $wrev);
1541 transmitfile($meta2->{filehash}, $file2);
1544 # We need to have retrieved something useful
1545 next unless ( defined ( $meta1 ) );
1547 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1548 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1550 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1551 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1554 # Apparently we only show diffs for locally modified files
1555 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1557 print "M Index: $filename\n";
1558 print "M ===================================================================\n";
1559 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1560 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1561 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1563 foreach my $opt ( keys %{$state->{opt}} )
1565 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1567 foreach my $value ( @{$state->{opt}{$opt}} )
1569 print "-$opt $value ";
1573 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1576 print "$filename\n";
1578 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1580 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1582 if ( exists $state->{opt}{u} )
1584 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1586 system("diff $file1 $file2 > $filediff");
1601 my ( $cmd, $data ) = @_;
1605 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1606 #$log->debug("log state : " . Dumper($state));
1608 my ( $minrev, $maxrev );
1609 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1614 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1615 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1616 $minrev++ if ( defined($minrev) and $control eq "::" );
1619 # Grab a handle to the SQLite db and do any necessary updates
1620 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1623 # if no files were specified, we need to work out what files we should be providing status on ...
1624 argsfromdir($updater);
1626 # foreach file specified on the command line ...
1627 foreach my $filename ( @{$state->{args}} )
1629 $filename = filecleanup($filename);
1631 my $headmeta = $updater->getmeta($filename);
1633 my $revisions = $updater->getlog($filename);
1634 my $totalrevisions = scalar(@$revisions);
1636 if ( defined ( $minrev ) )
1638 $log->debug("Removing revisions less than $minrev");
1639 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1644 if ( defined ( $maxrev ) )
1646 $log->debug("Removing revisions greater than $maxrev");
1647 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1653 next unless ( scalar(@$revisions) );
1656 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1657 print "M Working file: $filename\n";
1658 print "M head: 1.$headmeta->{revision}\n";
1659 print "M branch:\n";
1660 print "M locks: strict\n";
1661 print "M access list:\n";
1662 print "M symbolic names:\n";
1663 print "M keyword substitution: kv\n";
1664 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1665 print "M description:\n";
1667 foreach my $revision ( @$revisions )
1669 print "M ----------------------------\n";
1670 print "M revision 1.$revision->{revision}\n";
1671 # reformat the date for log output
1672 $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}) );
1673 $revision->{author} =~ s/\s+.*//;
1674 $revision->{author} =~ s/^(.{8}).*/$1/;
1675 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1676 my $commitmessage = $updater->commitmessage($revision->{commithash});
1677 $commitmessage =~ s/^/M /mg;
1678 print $commitmessage . "\n";
1680 print "M =============================================================================\n";
1688 my ( $cmd, $data ) = @_;
1690 argsplit("annotate");
1692 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1693 #$log->debug("status state : " . Dumper($state));
1695 # Grab a handle to the SQLite db and do any necessary updates
1696 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1699 # if no files were specified, we need to work out what files we should be providing annotate on ...
1700 argsfromdir($updater);
1702 # we'll need a temporary checkout dir
1703 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1704 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1705 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1707 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1708 $ENV{GIT_INDEX_FILE} = $file_index;
1712 # foreach file specified on the command line ...
1713 foreach my $filename ( @{$state->{args}} )
1715 $filename = filecleanup($filename);
1717 my $meta = $updater->getmeta($filename);
1719 next unless ( $meta->{revision} );
1721 # get all the commits that this file was in
1722 # in dense format -- aka skip dead revisions
1723 my $revisions = $updater->gethistorydense($filename);
1724 my $lastseenin = $revisions->[0][2];
1726 # populate the temporary index based on the latest commit were we saw
1727 # the file -- but do it cheaply without checking out any files
1728 # TODO: if we got a revision from the client, use that instead
1729 # to look up the commithash in sqlite (still good to default to
1730 # the current head as we do now)
1731 system("git-read-tree", $lastseenin);
1734 die "Error running git-read-tree $lastseenin $file_index $!";
1736 $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1738 # do a checkout of the file
1739 system('git-checkout-index', '-f', '-u', $filename);
1741 die "Error running git-checkout-index -f -u $filename : $!";
1744 $log->info("Annotate $filename");
1746 # Prepare a file with the commits from the linearized
1747 # history that annotate should know about. This prevents
1748 # git-jsannotate telling us about commits we are hiding
1751 open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
1752 for (my $i=0; $i < @$revisions; $i++)
1754 print ANNOTATEHINTS $revisions->[$i][2];
1755 if ($i+1 < @$revisions) { # have we got a parent?
1756 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1758 print ANNOTATEHINTS "\n";
1761 print ANNOTATEHINTS "\n";
1762 close ANNOTATEHINTS;
1764 my $annotatecmd = 'git-annotate';
1765 open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
1766 or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
1768 print "E Annotations for $filename\n";
1769 print "E ***************\n";
1770 while ( <ANNOTATE> )
1772 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1774 my $commithash = $1;
1776 unless ( defined ( $metadata->{$commithash} ) )
1778 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1779 $metadata->{$commithash}{author} =~ s/\s+.*//;
1780 $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
1781 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1783 printf("M 1.%-5d (%-8s %10s): %s\n",
1784 $metadata->{$commithash}{revision},
1785 $metadata->{$commithash}{author},
1786 $metadata->{$commithash}{modified},
1790 $log->warn("Error in annotate output! LINE: $_");
1791 print "E Annotate error \n";
1798 # done; get out of the tempdir
1805 # This method takes the state->{arguments} array and produces two new arrays.
1806 # The first is $state->{args} which is everything before the '--' argument, and
1807 # the second is $state->{files} which is everything after it.
1810 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1814 $state->{args} = [];
1815 $state->{files} = [];
1818 if ( defined($type) )
1821 $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" );
1822 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1823 $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" );
1824 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1825 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1826 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1827 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1828 $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" );
1831 while ( scalar ( @{$state->{arguments}} ) > 0 )
1833 my $arg = shift @{$state->{arguments}};
1835 next if ( $arg eq "--" );
1836 next unless ( $arg =~ /\S/ );
1838 # if the argument looks like a switch
1839 if ( $arg =~ /^-(\w)(.*)/ )
1841 # if it's a switch that takes an argument
1844 # If this switch has already been provided
1845 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1847 $state->{opt}{$1} = [ $state->{opt}{$1} ];
1848 if ( length($2) > 0 )
1850 push @{$state->{opt}{$1}},$2;
1852 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1855 # if there's extra data in the arg, use that as the argument for the switch
1856 if ( length($2) > 0 )
1858 $state->{opt}{$1} = $2;
1860 $state->{opt}{$1} = shift @{$state->{arguments}};
1864 $state->{opt}{$1} = undef;
1869 push @{$state->{args}}, $arg;
1877 foreach my $value ( @{$state->{arguments}} )
1879 if ( $value eq "--" )
1884 push @{$state->{args}}, $value if ( $mode == 0 );
1885 push @{$state->{files}}, $value if ( $mode == 1 );
1890 # This method uses $state->{directory} to populate $state->{args} with a list of filenames
1893 my $updater = shift;
1895 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
1897 return if ( scalar ( @{$state->{args}} ) > 1 );
1899 my @gethead = @{$updater->gethead};
1902 foreach my $file (keys %{$state->{entries}}) {
1903 if ( exists $state->{entries}{$file}{revision} &&
1904 $state->{entries}{$file}{revision} == 0 )
1906 push @gethead, { name => $file, filehash => 'added' };
1910 if ( scalar(@{$state->{args}}) == 1 )
1912 my $arg = $state->{args}[0];
1913 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
1915 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
1917 foreach my $file ( @gethead )
1919 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1920 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
1921 push @{$state->{args}}, $file->{name};
1924 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
1926 $log->info("Only one arg specified, populating file list automatically");
1928 $state->{args} = [];
1930 foreach my $file ( @gethead )
1932 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1933 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
1934 push @{$state->{args}}, $file->{name};
1939 # This method cleans up the $state variable after a command that uses arguments has run
1942 $state->{files} = [];
1943 $state->{args} = [];
1944 $state->{arguments} = [];
1945 $state->{entries} = {};
1950 my $filename = shift;
1952 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
1954 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
1955 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
1960 # This method takes a file hash and does a CVS "file transfer" which transmits the
1961 # size of the file, and then the file contents.
1962 # If a second argument $targetfile is given, the file is instead written out to
1963 # a file by the name of $targetfile
1966 my $filehash = shift;
1967 my $targetfile = shift;
1969 if ( defined ( $filehash ) and $filehash eq "deleted" )
1971 $log->warn("filehash is 'deleted'");
1975 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
1977 my $type = `git-cat-file -t $filehash`;
1980 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
1982 my $size = `git-cat-file -s $filehash`;
1985 $log->debug("transmitfile($filehash) size=$size, type=$type");
1987 if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
1989 if ( defined ( $targetfile ) )
1991 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
1992 print NEWFILE $_ while ( <$fh> );
1996 print while ( <$fh> );
1998 close $fh or die ("Couldn't close filehandle for transmitfile()");
2000 die("Couldn't execute git-cat-file");
2004 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2005 # refers to the directory portion and the file portion of the filename
2009 my $filename = shift;
2010 my $fixforlocaldir = shift;
2012 my ( $filepart, $dirpart ) = ( $filename, "." );
2013 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2016 if ( $fixforlocaldir )
2018 $dirpart =~ s/^$state->{prependdir}//;
2021 return ( $filepart, $dirpart );
2026 my $filename = shift;
2028 return undef unless(defined($filename));
2029 if ( $filename =~ /^\// )
2031 print "E absolute filenames '$filename' not supported by server\n";
2035 $filename =~ s/^\.\///g;
2036 $filename = $state->{prependdir} . $filename;
2040 # Given a path, this function returns a string containing the kopts
2041 # that should go into that path's Entries line. For example, a binary
2042 # file should get -kb.
2047 # Once it exists, the git attributes system should be used to look up
2048 # what attributes apply to this path.
2050 # Until then, take the setting from the config file
2051 unless ( defined ( $cfg->{gitcvs}{allbinary} ) and $cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i )
2053 # Return "" to give no special treatment to any path
2056 # Alternatively, to have all files treated as if they are binary (which
2057 # is more like git itself), always return the "-kb" option
2062 package GITCVS::log;
2065 #### Copyright The Open University UK - 2006.
2067 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2068 #### Martin Langhoff <martin@catalyst.net.nz>
2081 This module provides very crude logging with a similar interface to
2090 Creates a new log object, optionally you can specify a filename here to
2091 indicate the file to log to. If no log file is specified, you can specify one
2092 later with method setfile, or indicate you no longer want logging with method
2095 Until one of these methods is called, all log calls will buffer messages ready
2102 my $filename = shift;
2106 bless $self, $class;
2108 if ( defined ( $filename ) )
2110 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2118 This methods takes a filename, and attempts to open that file as the log file.
2119 If successful, all buffered data is written out to the file, and any further
2120 logging is written directly to the file.
2126 my $filename = shift;
2128 if ( defined ( $filename ) )
2130 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2133 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2135 while ( my $line = shift @{$self->{buffer}} )
2137 print {$self->{fh}} $line;
2143 This method indicates no logging is going to be used. It flushes any entries in
2144 the internal buffer, and sets a flag to ensure no further data is put there.
2153 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2155 $self->{buffer} = [];
2160 Internal method. Returns true if the log file is open, false otherwise.
2167 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2171 =head2 debug info warn fatal
2173 These four methods are wrappers to _log. They provide the actual interface for
2177 sub debug { my $self = shift; $self->_log("debug", @_); }
2178 sub info { my $self = shift; $self->_log("info" , @_); }
2179 sub warn { my $self = shift; $self->_log("warn" , @_); }
2180 sub fatal { my $self = shift; $self->_log("fatal", @_); }
2184 This is an internal method called by the logging functions. It generates a
2185 timestamp and pushes the logged line either to file, or internal buffer.
2193 return if ( $self->{nolog} );
2195 my @time = localtime;
2196 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2206 if ( $self->_logopen )
2208 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2210 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2216 This method simply closes the file handle if one is open
2223 if ( $self->_logopen )
2229 package GITCVS::updater;
2232 #### Copyright The Open University UK - 2006.
2234 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2235 #### Martin Langhoff <martin@catalyst.net.nz>
2257 die "Need to specify a git repository" unless ( defined($config) and -d $config );
2258 die "Need to specify a module" unless ( defined($module) );
2260 $class = ref($class) || $class;
2264 bless $self, $class;
2266 $self->{module} = $module;
2267 $self->{git_path} = $config . "/";
2269 $self->{log} = $log;
2271 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2273 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
2274 $cfg->{gitcvs}{dbdriver} || "SQLite";
2275 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2276 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2277 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2278 $cfg->{gitcvs}{dbuser} || "";
2279 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2280 $cfg->{gitcvs}{dbpass} || "";
2281 my %mapping = ( m => $module,
2282 a => $state->{method},
2283 u => getlogin || getpwuid($<) || $<,
2284 G => $self->{git_path},
2285 g => mangle_dirname($self->{git_path}),
2287 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2288 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
2290 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2291 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2292 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
2295 die "Error connecting to database\n" unless defined $self->{dbh};
2297 $self->{tables} = {};
2298 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
2300 $self->{tables}{$table} = 1;
2303 # Construct the revision table if required
2304 unless ( $self->{tables}{revision} )
2307 CREATE TABLE revision (
2309 revision INTEGER NOT NULL,
2310 filehash TEXT NOT NULL,
2311 commithash TEXT NOT NULL,
2312 author TEXT NOT NULL,
2313 modified TEXT NOT NULL,
2318 CREATE INDEX revision_ix1
2319 ON revision (name,revision)
2322 CREATE INDEX revision_ix2
2323 ON revision (name,commithash)
2327 # Construct the head table if required
2328 unless ( $self->{tables}{head} )
2333 revision INTEGER NOT NULL,
2334 filehash TEXT NOT NULL,
2335 commithash TEXT NOT NULL,
2336 author TEXT NOT NULL,
2337 modified TEXT NOT NULL,
2342 CREATE INDEX head_ix1
2347 # Construct the properties table if required
2348 unless ( $self->{tables}{properties} )
2351 CREATE TABLE properties (
2352 key TEXT NOT NULL PRIMARY KEY,
2358 # Construct the commitmsgs table if required
2359 unless ( $self->{tables}{commitmsgs} )
2362 CREATE TABLE commitmsgs (
2363 key TEXT NOT NULL PRIMARY KEY,
2379 # first lets get the commit list
2380 $ENV{GIT_DIR} = $self->{git_path};
2382 my $commitsha1 = `git rev-parse $self->{module}`;
2385 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
2386 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2388 die("Invalid module '$self->{module}'");
2393 my $lastcommit = $self->_get_prop("last_commit");
2395 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
2399 # Start exclusive lock here...
2400 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2402 # TODO: log processing is memory bound
2403 # if we can parse into a 2nd file that is in reverse order
2404 # we can probably do something really efficient
2405 my @git_log_params = ('--pretty', '--parents', '--topo-order');
2407 if (defined $lastcommit) {
2408 push @git_log_params, "$lastcommit..$self->{module}";
2410 push @git_log_params, $self->{module};
2412 # git-rev-list is the backend / plumbing version of git-log
2413 open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
2422 if (m/^commit\s+(.*)$/) {
2423 # on ^commit lines put the just seen commit in the stack
2424 # and prime things for the next one
2427 unshift @commits, \%copy;
2430 my @parents = split(m/\s+/, $1);
2431 $commit{hash} = shift @parents;
2432 $commit{parents} = \@parents;
2433 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2434 # on rfc822-like lines seen before we see any message,
2435 # lowercase the entry and put it in the hash as key-value
2436 $commit{lc($1)} = $2;
2438 # message lines - skip initial empty line
2439 # and trim whitespace
2440 if (!exists($commit{message}) && m/^\s*$/) {
2441 # define it to mark the end of headers
2442 $commit{message} = '';
2445 s/^\s+//; s/\s+$//; # trim ws
2446 $commit{message} .= $_ . "\n";
2451 unshift @commits, \%commit if ( keys %commit );
2453 # Now all the commits are in the @commits bucket
2454 # ordered by time DESC. for each commit that needs processing,
2455 # determine whether it's following the last head we've seen or if
2456 # it's on its own branch, grab a file list, and add whatever's changed
2457 # NOTE: $lastcommit refers to the last commit from previous run
2458 # $lastpicked is the last commit we picked in this run
2461 if (defined $lastcommit) {
2462 $lastpicked = $lastcommit;
2465 my $committotal = scalar(@commits);
2466 my $commitcount = 0;
2468 # Load the head table into $head (for cached lookups during the update process)
2469 foreach my $file ( @{$self->gethead()} )
2471 $head->{$file->{name}} = $file;
2474 foreach my $commit ( @commits )
2476 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2477 if (defined $lastpicked)
2479 if (!in_array($lastpicked, @{$commit->{parents}}))
2481 # skip, we'll see this delta
2482 # as part of a merge later
2483 # warn "skipping off-track $commit->{hash}\n";
2485 } elsif (@{$commit->{parents}} > 1) {
2486 # it is a merge commit, for each parent that is
2487 # not $lastpicked, see if we can get a log
2488 # from the merge-base to that parent to put it
2489 # in the message as a merge summary.
2490 my @parents = @{$commit->{parents}};
2491 foreach my $parent (@parents) {
2492 # git-merge-base can potentially (but rarely) throw
2493 # several candidate merge bases. let's assume
2494 # that the first one is the best one.
2495 if ($parent eq $lastpicked) {
2498 open my $p, 'git-merge-base '. $lastpicked . ' '
2500 my @output = (<$p>);
2502 my $base = join('', @output);
2506 # print "want to log between $base $parent \n";
2507 open(GITLOG, '-|', 'git-log', "$base..$parent")
2508 or die "Cannot call git-log: $!";
2512 if (!defined $mergedhash) {
2513 if (m/^commit\s+(.+)$/) {
2519 # grab the first line that looks non-rfc822
2520 # aka has content after leading space
2521 if (m/^\s+(\S.*)$/) {
2523 $title = substr($title,0,100); # truncate
2524 unshift @merged, "$mergedhash $title";
2531 $commit->{mergemsg} = $commit->{message};
2532 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
2533 foreach my $summary (@merged) {
2534 $commit->{mergemsg} .= "\t$summary\n";
2536 $commit->{mergemsg} .= "\n\n";
2537 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2544 # convert the date to CVS-happy format
2545 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2547 if ( defined ( $lastpicked ) )
2549 my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
2551 while ( <FILELIST> )
2554 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
2556 die("Couldn't process git-diff-tree line : $_");
2558 my ($mode, $hash, $change) = ($1, $2, $3);
2559 my $name = <FILELIST>;
2562 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
2565 $git_perms .= "r" if ( $mode & 4 );
2566 $git_perms .= "w" if ( $mode & 2 );
2567 $git_perms .= "x" if ( $mode & 1 );
2568 $git_perms = "rw" if ( $git_perms eq "" );
2570 if ( $change eq "D" )
2572 #$log->debug("DELETE $name");
2575 revision => $head->{$name}{revision} + 1,
2576 filehash => "deleted",
2577 commithash => $commit->{hash},
2578 modified => $commit->{date},
2579 author => $commit->{author},
2582 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2584 elsif ( $change eq "M" )
2586 #$log->debug("MODIFIED $name");
2589 revision => $head->{$name}{revision} + 1,
2591 commithash => $commit->{hash},
2592 modified => $commit->{date},
2593 author => $commit->{author},
2596 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2598 elsif ( $change eq "A" )
2600 #$log->debug("ADDED $name");
2603 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
2605 commithash => $commit->{hash},
2606 modified => $commit->{date},
2607 author => $commit->{author},
2610 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2614 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
2620 # this is used to detect files removed from the repo
2621 my $seen_files = {};
2623 my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
2625 while ( <FILELIST> )
2628 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
2630 die("Couldn't process git-ls-tree line : $_");
2633 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2635 $seen_files->{$git_filename} = 1;
2637 my ( $oldhash, $oldrevision, $oldmode ) = (
2638 $head->{$git_filename}{filehash},
2639 $head->{$git_filename}{revision},
2640 $head->{$git_filename}{mode}
2643 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2646 $git_perms .= "r" if ( $1 & 4 );
2647 $git_perms .= "w" if ( $1 & 2 );
2648 $git_perms .= "x" if ( $1 & 1 );
2653 # unless the file exists with the same hash, we need to update it ...
2654 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2656 my $newrevision = ( $oldrevision or 0 ) + 1;
2658 $head->{$git_filename} = {
2659 name => $git_filename,
2660 revision => $newrevision,
2661 filehash => $git_hash,
2662 commithash => $commit->{hash},
2663 modified => $commit->{date},
2664 author => $commit->{author},
2669 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2674 # Detect deleted files
2675 foreach my $file ( keys %$head )
2677 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
2679 $head->{$file}{revision}++;
2680 $head->{$file}{filehash} = "deleted";
2681 $head->{$file}{commithash} = $commit->{hash};
2682 $head->{$file}{modified} = $commit->{date};
2683 $head->{$file}{author} = $commit->{author};
2685 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
2688 # END : "Detect deleted files"
2692 if (exists $commit->{mergemsg})
2694 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
2697 $lastpicked = $commit->{hash};
2699 $self->_set_prop("last_commit", $commit->{hash});
2702 $self->delete_head();
2703 foreach my $file ( keys %$head )
2707 $head->{$file}{revision},
2708 $head->{$file}{filehash},
2709 $head->{$file}{commithash},
2710 $head->{$file}{modified},
2711 $head->{$file}{author},
2712 $head->{$file}{mode},
2715 # invalidate the gethead cache
2716 $self->{gethead_cache} = undef;
2719 # Ending exclusive lock here
2720 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
2727 my $revision = shift;
2728 my $filehash = shift;
2729 my $commithash = shift;
2730 my $modified = shift;
2734 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2735 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2744 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2745 $insert_mergelog->execute($key, $value);
2752 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
2753 $delete_head->execute();
2760 my $revision = shift;
2761 my $filehash = shift;
2762 my $commithash = shift;
2763 my $modified = shift;
2767 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2768 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2774 my $filename = shift;
2776 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2777 $db_query->execute($filename);
2778 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2780 return ( $hash, $revision, $mode );
2788 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2789 $db_query->execute($key);
2790 my ( $value ) = $db_query->fetchrow_array;
2801 my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2802 $db_query->execute($value, $key);
2804 unless ( $db_query->rows )
2806 $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2807 $db_query->execute($key, $value);
2821 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
2823 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
2824 $db_query->execute();
2827 while ( my $file = $db_query->fetchrow_hashref )
2832 $self->{gethead_cache} = $tree;
2844 my $filename = shift;
2846 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2847 $db_query->execute($filename);
2850 while ( my $file = $db_query->fetchrow_hashref )
2860 This function takes a filename (with path) argument and returns a hashref of
2861 metadata for that file.
2868 my $filename = shift;
2869 my $revision = shift;
2872 if ( defined($revision) and $revision =~ /^\d+$/ )
2874 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2875 $db_query->execute($filename, $revision);
2877 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2879 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2880 $db_query->execute($filename, $revision);
2882 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2883 $db_query->execute($filename);
2886 return $db_query->fetchrow_hashref;
2889 =head2 commitmessage
2891 this function takes a commithash and returns the commit message for that commit
2897 my $commithash = shift;
2899 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2902 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2903 $db_query->execute($commithash);
2905 my ( $message ) = $db_query->fetchrow_array;
2907 if ( defined ( $message ) )
2909 $message .= " " if ( $message =~ /\n$/ );
2913 my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
2914 shift @lines while ( $lines[0] =~ /\S/ );
2915 $message = join("",@lines);
2916 $message .= " " if ( $message =~ /\n$/ );
2922 This function takes a filename (with path) argument and returns an arrayofarrays
2923 containing revision,filehash,commithash ordered by revision descending
2929 my $filename = shift;
2932 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2933 $db_query->execute($filename);
2935 return $db_query->fetchall_arrayref;
2938 =head2 gethistorydense
2940 This function takes a filename (with path) argument and returns an arrayofarrays
2941 containing revision,filehash,commithash ordered by revision descending.
2943 This version of gethistory skips deleted entries -- so it is useful for annotate.
2944 The 'dense' part is a reference to a '--dense' option available for git-rev-list
2945 and other git tools that depend on it.
2951 my $filename = shift;
2954 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2955 $db_query->execute($filename);
2957 return $db_query->fetchall_arrayref;
2962 from Array::PAT - mimics the in_array() function
2963 found in PHP. Yuck but works for small arrays.
2968 my ($check, @array) = @_;
2970 foreach my $test (@array){
2971 if($check eq $test){
2978 =head2 safe_pipe_capture
2980 an alternative to `command` that allows input to be passed as an array
2981 to work around shell problems with weird characters in arguments
2984 sub safe_pipe_capture {
2988 if (my $pid = open my $child, '-|') {
2989 @output = (<$child>);
2990 close $child or die join(' ',@_).": $! $?";
2992 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
2994 return wantarray ? @output : join('',@output);
2997 =head2 mangle_dirname
2999 create a string from a directory name that is suitable to use as
3000 part of a filename, mainly by converting all chars except \w.- to _
3003 sub mangle_dirname {
3004 my $dirname = shift;
3005 return unless defined $dirname;
3007 $dirname =~ s/[^\w.-]/_/g;