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 # don't export the whole system unless the users requests it
134 if ($state->{'export-all'} && !@
{$state->{allowed_roots
}}) {
135 die "--export-all can only be used together with an explicit whitelist\n";
138 # if we are called with a pserver argument,
139 # deal with the authentication cat before entering the
141 if ($state->{method
} eq 'pserver') {
142 my $line = <STDIN
>; chomp $line;
143 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
144 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
147 $line = <STDIN
>; chomp $line;
148 req_Root
('root', $line) # reuse Root
149 or die "E Invalid root $line \n";
150 $line = <STDIN
>; chomp $line;
151 unless ($line eq 'anonymous') {
152 print "E Only anonymous user allowed via pserver\n";
153 print "I HATE YOU\n";
156 $line = <STDIN
>; chomp $line; # validate the password?
157 $line = <STDIN
>; chomp $line;
158 unless ($line eq "END $request REQUEST") {
159 die "E Do not understand $line -- expecting END $request REQUEST\n";
161 print "I LOVE YOU\n";
162 exit if $request eq 'VERIFICATION'; # cvs login
163 # and now back to our regular programme...
166 # Keep going until the client closes the connection
171 # Check to see if we've seen this method, and call appropriate function.
172 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
174 # use the $methods hash to call the appropriate sub for this command
175 #$log->info("Method : $1");
176 &{$methods->{$1}}($1,$2);
178 # log fatal because we don't understand this function. If this happens
179 # we're fairly screwed because we don't know if the client is expecting
180 # a response. If it is, the client will hang, we'll hang, and the whole
181 # thing will be custard.
182 $log->fatal("Don't understand command $_\n");
183 die("Unknown command $_");
187 $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
188 $log->info("--------------- FINISH -----------------");
190 # Magic catchall method.
191 # This is the method that will handle all commands we haven't yet
192 # implemented. It simply sends a warning to the log file indicating a
193 # command that hasn't been implemented has been invoked.
196 my ( $cmd, $data ) = @_;
197 $log->warn("Unhandled command : req_$cmd : $data");
202 # Response expected: no. Tell the server which CVSROOT to use. Note that
203 # pathname is a local directory and not a fully qualified CVSROOT variable.
204 # pathname must already exist; if creating a new root, use the init
205 # request, not Root. pathname does not include the hostname of the server,
206 # how to access the server, etc.; by the time the CVS protocol is in use,
207 # connection, authentication, etc., are already taken care of. The Root
208 # request must be sent only once, and it must be sent before any requests
209 # other than Valid-responses, valid-requests, UseUnchanged, Set or init.
212 my ( $cmd, $data ) = @_;
213 $log->debug("req_Root : $data");
215 unless ($data =~ m
#^/#) {
216 print "error 1 Root must be an absolute pathname\n";
220 my $cvsroot = $state->{'base-path'} || '';
224 if ($state->{CVSROOT
}
225 && ($state->{CVSROOT
} ne $cvsroot)) {
226 print "error 1 Conflicting roots specified\n";
230 $state->{CVSROOT
} = $cvsroot;
232 $ENV{GIT_DIR
} = $state->{CVSROOT
} . "/";
234 if (@
{$state->{allowed_roots
}}) {
236 foreach my $dir (@
{$state->{allowed_roots
}}) {
237 next unless $dir =~ m
#^/#;
239 if ($state->{'strict-paths'}) {
240 if ($ENV{GIT_DIR
} =~ m
#^\Q$dir\E/?$#) {
244 } elsif ($ENV{GIT_DIR
} =~ m
#^\Q$dir\E(/?$|/)#) {
251 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
253 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
258 unless (-d
$ENV{GIT_DIR
} && -e
$ENV{GIT_DIR
}.'HEAD') {
259 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
261 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
265 my @gitvars = `git-config -l`;
267 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
269 print "error 1 - problem executing git-config\n";
272 foreach my $line ( @gitvars )
274 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
278 $cfg->{$1}{$2}{$3} = $4;
282 my $enabled = ($cfg->{gitcvs
}{$state->{method
}}{enabled
}
283 || $cfg->{gitcvs
}{enabled
});
284 unless ($state->{'export-all'} ||
285 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
286 print "E GITCVS emulation needs to be enabled on this repo\n";
287 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
289 print "error 1 GITCVS emulation disabled\n";
293 my $logfile = $cfg->{gitcvs
}{$state->{method
}}{logfile
} || $cfg->{gitcvs
}{logfile
};
296 $log->setfile($logfile);
304 # Global_option option \n
305 # Response expected: no. Transmit one of the global options `-q', `-Q',
306 # `-l', `-t', `-r', or `-n'. option must be one of those strings, no
307 # variations (such as combining of options) are allowed. For graceful
308 # handling of valid-requests, it is probably better to make new global
309 # options separate requests, rather than trying to add them to this
313 my ( $cmd, $data ) = @_;
314 $log->debug("req_Globaloption : $data");
315 $state->{globaloptions
}{$data} = 1;
318 # Valid-responses request-list \n
319 # Response expected: no. Tell the server what responses the client will
320 # accept. request-list is a space separated list of tokens.
321 sub req_Validresponses
323 my ( $cmd, $data ) = @_;
324 $log->debug("req_Validresponses : $data");
326 # TODO : re-enable this, currently it's not particularly useful
327 #$state->{validresponses} = [ split /\s+/, $data ];
331 # Response expected: yes. Ask the server to send back a Valid-requests
333 sub req_validrequests
335 my ( $cmd, $data ) = @_;
337 $log->debug("req_validrequests");
339 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
340 $log->debug("SEND : ok");
342 print "Valid-requests " . join(" ",keys %$methods) . "\n";
346 # Directory local-directory \n
347 # Additional data: repository \n. Response expected: no. Tell the server
348 # what directory to use. The repository should be a directory name from a
349 # previous server response. Note that this both gives a default for Entry
350 # and Modified and also for ci and the other commands; normal usage is to
351 # send Directory for each directory in which there will be an Entry or
352 # Modified, and then a final Directory for the original directory, then the
353 # command. The local-directory is relative to the top level at which the
354 # command is occurring (i.e. the last Directory which is sent before the
355 # command); to indicate that top level, `.' should be sent for
359 my ( $cmd, $data ) = @_;
361 my $repository = <STDIN
>;
365 $state->{localdir
} = $data;
366 $state->{repository
} = $repository;
367 $state->{path
} = $repository;
368 $state->{path
} =~ s/^$state->{CVSROOT}\///;
369 $state->{module
} = $1 if ($state->{path
} =~ s/^(.*?)(\/|$)//);
370 $state->{path
} .= "/" if ( $state->{path
} =~ /\S
/ );
372 $state->{directory
} = $state->{localdir
};
373 $state->{directory
} = "" if ( $state->{directory
} eq "." );
374 $state->{directory
} .= "/" if ( $state->{directory
} =~ /\S
/ );
376 if ( (not defined($state->{prependdir
}) or $state->{prependdir
} eq '') and $state->{localdir
} eq "." and $state->{path
} =~ /\S/ )
378 $log->info("Setting prepend to '$state->{path}'");
379 $state->{prependdir
} = $state->{path
};
380 foreach my $entry ( keys %{$state->{entries
}} )
382 $state->{entries
}{$state->{prependdir
} . $entry} = $state->{entries
}{$entry};
383 delete $state->{entries
}{$entry};
387 if ( defined ( $state->{prependdir
} ) )
389 $log->debug("Prepending '$state->{prependdir}' to state|directory");
390 $state->{directory
} = $state->{prependdir
} . $state->{directory
}
392 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
395 # Entry entry-line \n
396 # Response expected: no. Tell the server what version of a file is on the
397 # local machine. The name in entry-line is a name relative to the directory
398 # most recently specified with Directory. If the user is operating on only
399 # some files in a directory, Entry requests for only those files need be
400 # included. If an Entry request is sent without Modified, Is-modified, or
401 # Unchanged, it means the file is lost (does not exist in the working
402 # directory). If both Entry and one of Modified, Is-modified, or Unchanged
403 # are sent for the same file, Entry must be sent first. For a given file,
404 # one can send Modified, Is-modified, or Unchanged, but not more than one
408 my ( $cmd, $data ) = @_;
410 #$log->debug("req_Entry : $data");
412 my @data = split(/\//, $data);
414 $state->{entries
}{$state->{directory
}.$data[1]} = {
415 revision
=> $data[2],
416 conflict
=> $data[3],
418 tag_or_date
=> $data[5],
421 $log->info("Received entry line '$data' => '" . $state->{directory
} . $data[1] . "'");
424 # Questionable filename \n
425 # Response expected: no. Additional data: no. Tell the server to check
426 # whether filename should be ignored, and if not, next time the server
427 # sends responses, send (in a M response) `?' followed by the directory and
428 # filename. filename must not contain `/'; it needs to be a file in the
429 # directory named by the most recent Directory request.
432 my ( $cmd, $data ) = @_;
434 $log->debug("req_Questionable : $data");
435 $state->{entries
}{$state->{directory
}.$data}{questionable
} = 1;
439 # Response expected: yes. Add a file or directory. This uses any previous
440 # Argument, Directory, Entry, or Modified requests, if they have been sent.
441 # The last Directory sent specifies the working directory at the time of
442 # the operation. To add a directory, send the directory to be added using
443 # Directory and Argument requests.
446 my ( $cmd, $data ) = @_;
450 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
453 argsfromdir
($updater);
457 foreach my $filename ( @
{$state->{args
}} )
459 $filename = filecleanup
($filename);
461 my $meta = $updater->getmeta($filename);
462 my $wrev = revparse
($filename);
464 if ($wrev && $meta && ($wrev < 0))
466 # previously removed file, add back
467 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
469 print "MT +updated\n";
470 print "MT text U \n";
471 print "MT fname $filename\n";
472 print "MT newline\n";
473 print "MT -updated\n";
475 unless ( $state->{globaloptions
}{-n
} )
477 my ( $filepart, $dirpart ) = filenamesplit
($filename,1);
479 print "Created $dirpart\n";
480 print $state->{CVSROOT
} . "/$state->{module}/$filename\n";
482 # this is an "entries" line
483 my $kopts = kopts_from_path
($filepart);
484 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
485 print "/$filepart/1.$meta->{revision}//$kopts/\n";
487 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
488 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
490 transmitfile
($meta->{filehash
});
496 unless ( defined ( $state->{entries
}{$filename}{modified_filename
} ) )
498 print "E cvs add: nothing known about `$filename'\n";
501 # TODO : check we're not squashing an already existing file
502 if ( defined ( $state->{entries
}{$filename}{revision
} ) )
504 print "E cvs add: `$filename' has already been entered\n";
508 my ( $filepart, $dirpart ) = filenamesplit
($filename, 1);
510 print "E cvs add: scheduling file `$filename' for addition\n";
512 print "Checked-in $dirpart\n";
514 my $kopts = kopts_from_path
($filepart);
515 print "/$filepart/0//$kopts/\n";
520 if ( $addcount == 1 )
522 print "E cvs add: use `cvs commit' to add this file permanently\n";
524 elsif ( $addcount > 1 )
526 print "E cvs add: use `cvs commit' to add these files permanently\n";
533 # Response expected: yes. Remove a file. This uses any previous Argument,
534 # Directory, Entry, or Modified requests, if they have been sent. The last
535 # Directory sent specifies the working directory at the time of the
536 # operation. Note that this request does not actually do anything to the
537 # repository; the only effect of a successful remove request is to supply
538 # the client with a new entries line containing `-' to indicate a removed
539 # file. In fact, the client probably could perform this operation without
540 # contacting the server, although using remove may cause the server to
541 # perform a few more checks. The client sends a subsequent ci request to
542 # actually record the removal in the repository.
545 my ( $cmd, $data ) = @_;
549 # Grab a handle to the SQLite db and do any necessary updates
550 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
553 #$log->debug("add state : " . Dumper($state));
557 foreach my $filename ( @
{$state->{args
}} )
559 $filename = filecleanup
($filename);
561 if ( defined ( $state->{entries
}{$filename}{unchanged
} ) or defined ( $state->{entries
}{$filename}{modified_filename
} ) )
563 print "E cvs remove: file `$filename' still in working directory\n";
567 my $meta = $updater->getmeta($filename);
568 my $wrev = revparse
($filename);
570 unless ( defined ( $wrev ) )
572 print "E cvs remove: nothing known about `$filename'\n";
576 if ( defined($wrev) and $wrev < 0 )
578 print "E cvs remove: file `$filename' already scheduled for removal\n";
582 unless ( $wrev == $meta->{revision
} )
584 # TODO : not sure if the format of this message is quite correct.
585 print "E cvs remove: Up to date check failed for `$filename'\n";
590 my ( $filepart, $dirpart ) = filenamesplit
($filename, 1);
592 print "E cvs remove: scheduling `$filename' for removal\n";
594 print "Checked-in $dirpart\n";
596 my $kopts = kopts_from_path
($filepart);
597 print "/$filepart/-1.$wrev//$kopts/\n";
604 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
606 elsif ( $rmcount > 1 )
608 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
614 # Modified filename \n
615 # Response expected: no. Additional data: mode, \n, file transmission. Send
616 # the server a copy of one locally modified file. filename is a file within
617 # the most recent directory sent with Directory; it must not contain `/'.
618 # If the user is operating on only some files in a directory, only those
619 # files need to be included. This can also be sent without Entry, if there
620 # is no entry for the file.
623 my ( $cmd, $data ) = @_;
630 # Grab config information
631 my $blocksize = 8192;
632 my $bytesleft = $size;
635 # Get a filehandle/name to write it to
636 my ( $fh, $filename ) = tempfile
( DIR
=> $TEMP_DIR );
638 # Loop over file data writing out to temporary file.
641 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
642 read STDIN
, $tmp, $blocksize;
644 $bytesleft -= $blocksize;
649 # Ensure we have something sensible for the file mode
650 if ( $mode =~ /u=(\w+)/ )
657 # Save the file data in $state
658 $state->{entries
}{$state->{directory
}.$data}{modified_filename
} = $filename;
659 $state->{entries
}{$state->{directory
}.$data}{modified_mode
} = $mode;
660 $state->{entries
}{$state->{directory
}.$data}{modified_hash
} = `git-hash-object $filename`;
661 $state->{entries
}{$state->{directory
}.$data}{modified_hash
} =~ s/\s.*$//s;
663 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
666 # Unchanged filename \n
667 # Response expected: no. Tell the server that filename has not been
668 # modified in the checked out directory. The filename is a file within the
669 # most recent directory sent with Directory; it must not contain `/'.
672 my ( $cmd, $data ) = @_;
674 $state->{entries
}{$state->{directory
}.$data}{unchanged
} = 1;
676 #$log->debug("req_Unchanged : $data");
680 # Response expected: no. Save argument for use in a subsequent command.
681 # Arguments accumulate until an argument-using command is given, at which
682 # point they are forgotten.
684 # Response expected: no. Append \n followed by text to the current argument
688 my ( $cmd, $data ) = @_;
690 # Argumentx means: append to last Argument (with a newline in front)
692 $log->debug("$cmd : $data");
694 if ( $cmd eq 'Argumentx') {
695 ${$state->{arguments
}}[$#{$state->{arguments}}] .= "\n" . $data;
697 push @
{$state->{arguments
}}, $data;
702 # Response expected: yes. Expand the modules which are specified in the
703 # arguments. Returns the data in Module-expansion responses. Note that the
704 # server can assume that this is checkout or export, not rtag or rdiff; the
705 # latter do not access the working directory and thus have no need to
706 # expand modules on the client side. Expand may not be the best word for
707 # what this request does. It does not necessarily tell you all the files
708 # contained in a module, for example. Basically it is a way of telling you
709 # which working directories the server needs to know about in order to
710 # handle a checkout of the specified modules. For example, suppose that the
711 # server has a module defined by
712 # aliasmodule -a 1dir
713 # That is, one can check out aliasmodule and it will take 1dir in the
714 # repository and check it out to 1dir in the working directory. Now suppose
715 # the client already has this module checked out and is planning on using
716 # the co request to update it. Without using expand-modules, the client
717 # would have two bad choices: it could either send information about all
718 # working directories under the current directory, which could be
719 # unnecessarily slow, or it could be ignorant of the fact that aliasmodule
720 # stands for 1dir, and neglect to send information for 1dir, which would
721 # lead to incorrect operation. With expand-modules, the client would first
722 # ask for the module to be expanded:
723 sub req_expandmodules
725 my ( $cmd, $data ) = @_;
729 $log->debug("req_expandmodules : " . ( defined($data) ?
$data : "[NULL]" ) );
731 unless ( ref $state->{arguments
} eq "ARRAY" )
737 foreach my $module ( @
{$state->{arguments
}} )
739 $log->debug("SEND : Module-expansion $module");
740 print "Module-expansion $module\n";
748 # Response expected: yes. Get files from the repository. This uses any
749 # previous Argument, Directory, Entry, or Modified requests, if they have
750 # been sent. Arguments to this command are module names; the client cannot
751 # know what directories they correspond to except by (1) just sending the
752 # co request, and then seeing what directory names the server sends back in
753 # its responses, and (2) the expand-modules request.
756 my ( $cmd, $data ) = @_;
760 my $module = $state->{args
}[0];
761 my $checkout_path = $module;
763 # use the user specified directory if we're given it
764 $checkout_path = $state->{opt
}{d
} if ( exists ( $state->{opt
}{d
} ) );
766 $log->debug("req_co : " . ( defined($data) ?
$data : "[NULL]" ) );
768 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
770 $ENV{GIT_DIR
} = $state->{CVSROOT
} . "/";
772 # Grab a handle to the SQLite db and do any necessary updates
773 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $module, $log);
776 $checkout_path =~ s
|/$||; # get rid of trailing slashes
778 # Eclipse seems to need the Clear-sticky command
779 # to prepare the 'Entries' file for the new directory.
780 print "Clear-sticky $checkout_path/\n";
781 print $state->{CVSROOT
} . "/$module/\n";
782 print "Clear-static-directory $checkout_path/\n";
783 print $state->{CVSROOT
} . "/$module/\n";
784 print "Clear-sticky $checkout_path/\n"; # yes, twice
785 print $state->{CVSROOT
} . "/$module/\n";
786 print "Template $checkout_path/\n";
787 print $state->{CVSROOT
} . "/$module/\n";
790 # instruct the client that we're checking out to $checkout_path
791 print "E cvs checkout: Updating $checkout_path\n";
798 my ($dir, $repodir, $remotedir, $seendirs) = @_;
799 my $parent = dirname
($dir);
802 $remotedir =~ s
|/+$||;
804 $log->debug("announcedir $dir, $repodir, $remotedir" );
806 if ($parent eq '.' || $parent eq './') {
809 # recurse to announce unseen parents first
810 if (length($parent) && !exists($seendirs->{$parent})) {
811 prepdir
($parent, $repodir, $remotedir, $seendirs);
813 # Announce that we are going to modify at the parent level
815 print "E cvs checkout: Updating $remotedir/$parent\n";
817 print "E cvs checkout: Updating $remotedir\n";
819 print "Clear-sticky $remotedir/$parent/\n";
820 print "$repodir/$parent/\n";
822 print "Clear-static-directory $remotedir/$dir/\n";
823 print "$repodir/$dir/\n";
824 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
825 print "$repodir/$parent/\n";
826 print "Template $remotedir/$dir/\n";
827 print "$repodir/$dir/\n";
830 $seendirs->{$dir} = 1;
833 foreach my $git ( @
{$updater->gethead} )
835 # Don't want to check out deleted files
836 next if ( $git->{filehash
} eq "deleted" );
838 ( $git->{name
}, $git->{dir
} ) = filenamesplit
($git->{name
});
840 if (length($git->{dir
}) && $git->{dir
} ne './'
841 && $git->{dir
} ne $lastdir ) {
842 unless (exists($seendirs{$git->{dir
}})) {
843 prepdir
($git->{dir
}, $state->{CVSROOT
} . "/$module/",
844 $checkout_path, \
%seendirs);
845 $lastdir = $git->{dir
};
846 $seendirs{$git->{dir
}} = 1;
848 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
851 # modification time of this file
852 print "Mod-time $git->{modified}\n";
854 # print some information to the client
855 if ( defined ( $git->{dir
} ) and $git->{dir
} ne "./" )
857 print "M U $checkout_path/$git->{dir}$git->{name}\n";
859 print "M U $checkout_path/$git->{name}\n";
862 # instruct client we're sending a file to put in this path
863 print "Created $checkout_path/" . ( defined ( $git->{dir
} ) and $git->{dir
} ne "./" ?
$git->{dir
} . "/" : "" ) . "\n";
865 print $state->{CVSROOT
} . "/$module/" . ( defined ( $git->{dir
} ) and $git->{dir
} ne "./" ?
$git->{dir
} . "/" : "" ) . "$git->{name}\n";
867 # this is an "entries" line
868 my $kopts = kopts_from_path
($git->{name
});
869 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
871 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
874 transmitfile
($git->{filehash
});
883 # Response expected: yes. Actually do a cvs update command. This uses any
884 # previous Argument, Directory, Entry, or Modified requests, if they have
885 # been sent. The last Directory sent specifies the working directory at the
886 # time of the operation. The -I option is not used--files which the client
887 # can decide whether to ignore are not mentioned and the client sends the
888 # Questionable request for others.
891 my ( $cmd, $data ) = @_;
893 $log->debug("req_update : " . ( defined($data) ?
$data : "[NULL]" ));
898 # It may just be a client exploring the available heads/modules
899 # in that case, list them as top level directories and leave it
900 # at that. Eclipse uses this technique to offer you a list of
901 # projects (heads in this case) to checkout.
903 if ($state->{module
} eq '') {
904 print "E cvs update: Updating .\n";
905 opendir HEADS
, $state->{CVSROOT
} . '/refs/heads';
906 while (my $head = readdir(HEADS
)) {
907 if (-f
$state->{CVSROOT
} . '/refs/heads/' . $head) {
908 print "E cvs update: New directory `$head'\n";
917 # Grab a handle to the SQLite db and do any necessary updates
918 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
922 argsfromdir
($updater);
924 #$log->debug("update state : " . Dumper($state));
926 # foreach file specified on the command line ...
927 foreach my $filename ( @
{$state->{args
}} )
929 $filename = filecleanup
($filename);
931 $log->debug("Processing file $filename");
933 # if we have a -C we should pretend we never saw modified stuff
934 if ( exists ( $state->{opt
}{C
} ) )
936 delete $state->{entries
}{$filename}{modified_hash
};
937 delete $state->{entries
}{$filename}{modified_filename
};
938 $state->{entries
}{$filename}{unchanged
} = 1;
942 if ( defined($state->{opt
}{r
}) and $state->{opt
}{r
} =~ /^1\.(\d+)/ )
944 $meta = $updater->getmeta($filename, $1);
946 $meta = $updater->getmeta($filename);
949 if ( ! defined $meta )
960 my $wrev = revparse
($filename);
962 # If the working copy is an old revision, lets get that version too for comparison.
963 if ( defined($wrev) and $wrev != $meta->{revision
} )
965 $oldmeta = $updater->getmeta($filename, $wrev);
968 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
970 # Files are up to date if the working copy and repo copy have the same revision,
971 # and the working copy is unmodified _and_ the user hasn't specified -C
972 next if ( defined ( $wrev )
973 and defined($meta->{revision
})
974 and $wrev == $meta->{revision
}
975 and $state->{entries
}{$filename}{unchanged
}
976 and not exists ( $state->{opt
}{C
} ) );
978 # If the working copy and repo copy have the same revision,
979 # but the working copy is modified, tell the client it's modified
980 if ( defined ( $wrev )
981 and defined($meta->{revision
})
982 and $wrev == $meta->{revision
}
983 and defined($state->{entries
}{$filename}{modified_hash
})
984 and not exists ( $state->{opt
}{C
} ) )
986 $log->info("Tell the client the file is modified");
987 print "MT text M \n";
988 print "MT fname $filename\n";
989 print "MT newline\n";
993 if ( $meta->{filehash
} eq "deleted" )
995 my ( $filepart, $dirpart ) = filenamesplit
($filename,1);
997 $log->info("Removing '$filename' from working copy (no longer in the repo)");
999 print "E cvs update: `$filename' is no longer in the repository\n";
1000 # Don't want to actually _DO_ the update if -n specified
1001 unless ( $state->{globaloptions
}{-n
} ) {
1002 print "Removed $dirpart\n";
1003 print "$filepart\n";
1006 elsif ( not defined ( $state->{entries
}{$filename}{modified_hash
} )
1007 or $state->{entries
}{$filename}{modified_hash
} eq $oldmeta->{filehash
}
1008 or $meta->{filehash
} eq 'added' )
1010 # normal update, just send the new revision (either U=Update,
1011 # or A=Add, or R=Remove)
1012 if ( defined($wrev) && $wrev < 0 )
1014 $log->info("Tell the client the file is scheduled for removal");
1015 print "MT text R \n";
1016 print "MT fname $filename\n";
1017 print "MT newline\n";
1020 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision
}) || $meta->{revision
} == 0) )
1022 $log->info("Tell the client the file is scheduled for addition");
1023 print "MT text A \n";
1024 print "MT fname $filename\n";
1025 print "MT newline\n";
1030 $log->info("Updating '$filename' to ".$meta->{revision
});
1031 print "MT +updated\n";
1032 print "MT text U \n";
1033 print "MT fname $filename\n";
1034 print "MT newline\n";
1035 print "MT -updated\n";
1038 my ( $filepart, $dirpart ) = filenamesplit
($filename,1);
1040 # Don't want to actually _DO_ the update if -n specified
1041 unless ( $state->{globaloptions
}{-n
} )
1043 if ( defined ( $wrev ) )
1045 # instruct client we're sending a file to put in this path as a replacement
1046 print "Update-existing $dirpart\n";
1047 $log->debug("Updating existing file 'Update-existing $dirpart'");
1049 # instruct client we're sending a file to put in this path as a new file
1050 print "Clear-static-directory $dirpart\n";
1051 print $state->{CVSROOT
} . "/$state->{module}/$dirpart\n";
1052 print "Clear-sticky $dirpart\n";
1053 print $state->{CVSROOT
} . "/$state->{module}/$dirpart\n";
1055 $log->debug("Creating new file 'Created $dirpart'");
1056 print "Created $dirpart\n";
1058 print $state->{CVSROOT
} . "/$state->{module}/$filename\n";
1060 # this is an "entries" line
1061 my $kopts = kopts_from_path
($filepart);
1062 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1063 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1066 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1067 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1070 transmitfile
($meta->{filehash
});
1073 $log->info("Updating '$filename'");
1074 my ( $filepart, $dirpart ) = filenamesplit
($meta->{name
},1);
1076 my $dir = tempdir
( DIR
=> $TEMP_DIR, CLEANUP
=> 1 ) . "/";
1079 my $file_local = $filepart . ".mine";
1080 system("ln","-s",$state->{entries
}{$filename}{modified_filename
}, $file_local);
1081 my $file_old = $filepart . "." . $oldmeta->{revision
};
1082 transmitfile
($oldmeta->{filehash
}, $file_old);
1083 my $file_new = $filepart . "." . $meta->{revision
};
1084 transmitfile
($meta->{filehash
}, $file_new);
1086 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1087 $log->info("Merging $file_local, $file_old, $file_new");
1088 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
1090 $log->debug("Temporary directory for merge is $dir");
1092 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1097 $log->info("Merged successfully");
1098 print "M M $filename\n";
1099 $log->debug("Merged $dirpart");
1101 # Don't want to actually _DO_ the update if -n specified
1102 unless ( $state->{globaloptions
}{-n
} )
1104 print "Merged $dirpart\n";
1105 $log->debug($state->{CVSROOT
} . "/$state->{module}/$filename");
1106 print $state->{CVSROOT
} . "/$state->{module}/$filename\n";
1107 my $kopts = kopts_from_path
($filepart);
1108 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1109 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1112 elsif ( $return == 1 )
1114 $log->info("Merged with conflicts");
1115 print "E cvs update: conflicts found in $filename\n";
1116 print "M C $filename\n";
1118 # Don't want to actually _DO_ the update if -n specified
1119 unless ( $state->{globaloptions
}{-n
} )
1121 print "Merged $dirpart\n";
1122 print $state->{CVSROOT
} . "/$state->{module}/$filename\n";
1123 my $kopts = kopts_from_path
($filepart);
1124 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
1129 $log->warn("Merge failed");
1133 # Don't want to actually _DO_ the update if -n specified
1134 unless ( $state->{globaloptions
}{-n
} )
1137 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1138 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1140 # transmit file, format is single integer on a line by itself (file
1141 # size) followed by the file contents
1142 # TODO : we should copy files in blocks
1143 my $data = `cat $file_local`;
1144 $log->debug("File size : " . length($data));
1145 print length($data) . "\n";
1159 my ( $cmd, $data ) = @_;
1163 #$log->debug("State : " . Dumper($state));
1165 $log->info("req_ci : " . ( defined($data) ?
$data : "[NULL]" ));
1167 if ( $state->{method
} eq 'pserver')
1169 print "error 1 pserver access cannot commit\n";
1173 if ( -e
$state->{CVSROOT
} . "/index" )
1175 $log->warn("file 'index' already exists in the git repository");
1176 print "error 1 Index already exists in git repo\n";
1180 # Grab a handle to the SQLite db and do any necessary updates
1181 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
1184 my $tmpdir = tempdir
( DIR
=> $TEMP_DIR );
1185 my ( undef, $file_index ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1186 $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");
1188 $ENV{GIT_DIR
} = $state->{CVSROOT
} . "/";
1189 $ENV{GIT_INDEX_FILE
} = $file_index;
1191 # Remember where the head was at the beginning.
1192 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1194 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1195 print "error 1 pserver cannot find the current HEAD of module";
1201 # populate the temporary index based
1202 system("git-read-tree", $parenthash);
1205 die "Error running git-read-tree $state->{module} $file_index $!";
1207 $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
1209 my @committedfiles = ();
1212 # foreach file specified on the command line ...
1213 foreach my $filename ( @
{$state->{args
}} )
1215 my $committedfile = $filename;
1216 $filename = filecleanup
($filename);
1218 next unless ( exists $state->{entries
}{$filename}{modified_filename
} or not $state->{entries
}{$filename}{unchanged
} );
1220 my $meta = $updater->getmeta($filename);
1221 $oldmeta{$filename} = $meta;
1223 my $wrev = revparse
($filename);
1225 my ( $filepart, $dirpart ) = filenamesplit
($filename);
1227 # do a checkout of the file if it part of this tree
1229 system('git-checkout-index', '-f', '-u', $filename);
1231 die "Error running git-checkout-index -f -u $filename : $!";
1237 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1238 $addflag = 1 unless ( -e
$filename );
1240 # Do up to date checking
1241 unless ( $addflag or $wrev == $meta->{revision
} or ( $rmflag and -$wrev == $meta->{revision
} ) )
1243 # fail everything if an up to date check fails
1244 print "error 1 Up to date check failed for $filename\n";
1249 push @committedfiles, $committedfile;
1250 $log->info("Committing $filename");
1252 system("mkdir","-p",$dirpart) unless ( -d
$dirpart );
1256 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1257 rename $state->{entries
}{$filename}{modified_filename
},$filename;
1259 # Calculate modes to remove
1261 foreach ( qw
(r w x
) ) { $invmode .= $_ unless ( $state->{entries
}{$filename}{modified_mode
} =~ /$_/ ); }
1263 $log->debug("chmod u+" . $state->{entries
}{$filename}{modified_mode
} . "-" . $invmode . " $filename");
1264 system("chmod","u+" . $state->{entries
}{$filename}{modified_mode
} . "-" . $invmode, $filename);
1269 $log->info("Removing file '$filename'");
1271 system("git-update-index", "--remove", $filename);
1275 $log->info("Adding file '$filename'");
1276 system("git-update-index", "--add", $filename);
1278 $log->info("Updating file '$filename'");
1279 system("git-update-index", $filename);
1283 unless ( scalar(@committedfiles) > 0 )
1285 print "E No files to commit\n";
1291 my $treehash = `git-write-tree`;
1294 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1296 # write our commit message out if we have one ...
1297 my ( $msg_fh, $msg_filename ) = tempfile
( DIR
=> $TEMP_DIR );
1298 print $msg_fh $state->{opt
}{m
};# if ( exists ( $state->{opt}{m} ) );
1299 print $msg_fh "\n\nvia git-CVS emulator\n";
1302 my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1304 $log->info("Commit hash : $commithash");
1306 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1308 $log->warn("Commit failed (Invalid commit hash)");
1309 print "error 1 Commit failed (unknown reason)\n";
1314 # Check that this is allowed, just as we would with a receive-pack
1315 my @cmd = ( $ENV{GIT_DIR
}.'hooks/update', "refs/heads/$state->{module}",
1316 $parenthash, $commithash );
1318 unless( system( @cmd ) == 0 )
1320 $log->warn("Commit failed (update hook declined to update ref)");
1321 print "error 1 Commit failed (update hook declined)\n";
1327 if (system(qw(git update-ref -m), "cvsserver ci",
1328 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1329 $log->warn("update-ref for $state->{module} failed.");
1330 print "error 1 Cannot commit -- update first\n";
1336 # foreach file specified on the command line ...
1337 foreach my $filename ( @committedfiles )
1339 $filename = filecleanup
($filename);
1341 my $meta = $updater->getmeta($filename);
1342 unless (defined $meta->{revision
}) {
1343 $meta->{revision
} = 1;
1346 my ( $filepart, $dirpart ) = filenamesplit
($filename, 1);
1348 $log->debug("Checked-in $dirpart : $filename");
1350 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
1351 if ( defined $meta->{filehash
} && $meta->{filehash
} eq "deleted" )
1353 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
1354 print "Remove-entry $dirpart\n";
1355 print "$filename\n";
1357 if ($meta->{revision
} == 1) {
1358 print "M initial revision: 1.1\n";
1360 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
1362 print "Checked-in $dirpart\n";
1363 print "$filename\n";
1364 my $kopts = kopts_from_path
($filepart);
1365 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1375 my ( $cmd, $data ) = @_;
1379 $log->info("req_status : " . ( defined($data) ?
$data : "[NULL]" ));
1380 #$log->debug("status state : " . Dumper($state));
1382 # Grab a handle to the SQLite db and do any necessary updates
1383 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
1386 # if no files were specified, we need to work out what files we should be providing status on ...
1387 argsfromdir
($updater);
1389 # foreach file specified on the command line ...
1390 foreach my $filename ( @
{$state->{args
}} )
1392 $filename = filecleanup
($filename);
1394 my $meta = $updater->getmeta($filename);
1395 my $oldmeta = $meta;
1397 my $wrev = revparse
($filename);
1399 # If the working copy is an old revision, lets get that version too for comparison.
1400 if ( defined($wrev) and $wrev != $meta->{revision
} )
1402 $oldmeta = $updater->getmeta($filename, $wrev);
1405 # TODO : All possible statuses aren't yet implemented
1407 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1408 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision
}) and $wrev == $meta->{revision
}
1410 ( ( $state->{entries
}{$filename}{unchanged
} and ( not defined ( $state->{entries
}{$filename}{conflict
} ) or $state->{entries
}{$filename}{conflict
} !~ /^\+=/ ) )
1411 or ( defined($state->{entries
}{$filename}{modified_hash
}) and $state->{entries
}{$filename}{modified_hash
} eq $meta->{filehash
} ) )
1414 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1415 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision
} ) and $meta->{revision
} > $wrev
1417 ( $state->{entries
}{$filename}{unchanged
}
1418 or ( defined($state->{entries
}{$filename}{modified_hash
}) and $state->{entries
}{$filename}{modified_hash
} eq $oldmeta->{filehash
} ) )
1421 # Need checkout if it exists in the repo but doesn't have a working copy
1422 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision
} ) );
1424 # Locally modified if working copy and repo copy have the same revision but there are local changes
1425 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision
}) and $wrev == $meta->{revision
} and $state->{entries
}{$filename}{modified_filename
} );
1427 # Needs Merge if working copy revision is less than repo copy and there are local changes
1428 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision
} ) and $meta->{revision
} > $wrev and $state->{entries
}{$filename}{modified_filename
} );
1430 $status ||= "Locally Added" if ( defined ( $state->{entries
}{$filename}{revision
} ) and not defined ( $meta->{revision
} ) );
1431 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision
} ) and -$wrev == $meta->{revision
} );
1432 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries
}{$filename}{conflict
} ) and $state->{entries
}{$filename}{conflict
} =~ /^\+=/ );
1433 $status ||= "File had conflicts on merge" if ( 0 );
1435 $status ||= "Unknown";
1437 print "M ===================================================================\n";
1438 print "M File: $filename\tStatus: $status\n";
1439 if ( defined($state->{entries
}{$filename}{revision
}) )
1441 print "M Working revision:\t" . $state->{entries
}{$filename}{revision
} . "\n";
1443 print "M Working revision:\tNo entry for $filename\n";
1445 if ( defined($meta->{revision
}) )
1447 print "M Repository revision:\t1." . $meta->{revision
} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1448 print "M Sticky Tag:\t\t(none)\n";
1449 print "M Sticky Date:\t\t(none)\n";
1450 print "M Sticky Options:\t\t(none)\n";
1452 print "M Repository revision:\tNo revision control file\n";
1462 my ( $cmd, $data ) = @_;
1466 $log->debug("req_diff : " . ( defined($data) ?
$data : "[NULL]" ));
1467 #$log->debug("status state : " . Dumper($state));
1469 my ($revision1, $revision2);
1470 if ( defined ( $state->{opt
}{r
} ) and ref $state->{opt
}{r
} eq "ARRAY" )
1472 $revision1 = $state->{opt
}{r
}[0];
1473 $revision2 = $state->{opt
}{r
}[1];
1475 $revision1 = $state->{opt
}{r
};
1478 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1479 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1481 $log->debug("Diffing revisions " . ( defined($revision1) ?
$revision1 : "[NULL]" ) . " and " . ( defined($revision2) ?
$revision2 : "[NULL]" ) );
1483 # Grab a handle to the SQLite db and do any necessary updates
1484 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
1487 # if no files were specified, we need to work out what files we should be providing status on ...
1488 argsfromdir
($updater);
1490 # foreach file specified on the command line ...
1491 foreach my $filename ( @
{$state->{args
}} )
1493 $filename = filecleanup
($filename);
1495 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1497 my $wrev = revparse
($filename);
1499 # We need _something_ to diff against
1500 next unless ( defined ( $wrev ) );
1502 # if we have a -r switch, use it
1503 if ( defined ( $revision1 ) )
1505 ( undef, $file1 ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1506 $meta1 = $updater->getmeta($filename, $revision1);
1507 unless ( defined ( $meta1 ) and $meta1->{filehash
} ne "deleted" )
1509 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1512 transmitfile
($meta1->{filehash
}, $file1);
1514 # otherwise we just use the working copy revision
1517 ( undef, $file1 ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1518 $meta1 = $updater->getmeta($filename, $wrev);
1519 transmitfile
($meta1->{filehash
}, $file1);
1522 # if we have a second -r switch, use it too
1523 if ( defined ( $revision2 ) )
1525 ( undef, $file2 ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1526 $meta2 = $updater->getmeta($filename, $revision2);
1528 unless ( defined ( $meta2 ) and $meta2->{filehash
} ne "deleted" )
1530 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1534 transmitfile
($meta2->{filehash
}, $file2);
1536 # otherwise we just use the working copy
1539 $file2 = $state->{entries
}{$filename}{modified_filename
};
1542 # if we have been given -r, and we don't have a $file2 yet, lets get one
1543 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1545 ( undef, $file2 ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1546 $meta2 = $updater->getmeta($filename, $wrev);
1547 transmitfile
($meta2->{filehash
}, $file2);
1550 # We need to have retrieved something useful
1551 next unless ( defined ( $meta1 ) );
1553 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1554 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision
}
1556 ( ( $state->{entries
}{$filename}{unchanged
} and ( not defined ( $state->{entries
}{$filename}{conflict
} ) or $state->{entries
}{$filename}{conflict
} !~ /^\+=/ ) )
1557 or ( defined($state->{entries
}{$filename}{modified_hash
}) and $state->{entries
}{$filename}{modified_hash
} eq $meta1->{filehash
} ) )
1560 # Apparently we only show diffs for locally modified files
1561 next unless ( defined($meta2) or defined ( $state->{entries
}{$filename}{modified_filename
} ) );
1563 print "M Index: $filename\n";
1564 print "M ===================================================================\n";
1565 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1566 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1567 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1569 foreach my $opt ( keys %{$state->{opt
}} )
1571 if ( ref $state->{opt
}{$opt} eq "ARRAY" )
1573 foreach my $value ( @
{$state->{opt
}{$opt}} )
1575 print "-$opt $value ";
1579 print "$state->{opt}{$opt} " if ( defined ( $state->{opt
}{$opt} ) );
1582 print "$filename\n";
1584 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision
} or "workingcopy" ));
1586 ( $fh, $filediff ) = tempfile
( DIR
=> $TEMP_DIR );
1588 if ( exists $state->{opt
}{u
} )
1590 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision
}) ?
"revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1592 system("diff $file1 $file2 > $filediff");
1607 my ( $cmd, $data ) = @_;
1611 $log->debug("req_log : " . ( defined($data) ?
$data : "[NULL]" ));
1612 #$log->debug("log state : " . Dumper($state));
1614 my ( $minrev, $maxrev );
1615 if ( defined ( $state->{opt
}{r
} ) and $state->{opt
}{r
} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1620 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1621 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1622 $minrev++ if ( defined($minrev) and $control eq "::" );
1625 # Grab a handle to the SQLite db and do any necessary updates
1626 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
1629 # if no files were specified, we need to work out what files we should be providing status on ...
1630 argsfromdir
($updater);
1632 # foreach file specified on the command line ...
1633 foreach my $filename ( @
{$state->{args
}} )
1635 $filename = filecleanup
($filename);
1637 my $headmeta = $updater->getmeta($filename);
1639 my $revisions = $updater->getlog($filename);
1640 my $totalrevisions = scalar(@
$revisions);
1642 if ( defined ( $minrev ) )
1644 $log->debug("Removing revisions less than $minrev");
1645 while ( scalar(@
$revisions) > 0 and $revisions->[-1]{revision
} < $minrev )
1650 if ( defined ( $maxrev ) )
1652 $log->debug("Removing revisions greater than $maxrev");
1653 while ( scalar(@
$revisions) > 0 and $revisions->[0]{revision
} > $maxrev )
1659 next unless ( scalar(@
$revisions) );
1662 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1663 print "M Working file: $filename\n";
1664 print "M head: 1.$headmeta->{revision}\n";
1665 print "M branch:\n";
1666 print "M locks: strict\n";
1667 print "M access list:\n";
1668 print "M symbolic names:\n";
1669 print "M keyword substitution: kv\n";
1670 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@
$revisions) . "\n";
1671 print "M description:\n";
1673 foreach my $revision ( @
$revisions )
1675 print "M ----------------------------\n";
1676 print "M revision 1.$revision->{revision}\n";
1677 # reformat the date for log output
1678 $revision->{modified
} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified
} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
1679 $revision->{author
} =~ s/\s+.*//;
1680 $revision->{author
} =~ s/^(.{8}).*/$1/;
1681 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash
} eq "deleted" ?
"dead" : "Exp" ) . "; lines: +2 -3\n";
1682 my $commitmessage = $updater->commitmessage($revision->{commithash
});
1683 $commitmessage =~ s/^/M /mg;
1684 print $commitmessage . "\n";
1686 print "M =============================================================================\n";
1694 my ( $cmd, $data ) = @_;
1696 argsplit
("annotate");
1698 $log->info("req_annotate : " . ( defined($data) ?
$data : "[NULL]" ));
1699 #$log->debug("status state : " . Dumper($state));
1701 # Grab a handle to the SQLite db and do any necessary updates
1702 my $updater = GITCVS
::updater
->new($state->{CVSROOT
}, $state->{module
}, $log);
1705 # if no files were specified, we need to work out what files we should be providing annotate on ...
1706 argsfromdir
($updater);
1708 # we'll need a temporary checkout dir
1709 my $tmpdir = tempdir
( DIR
=> $TEMP_DIR );
1710 my ( undef, $file_index ) = tempfile
( DIR
=> $TEMP_DIR, OPEN
=> 0 );
1711 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1713 $ENV{GIT_DIR
} = $state->{CVSROOT
} . "/";
1714 $ENV{GIT_INDEX_FILE
} = $file_index;
1718 # foreach file specified on the command line ...
1719 foreach my $filename ( @
{$state->{args
}} )
1721 $filename = filecleanup
($filename);
1723 my $meta = $updater->getmeta($filename);
1725 next unless ( $meta->{revision
} );
1727 # get all the commits that this file was in
1728 # in dense format -- aka skip dead revisions
1729 my $revisions = $updater->gethistorydense($filename);
1730 my $lastseenin = $revisions->[0][2];
1732 # populate the temporary index based on the latest commit were we saw
1733 # the file -- but do it cheaply without checking out any files
1734 # TODO: if we got a revision from the client, use that instead
1735 # to look up the commithash in sqlite (still good to default to
1736 # the current head as we do now)
1737 system("git-read-tree", $lastseenin);
1740 die "Error running git-read-tree $lastseenin $file_index $!";
1742 $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1744 # do a checkout of the file
1745 system('git-checkout-index', '-f', '-u', $filename);
1747 die "Error running git-checkout-index -f -u $filename : $!";
1750 $log->info("Annotate $filename");
1752 # Prepare a file with the commits from the linearized
1753 # history that annotate should know about. This prevents
1754 # git-jsannotate telling us about commits we are hiding
1757 open(ANNOTATEHINTS
, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
1758 for (my $i=0; $i < @
$revisions; $i++)
1760 print ANNOTATEHINTS
$revisions->[$i][2];
1761 if ($i+1 < @
$revisions) { # have we got a parent?
1762 print ANNOTATEHINTS
' ' . $revisions->[$i+1][2];
1764 print ANNOTATEHINTS
"\n";
1767 print ANNOTATEHINTS
"\n";
1768 close ANNOTATEHINTS
;
1770 my $annotatecmd = 'git-annotate';
1771 open(ANNOTATE
, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
1772 or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
1774 print "E Annotations for $filename\n";
1775 print "E ***************\n";
1776 while ( <ANNOTATE
> )
1778 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1780 my $commithash = $1;
1782 unless ( defined ( $metadata->{$commithash} ) )
1784 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1785 $metadata->{$commithash}{author
} =~ s/\s+.*//;
1786 $metadata->{$commithash}{author
} =~ s/^(.{8}).*/$1/;
1787 $metadata->{$commithash}{modified
} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified
} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1789 printf("M 1.%-5d (%-8s %10s): %s\n",
1790 $metadata->{$commithash}{revision
},
1791 $metadata->{$commithash}{author
},
1792 $metadata->{$commithash}{modified
},
1796 $log->warn("Error in annotate output! LINE: $_");
1797 print "E Annotate error \n";
1804 # done; get out of the tempdir
1811 # This method takes the state->{arguments} array and produces two new arrays.
1812 # The first is $state->{args} which is everything before the '--' argument, and
1813 # the second is $state->{files} which is everything after it.
1816 $state->{args
} = [];
1817 $state->{files
} = [];
1820 return unless( defined($state->{arguments
}) and ref $state->{arguments
} eq "ARRAY" );
1824 if ( defined($type) )
1827 $opt = { A
=> 0, N
=> 0, P
=> 0, R
=> 0, c
=> 0, f
=> 0, l
=> 0, n
=> 0, p
=> 0, s
=> 0, r
=> 1, D
=> 1, d
=> 1, k
=> 1, j
=> 1, } if ( $type eq "co" );
1828 $opt = { v
=> 0, l
=> 0, R
=> 0 } if ( $type eq "status" );
1829 $opt = { A
=> 0, P
=> 0, C
=> 0, d
=> 0, f
=> 0, l
=> 0, R
=> 0, p
=> 0, k
=> 1, r
=> 1, D
=> 1, j
=> 1, I
=> 1, W
=> 1 } if ( $type eq "update" );
1830 $opt = { l
=> 0, R
=> 0, k
=> 1, D
=> 1, D
=> 1, r
=> 2 } if ( $type eq "diff" );
1831 $opt = { c
=> 0, R
=> 0, l
=> 0, f
=> 0, F
=> 1, m
=> 1, r
=> 1 } if ( $type eq "ci" );
1832 $opt = { k
=> 1, m
=> 1 } if ( $type eq "add" );
1833 $opt = { f
=> 0, l
=> 0, R
=> 0 } if ( $type eq "remove" );
1834 $opt = { l
=> 0, b
=> 0, h
=> 0, R
=> 0, t
=> 0, N
=> 0, S
=> 0, r
=> 1, d
=> 1, s
=> 1, w
=> 1 } if ( $type eq "log" );
1837 while ( scalar ( @
{$state->{arguments
}} ) > 0 )
1839 my $arg = shift @
{$state->{arguments
}};
1841 next if ( $arg eq "--" );
1842 next unless ( $arg =~ /\S/ );
1844 # if the argument looks like a switch
1845 if ( $arg =~ /^-(\w)(.*)/ )
1847 # if it's a switch that takes an argument
1850 # If this switch has already been provided
1851 if ( $opt->{$1} > 1 and exists ( $state->{opt
}{$1} ) )
1853 $state->{opt
}{$1} = [ $state->{opt
}{$1} ];
1854 if ( length($2) > 0 )
1856 push @
{$state->{opt
}{$1}},$2;
1858 push @
{$state->{opt
}{$1}}, shift @
{$state->{arguments
}};
1861 # if there's extra data in the arg, use that as the argument for the switch
1862 if ( length($2) > 0 )
1864 $state->{opt
}{$1} = $2;
1866 $state->{opt
}{$1} = shift @
{$state->{arguments
}};
1870 $state->{opt
}{$1} = undef;
1875 push @
{$state->{args
}}, $arg;
1883 foreach my $value ( @
{$state->{arguments
}} )
1885 if ( $value eq "--" )
1890 push @
{$state->{args
}}, $value if ( $mode == 0 );
1891 push @
{$state->{files
}}, $value if ( $mode == 1 );
1896 # This method uses $state->{directory} to populate $state->{args} with a list of filenames
1899 my $updater = shift;
1901 $state->{args
} = [] if ( scalar(@
{$state->{args
}}) == 1 and $state->{args
}[0] eq "." );
1903 return if ( scalar ( @
{$state->{args
}} ) > 1 );
1905 my @gethead = @
{$updater->gethead};
1908 foreach my $file (keys %{$state->{entries
}}) {
1909 if ( exists $state->{entries
}{$file}{revision
} &&
1910 $state->{entries
}{$file}{revision
} == 0 )
1912 push @gethead, { name
=> $file, filehash
=> 'added' };
1916 if ( scalar(@
{$state->{args
}}) == 1 )
1918 my $arg = $state->{args
}[0];
1919 $arg .= $state->{prependdir
} if ( defined ( $state->{prependdir
} ) );
1921 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
1923 foreach my $file ( @gethead )
1925 next if ( $file->{filehash
} eq "deleted" and not defined ( $state->{entries
}{$file->{name
}} ) );
1926 next unless ( $file->{name
} =~ /^$arg\// or $file->{name
} eq $arg );
1927 push @
{$state->{args
}}, $file->{name
};
1930 shift @
{$state->{args
}} if ( scalar(@
{$state->{args
}}) > 1 );
1932 $log->info("Only one arg specified, populating file list automatically");
1934 $state->{args
} = [];
1936 foreach my $file ( @gethead )
1938 next if ( $file->{filehash
} eq "deleted" and not defined ( $state->{entries
}{$file->{name
}} ) );
1939 next unless ( $file->{name
} =~ s/^$state->{prependdir}// );
1940 push @
{$state->{args
}}, $file->{name
};
1945 # This method cleans up the $state variable after a command that uses arguments has run
1948 $state->{files
} = [];
1949 $state->{args
} = [];
1950 $state->{arguments
} = [];
1951 $state->{entries
} = {};
1956 my $filename = shift;
1958 return undef unless ( defined ( $state->{entries
}{$filename}{revision
} ) );
1960 return $1 if ( $state->{entries
}{$filename}{revision
} =~ /^1\.(\d+)/ );
1961 return -$1 if ( $state->{entries
}{$filename}{revision
} =~ /^-1\.(\d+)/ );
1966 # This method takes a file hash and does a CVS "file transfer" which transmits the
1967 # size of the file, and then the file contents.
1968 # If a second argument $targetfile is given, the file is instead written out to
1969 # a file by the name of $targetfile
1972 my $filehash = shift;
1973 my $targetfile = shift;
1975 if ( defined ( $filehash ) and $filehash eq "deleted" )
1977 $log->warn("filehash is 'deleted'");
1981 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
1983 my $type = `git-cat-file -t $filehash`;
1986 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
1988 my $size = `git-cat-file -s $filehash`;
1991 $log->debug("transmitfile($filehash) size=$size, type=$type");
1993 if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
1995 if ( defined ( $targetfile ) )
1997 open NEWFILE
, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
1998 print NEWFILE
$_ while ( <$fh> );
2002 print while ( <$fh> );
2004 close $fh or die ("Couldn't close filehandle for transmitfile()");
2006 die("Couldn't execute git-cat-file");
2010 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2011 # refers to the directory portion and the file portion of the filename
2015 my $filename = shift;
2016 my $fixforlocaldir = shift;
2018 my ( $filepart, $dirpart ) = ( $filename, "." );
2019 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2022 if ( $fixforlocaldir )
2024 $dirpart =~ s/^$state->{prependdir}//;
2027 return ( $filepart, $dirpart );
2032 my $filename = shift;
2034 return undef unless(defined($filename));
2035 if ( $filename =~ /^\// )
2037 print "E absolute filenames '$filename' not supported by server\n";
2041 $filename =~ s/^\.\///g
;
2042 $filename = $state->{prependdir
} . $filename;
2046 # Given a path, this function returns a string containing the kopts
2047 # that should go into that path's Entries line. For example, a binary
2048 # file should get -kb.
2053 # Once it exists, the git attributes system should be used to look up
2054 # what attributes apply to this path.
2056 # Until then, take the setting from the config file
2057 unless ( defined ( $cfg->{gitcvs
}{allbinary
} ) and $cfg->{gitcvs
}{allbinary
} =~ /^\s*(1|true|yes)\s*$/i )
2059 # Return "" to give no special treatment to any path
2062 # Alternatively, to have all files treated as if they are binary (which
2063 # is more like git itself), always return the "-kb" option
2068 package GITCVS
::log;
2071 #### Copyright The Open University UK - 2006.
2073 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2074 #### Martin Langhoff <martin@catalyst.net.nz>
2087 This module provides very crude logging with a similar interface to
2096 Creates a new log object, optionally you can specify a filename here to
2097 indicate the file to log to. If no log file is specified, you can specify one
2098 later with method setfile, or indicate you no longer want logging with method
2101 Until one of these methods is called, all log calls will buffer messages ready
2108 my $filename = shift;
2112 bless $self, $class;
2114 if ( defined ( $filename ) )
2116 open $self->{fh
}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2124 This methods takes a filename, and attempts to open that file as the log file.
2125 If successful, all buffered data is written out to the file, and any further
2126 logging is written directly to the file.
2132 my $filename = shift;
2134 if ( defined ( $filename ) )
2136 open $self->{fh
}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2139 return unless ( defined ( $self->{buffer
} ) and ref $self->{buffer
} eq "ARRAY" );
2141 while ( my $line = shift @
{$self->{buffer
}} )
2143 print {$self->{fh
}} $line;
2149 This method indicates no logging is going to be used. It flushes any entries in
2150 the internal buffer, and sets a flag to ensure no further data is put there.
2159 return unless ( defined ( $self->{buffer
} ) and ref $self->{buffer
} eq "ARRAY" );
2161 $self->{buffer
} = [];
2166 Internal method. Returns true if the log file is open, false otherwise.
2173 return 1 if ( defined ( $self->{fh
} ) and ref $self->{fh
} eq "GLOB" );
2177 =head2 debug info warn fatal
2179 These four methods are wrappers to _log. They provide the actual interface for
2183 sub debug
{ my $self = shift; $self->_log("debug", @_); }
2184 sub info
{ my $self = shift; $self->_log("info" , @_); }
2185 sub warn { my $self = shift; $self->_log("warn" , @_); }
2186 sub fatal
{ my $self = shift; $self->_log("fatal", @_); }
2190 This is an internal method called by the logging functions. It generates a
2191 timestamp and pushes the logged line either to file, or internal buffer.
2199 return if ( $self->{nolog
} );
2201 my @time = localtime;
2202 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2212 if ( $self->_logopen )
2214 print {$self->{fh
}} $timestring . " - " . join(" ",@_) . "\n";
2216 push @
{$self->{buffer
}}, $timestring . " - " . join(" ",@_) . "\n";
2222 This method simply closes the file handle if one is open
2229 if ( $self->_logopen )
2235 package GITCVS
::updater
;
2238 #### Copyright The Open University UK - 2006.
2240 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2241 #### Martin Langhoff <martin@catalyst.net.nz>
2263 die "Need to specify a git repository" unless ( defined($config) and -d
$config );
2264 die "Need to specify a module" unless ( defined($module) );
2266 $class = ref($class) || $class;
2270 bless $self, $class;
2272 $self->{module
} = $module;
2273 $self->{git_path
} = $config . "/";
2275 $self->{log} = $log;
2277 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d
$self->{git_path
} );
2279 $self->{dbdriver
} = $cfg->{gitcvs
}{$state->{method
}}{dbdriver
} ||
2280 $cfg->{gitcvs
}{dbdriver
} || "SQLite";
2281 $self->{dbname
} = $cfg->{gitcvs
}{$state->{method
}}{dbname
} ||
2282 $cfg->{gitcvs
}{dbname
} || "%Ggitcvs.%m.sqlite";
2283 $self->{dbuser
} = $cfg->{gitcvs
}{$state->{method
}}{dbuser
} ||
2284 $cfg->{gitcvs
}{dbuser
} || "";
2285 $self->{dbpass
} = $cfg->{gitcvs
}{$state->{method
}}{dbpass
} ||
2286 $cfg->{gitcvs
}{dbpass
} || "";
2287 my %mapping = ( m
=> $module,
2288 a
=> $state->{method
},
2289 u
=> getlogin || getpwuid($<) || $<,
2290 G
=> $self->{git_path
},
2291 g
=> mangle_dirname
($self->{git_path
}),
2293 $self->{dbname
} =~ s/%([mauGg])/$mapping{$1}/eg;
2294 $self->{dbuser
} =~ s/%([mauGg])/$mapping{$1}/eg;
2296 die "Invalid char ':' in dbdriver" if $self->{dbdriver
} =~ /:/;
2297 die "Invalid char ';' in dbname" if $self->{dbname
} =~ /;/;
2298 $self->{dbh
} = DBI
->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
2301 die "Error connecting to database\n" unless defined $self->{dbh
};
2303 $self->{tables
} = {};
2304 foreach my $table ( keys %{$self->{dbh
}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
2306 $self->{tables
}{$table} = 1;
2309 # Construct the revision table if required
2310 unless ( $self->{tables
}{revision
} )
2313 CREATE TABLE revision (
2315 revision INTEGER NOT NULL,
2316 filehash TEXT NOT NULL,
2317 commithash TEXT NOT NULL,
2318 author TEXT NOT NULL,
2319 modified TEXT NOT NULL,
2324 CREATE INDEX revision_ix1
2325 ON revision (name,revision)
2328 CREATE INDEX revision_ix2
2329 ON revision (name,commithash)
2333 # Construct the head table if required
2334 unless ( $self->{tables
}{head
} )
2339 revision INTEGER NOT NULL,
2340 filehash TEXT NOT NULL,
2341 commithash TEXT NOT NULL,
2342 author TEXT NOT NULL,
2343 modified TEXT NOT NULL,
2348 CREATE INDEX head_ix1
2353 # Construct the properties table if required
2354 unless ( $self->{tables
}{properties
} )
2357 CREATE TABLE properties (
2358 key TEXT NOT NULL PRIMARY KEY,
2364 # Construct the commitmsgs table if required
2365 unless ( $self->{tables
}{commitmsgs
} )
2368 CREATE TABLE commitmsgs (
2369 key TEXT NOT NULL PRIMARY KEY,
2385 # first lets get the commit list
2386 $ENV{GIT_DIR
} = $self->{git_path
};
2388 my $commitsha1 = `git rev-parse $self->{module}`;
2391 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
2392 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2394 die("Invalid module '$self->{module}'");
2399 my $lastcommit = $self->_get_prop("last_commit");
2401 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
2405 # Start exclusive lock here...
2406 $self->{dbh
}->begin_work() or die "Cannot lock database for BEGIN";
2408 # TODO: log processing is memory bound
2409 # if we can parse into a 2nd file that is in reverse order
2410 # we can probably do something really efficient
2411 my @git_log_params = ('--pretty', '--parents', '--topo-order');
2413 if (defined $lastcommit) {
2414 push @git_log_params, "$lastcommit..$self->{module}";
2416 push @git_log_params, $self->{module
};
2418 # git-rev-list is the backend / plumbing version of git-log
2419 open(GITLOG
, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
2428 if (m/^commit\s+(.*)$/) {
2429 # on ^commit lines put the just seen commit in the stack
2430 # and prime things for the next one
2433 unshift @commits, \
%copy;
2436 my @parents = split(m/\s+/, $1);
2437 $commit{hash
} = shift @parents;
2438 $commit{parents
} = \
@parents;
2439 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message
})) {
2440 # on rfc822-like lines seen before we see any message,
2441 # lowercase the entry and put it in the hash as key-value
2442 $commit{lc($1)} = $2;
2444 # message lines - skip initial empty line
2445 # and trim whitespace
2446 if (!exists($commit{message
}) && m/^\s*$/) {
2447 # define it to mark the end of headers
2448 $commit{message
} = '';
2451 s/^\s+//; s/\s+$//; # trim ws
2452 $commit{message
} .= $_ . "\n";
2457 unshift @commits, \
%commit if ( keys %commit );
2459 # Now all the commits are in the @commits bucket
2460 # ordered by time DESC. for each commit that needs processing,
2461 # determine whether it's following the last head we've seen or if
2462 # it's on its own branch, grab a file list, and add whatever's changed
2463 # NOTE: $lastcommit refers to the last commit from previous run
2464 # $lastpicked is the last commit we picked in this run
2467 if (defined $lastcommit) {
2468 $lastpicked = $lastcommit;
2471 my $committotal = scalar(@commits);
2472 my $commitcount = 0;
2474 # Load the head table into $head (for cached lookups during the update process)
2475 foreach my $file ( @
{$self->gethead()} )
2477 $head->{$file->{name
}} = $file;
2480 foreach my $commit ( @commits )
2482 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2483 if (defined $lastpicked)
2485 if (!in_array
($lastpicked, @
{$commit->{parents
}}))
2487 # skip, we'll see this delta
2488 # as part of a merge later
2489 # warn "skipping off-track $commit->{hash}\n";
2491 } elsif (@
{$commit->{parents
}} > 1) {
2492 # it is a merge commit, for each parent that is
2493 # not $lastpicked, see if we can get a log
2494 # from the merge-base to that parent to put it
2495 # in the message as a merge summary.
2496 my @parents = @
{$commit->{parents
}};
2497 foreach my $parent (@parents) {
2498 # git-merge-base can potentially (but rarely) throw
2499 # several candidate merge bases. let's assume
2500 # that the first one is the best one.
2501 if ($parent eq $lastpicked) {
2504 open my $p, 'git-merge-base '. $lastpicked . ' '
2506 my @output = (<$p>);
2508 my $base = join('', @output);
2512 # print "want to log between $base $parent \n";
2513 open(GITLOG
, '-|', 'git-log', "$base..$parent")
2514 or die "Cannot call git-log: $!";
2518 if (!defined $mergedhash) {
2519 if (m/^commit\s+(.+)$/) {
2525 # grab the first line that looks non-rfc822
2526 # aka has content after leading space
2527 if (m/^\s+(\S.*)$/) {
2529 $title = substr($title,0,100); # truncate
2530 unshift @merged, "$mergedhash $title";
2537 $commit->{mergemsg
} = $commit->{message
};
2538 $commit->{mergemsg
} .= "\nSummary of merged commits:\n\n";
2539 foreach my $summary (@merged) {
2540 $commit->{mergemsg
} .= "\t$summary\n";
2542 $commit->{mergemsg
} .= "\n\n";
2543 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2550 # convert the date to CVS-happy format
2551 $commit->{date
} = "$2 $1 $4 $3 $5" if ( $commit->{date
} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2553 if ( defined ( $lastpicked ) )
2555 my $filepipe = open(FILELIST
, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash
}) or die("Cannot call git-diff-tree : $!");
2557 while ( <FILELIST
> )
2560 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
2562 die("Couldn't process git-diff-tree line : $_");
2564 my ($mode, $hash, $change) = ($1, $2, $3);
2565 my $name = <FILELIST
>;
2568 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
2571 $git_perms .= "r" if ( $mode & 4 );
2572 $git_perms .= "w" if ( $mode & 2 );
2573 $git_perms .= "x" if ( $mode & 1 );
2574 $git_perms = "rw" if ( $git_perms eq "" );
2576 if ( $change eq "D" )
2578 #$log->debug("DELETE $name");
2581 revision
=> $head->{$name}{revision
} + 1,
2582 filehash
=> "deleted",
2583 commithash
=> $commit->{hash
},
2584 modified
=> $commit->{date
},
2585 author
=> $commit->{author
},
2588 $self->insert_rev($name, $head->{$name}{revision
}, $hash, $commit->{hash
}, $commit->{date
}, $commit->{author
}, $git_perms);
2590 elsif ( $change eq "M" )
2592 #$log->debug("MODIFIED $name");
2595 revision
=> $head->{$name}{revision
} + 1,
2597 commithash
=> $commit->{hash
},
2598 modified
=> $commit->{date
},
2599 author
=> $commit->{author
},
2602 $self->insert_rev($name, $head->{$name}{revision
}, $hash, $commit->{hash
}, $commit->{date
}, $commit->{author
}, $git_perms);
2604 elsif ( $change eq "A" )
2606 #$log->debug("ADDED $name");
2609 revision
=> $head->{$name}{revision
} ?
$head->{$name}{revision
}+1 : 1,
2611 commithash
=> $commit->{hash
},
2612 modified
=> $commit->{date
},
2613 author
=> $commit->{author
},
2616 $self->insert_rev($name, $head->{$name}{revision
}, $hash, $commit->{hash
}, $commit->{date
}, $commit->{author
}, $git_perms);
2620 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
2626 # this is used to detect files removed from the repo
2627 my $seen_files = {};
2629 my $filepipe = open(FILELIST
, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash
}) or die("Cannot call git-ls-tree : $!");
2631 while ( <FILELIST
> )
2634 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
2636 die("Couldn't process git-ls-tree line : $_");
2639 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2641 $seen_files->{$git_filename} = 1;
2643 my ( $oldhash, $oldrevision, $oldmode ) = (
2644 $head->{$git_filename}{filehash
},
2645 $head->{$git_filename}{revision
},
2646 $head->{$git_filename}{mode
}
2649 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2652 $git_perms .= "r" if ( $1 & 4 );
2653 $git_perms .= "w" if ( $1 & 2 );
2654 $git_perms .= "x" if ( $1 & 1 );
2659 # unless the file exists with the same hash, we need to update it ...
2660 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2662 my $newrevision = ( $oldrevision or 0 ) + 1;
2664 $head->{$git_filename} = {
2665 name
=> $git_filename,
2666 revision
=> $newrevision,
2667 filehash
=> $git_hash,
2668 commithash
=> $commit->{hash
},
2669 modified
=> $commit->{date
},
2670 author
=> $commit->{author
},
2675 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash
}, $commit->{date
}, $commit->{author
}, $git_perms);
2680 # Detect deleted files
2681 foreach my $file ( keys %$head )
2683 unless ( exists $seen_files->{$file} or $head->{$file}{filehash
} eq "deleted" )
2685 $head->{$file}{revision
}++;
2686 $head->{$file}{filehash
} = "deleted";
2687 $head->{$file}{commithash
} = $commit->{hash
};
2688 $head->{$file}{modified
} = $commit->{date
};
2689 $head->{$file}{author
} = $commit->{author
};
2691 $self->insert_rev($file, $head->{$file}{revision
}, $head->{$file}{filehash
}, $commit->{hash
}, $commit->{date
}, $commit->{author
}, $head->{$file}{mode
});
2694 # END : "Detect deleted files"
2698 if (exists $commit->{mergemsg
})
2700 $self->insert_mergelog($commit->{hash
}, $commit->{mergemsg
});
2703 $lastpicked = $commit->{hash
};
2705 $self->_set_prop("last_commit", $commit->{hash
});
2708 $self->delete_head();
2709 foreach my $file ( keys %$head )
2713 $head->{$file}{revision
},
2714 $head->{$file}{filehash
},
2715 $head->{$file}{commithash
},
2716 $head->{$file}{modified
},
2717 $head->{$file}{author
},
2718 $head->{$file}{mode
},
2721 # invalidate the gethead cache
2722 $self->{gethead_cache
} = undef;
2725 # Ending exclusive lock here
2726 $self->{dbh
}->commit() or die "Failed to commit changes to SQLite";
2733 my $revision = shift;
2734 my $filehash = shift;
2735 my $commithash = shift;
2736 my $modified = shift;
2740 my $insert_rev = $self->{dbh
}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2741 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2750 my $insert_mergelog = $self->{dbh
}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2751 $insert_mergelog->execute($key, $value);
2758 my $delete_head = $self->{dbh
}->prepare_cached("DELETE FROM head",{},1);
2759 $delete_head->execute();
2766 my $revision = shift;
2767 my $filehash = shift;
2768 my $commithash = shift;
2769 my $modified = shift;
2773 my $insert_head = $self->{dbh
}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2774 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2780 my $filename = shift;
2782 my $db_query = $self->{dbh
}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2783 $db_query->execute($filename);
2784 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2786 return ( $hash, $revision, $mode );
2794 my $db_query = $self->{dbh
}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2795 $db_query->execute($key);
2796 my ( $value ) = $db_query->fetchrow_array;
2807 my $db_query = $self->{dbh
}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2808 $db_query->execute($value, $key);
2810 unless ( $db_query->rows )
2812 $db_query = $self->{dbh
}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2813 $db_query->execute($key, $value);
2827 return $self->{gethead_cache
} if ( defined ( $self->{gethead_cache
} ) );
2829 my $db_query = $self->{dbh
}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
2830 $db_query->execute();
2833 while ( my $file = $db_query->fetchrow_hashref )
2838 $self->{gethead_cache
} = $tree;
2850 my $filename = shift;
2852 my $db_query = $self->{dbh
}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2853 $db_query->execute($filename);
2856 while ( my $file = $db_query->fetchrow_hashref )
2866 This function takes a filename (with path) argument and returns a hashref of
2867 metadata for that file.
2874 my $filename = shift;
2875 my $revision = shift;
2878 if ( defined($revision) and $revision =~ /^\d+$/ )
2880 $db_query = $self->{dbh
}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2881 $db_query->execute($filename, $revision);
2883 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2885 $db_query = $self->{dbh
}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2886 $db_query->execute($filename, $revision);
2888 $db_query = $self->{dbh
}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2889 $db_query->execute($filename);
2892 return $db_query->fetchrow_hashref;
2895 =head2 commitmessage
2897 this function takes a commithash and returns the commit message for that commit
2903 my $commithash = shift;
2905 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2908 $db_query = $self->{dbh
}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2909 $db_query->execute($commithash);
2911 my ( $message ) = $db_query->fetchrow_array;
2913 if ( defined ( $message ) )
2915 $message .= " " if ( $message =~ /\n$/ );
2919 my @lines = safe_pipe_capture
("git-cat-file", "commit", $commithash);
2920 shift @lines while ( $lines[0] =~ /\S/ );
2921 $message = join("",@lines);
2922 $message .= " " if ( $message =~ /\n$/ );
2928 This function takes a filename (with path) argument and returns an arrayofarrays
2929 containing revision,filehash,commithash ordered by revision descending
2935 my $filename = shift;
2938 $db_query = $self->{dbh
}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2939 $db_query->execute($filename);
2941 return $db_query->fetchall_arrayref;
2944 =head2 gethistorydense
2946 This function takes a filename (with path) argument and returns an arrayofarrays
2947 containing revision,filehash,commithash ordered by revision descending.
2949 This version of gethistory skips deleted entries -- so it is useful for annotate.
2950 The 'dense' part is a reference to a '--dense' option available for git-rev-list
2951 and other git tools that depend on it.
2957 my $filename = shift;
2960 $db_query = $self->{dbh
}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2961 $db_query->execute($filename);
2963 return $db_query->fetchall_arrayref;
2968 from Array::PAT - mimics the in_array() function
2969 found in PHP. Yuck but works for small arrays.
2974 my ($check, @array) = @_;
2976 foreach my $test (@array){
2977 if($check eq $test){
2984 =head2 safe_pipe_capture
2986 an alternative to `command` that allows input to be passed as an array
2987 to work around shell problems with weird characters in arguments
2990 sub safe_pipe_capture
{
2994 if (my $pid = open my $child, '-|') {
2995 @output = (<$child>);
2996 close $child or die join(' ',@_).": $! $?";
2998 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3000 return wantarray ?
@output : join('',@output);
3003 =head2 mangle_dirname
3005 create a string from a directory name that is suitable to use as
3006 part of a filename, mainly by converting all chars except \w.- to _
3009 sub mangle_dirname
{
3010 my $dirname = shift;
3011 return unless defined $dirname;
3013 $dirname =~ s/[^\w.-]/_/g;