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@laptop.org>
14 #### Released under the GNU Public License, version 2.
24 use File::Temp qw/tempdir tempfile/;
25 use File::Path qw/rmtree/;
27 use Getopt::Long qw(:config require_order no_ignore_case);
29 my $VERSION = '@@GIT_VERSION@@';
31 my $log = GITCVS::log->new();
49 # Enable autoflush for STDOUT (otherwise the whole thing falls apart)
52 #### Definition and mappings of functions ####
54 # NOTE: Despite the existence of req_CATCHALL and req_EMPTY unimplemented
55 # requests, this list is incomplete. It is missing many rarer/optional
56 # requests. Perhaps some clients require a claim of support for
57 # these specific requests for main functionality to work?
60 'Valid-responses' => \&req_Validresponses,
61 'valid-requests' => \&req_validrequests,
62 'Directory' => \&req_Directory,
63 'Sticky' => \&req_Sticky,
64 'Entry' => \&req_Entry,
65 'Modified' => \&req_Modified,
66 'Unchanged' => \&req_Unchanged,
67 'Questionable' => \&req_Questionable,
68 'Argument' => \&req_Argument,
69 'Argumentx' => \&req_Argument,
70 'expand-modules' => \&req_expandmodules,
72 'remove' => \&req_remove,
74 'update' => \&req_update,
79 'tag' => \&req_CATCHALL,
80 'status' => \&req_status,
81 'admin' => \&req_CATCHALL,
82 'history' => \&req_CATCHALL,
83 'watchers' => \&req_EMPTY,
84 'editors' => \&req_EMPTY,
85 'noop' => \&req_EMPTY,
86 'annotate' => \&req_annotate,
87 'Global_option' => \&req_Globaloption,
90 ##############################################
93 # $state holds all the bits of information the clients sends us that could
94 # potentially be useful when it comes to actually _doing_ something.
95 my $state = { prependdir => '' };
97 # Work is for managing temporary working directory
100 state => undef, # undef, 1 (empty), 2 (with stuff)
107 $log->info("--------------- STARTING -----------------");
110 "Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n".
111 " --base-path <path> : Prepend to requested CVSROOT\n".
112 " Can be read from GIT_CVSSERVER_BASE_PATH\n".
113 " --strict-paths : Don't allow recursing into subdirectories\n".
114 " --export-all : Don't check for gitcvs.enabled in config\n".
115 " --version, -V : Print version information and exit\n".
116 " -h, -H : Print usage information and exit\n".
118 "<directory> ... is a list of allowed directories. If no directories\n".
119 "are given, all are allowed. This is an additional restriction, gitcvs\n".
120 "access still needs to be enabled by the gitcvs.enabled config option.\n".
121 "Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n";
123 my @opts = ( 'h|H', 'version|V',
124 'base-path=s', 'strict-paths', 'export-all' );
125 GetOptions( $state, @opts )
128 if ($state->{version}) {
129 print "git-cvsserver version $VERSION\n";
132 if ($state->{help}) {
137 my $TEMP_DIR = tempdir( CLEANUP => 1 );
138 $log->debug("Temporary directory is '$TEMP_DIR'");
140 $state->{method} = 'ext';
142 if ($ARGV[0] eq 'pserver') {
143 $state->{method} = 'pserver';
145 } elsif ($ARGV[0] eq 'server') {
150 # everything else is a directory
151 $state->{allowed_roots} = [ @ARGV ];
153 # don't export the whole system unless the users requests it
154 if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
155 die "--export-all can only be used together with an explicit whitelist\n";
158 # Environment handling for running under git-shell
159 if (exists $ENV{GIT_CVSSERVER_BASE_PATH}) {
160 if ($state->{'base-path'}) {
161 die "Cannot specify base path both ways.\n";
163 my $base_path = $ENV{GIT_CVSSERVER_BASE_PATH};
164 $state->{'base-path'} = $base_path;
165 $log->debug("Picked up base path '$base_path' from environment.\n");
167 if (exists $ENV{GIT_CVSSERVER_ROOT}) {
168 if (@{$state->{allowed_roots}}) {
169 die "Cannot specify roots both ways: @ARGV\n";
171 my $allowed_root = $ENV{GIT_CVSSERVER_ROOT};
172 $state->{allowed_roots} = [ $allowed_root ];
173 $log->debug("Picked up allowed root '$allowed_root' from environment.\n");
176 # if we are called with a pserver argument,
177 # deal with the authentication cat before entering the
179 if ($state->{method} eq 'pserver') {
180 my $line = <STDIN>; chomp $line;
181 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
182 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
185 $line = <STDIN>; chomp $line;
186 unless (req_Root('root', $line)) { # reuse Root
187 print "E Invalid root $line \n";
190 $line = <STDIN>; chomp $line;
192 $line = <STDIN>; chomp $line;
193 my $password = $line;
195 if ($user eq 'anonymous') {
196 # "A" will be 1 byte, use length instead in case the
197 # encryption method ever changes (yeah, right!)
198 if (length($password) > 1 ) {
199 print "E Don't supply a password for the `anonymous' user\n";
200 print "I HATE YOU\n";
204 # Fall through to LOVE
206 # Trying to authenticate a user
207 if (not exists $cfg->{gitcvs}->{authdb}) {
208 print "E the repo config file needs a [gitcvs] section with an 'authdb' parameter set to the filename of the authentication database\n";
209 print "I HATE YOU\n";
213 my $authdb = $cfg->{gitcvs}->{authdb};
215 unless (-e $authdb) {
216 print "E The authentication database specified in [gitcvs.authdb] does not exist\n";
217 print "I HATE YOU\n";
222 open my $passwd, "<", $authdb or die $!;
224 if (m{^\Q$user\E:(.*)}) {
225 if (crypt($user, descramble($password)) eq $1) {
233 print "I HATE YOU\n";
237 # Fall through to LOVE
240 # For checking whether the user is anonymous on commit
241 $state->{user} = $user;
243 $line = <STDIN>; chomp $line;
244 unless ($line eq "END $request REQUEST") {
245 die "E Do not understand $line -- expecting END $request REQUEST\n";
247 print "I LOVE YOU\n";
248 exit if $request eq 'VERIFICATION'; # cvs login
249 # and now back to our regular programme...
252 # Keep going until the client closes the connection
257 # Check to see if we've seen this method, and call appropriate function.
258 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
260 # use the $methods hash to call the appropriate sub for this command
261 #$log->info("Method : $1");
262 &{$methods->{$1}}($1,$2);
264 # log fatal because we don't understand this function. If this happens
265 # we're fairly screwed because we don't know if the client is expecting
266 # a response. If it is, the client will hang, we'll hang, and the whole
267 # thing will be custard.
268 $log->fatal("Don't understand command $_\n");
269 die("Unknown command $_");
273 $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
274 $log->info("--------------- FINISH -----------------");
279 # Magic catchall method.
280 # This is the method that will handle all commands we haven't yet
281 # implemented. It simply sends a warning to the log file indicating a
282 # command that hasn't been implemented has been invoked.
285 my ( $cmd, $data ) = @_;
286 $log->warn("Unhandled command : req_$cmd : $data");
289 # This method invariably succeeds with an empty response.
296 # Response expected: no. Tell the server which CVSROOT to use. Note that
297 # pathname is a local directory and not a fully qualified CVSROOT variable.
298 # pathname must already exist; if creating a new root, use the init
299 # request, not Root. pathname does not include the hostname of the server,
300 # how to access the server, etc.; by the time the CVS protocol is in use,
301 # connection, authentication, etc., are already taken care of. The Root
302 # request must be sent only once, and it must be sent before any requests
303 # other than Valid-responses, valid-requests, UseUnchanged, Set or init.
306 my ( $cmd, $data ) = @_;
307 $log->debug("req_Root : $data");
309 unless ($data =~ m#^/#) {
310 print "error 1 Root must be an absolute pathname\n";
314 my $cvsroot = $state->{'base-path'} || '';
318 if ($state->{CVSROOT}
319 && ($state->{CVSROOT} ne $cvsroot)) {
320 print "error 1 Conflicting roots specified\n";
324 $state->{CVSROOT} = $cvsroot;
326 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
328 if (@{$state->{allowed_roots}}) {
330 foreach my $dir (@{$state->{allowed_roots}}) {
331 next unless $dir =~ m#^/#;
333 if ($state->{'strict-paths'}) {
334 if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
338 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
345 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
347 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
352 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
353 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
355 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
359 my @gitvars = `git config -l`;
361 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
363 print "error 1 - problem executing git-config\n";
366 foreach my $line ( @gitvars )
368 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
372 $cfg->{$1}{$2}{$3} = $4;
376 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
377 || $cfg->{gitcvs}{enabled});
378 unless ($state->{'export-all'} ||
379 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
380 print "E GITCVS emulation needs to be enabled on this repo\n";
381 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
383 print "error 1 GITCVS emulation disabled\n";
387 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
390 $log->setfile($logfile);
398 # Global_option option \n
399 # Response expected: no. Transmit one of the global options `-q', `-Q',
400 # `-l', `-t', `-r', or `-n'. option must be one of those strings, no
401 # variations (such as combining of options) are allowed. For graceful
402 # handling of valid-requests, it is probably better to make new global
403 # options separate requests, rather than trying to add them to this
407 my ( $cmd, $data ) = @_;
408 $log->debug("req_Globaloption : $data");
409 $state->{globaloptions}{$data} = 1;
412 # Valid-responses request-list \n
413 # Response expected: no. Tell the server what responses the client will
414 # accept. request-list is a space separated list of tokens.
415 sub req_Validresponses
417 my ( $cmd, $data ) = @_;
418 $log->debug("req_Validresponses : $data");
420 # TODO : re-enable this, currently it's not particularly useful
421 #$state->{validresponses} = [ split /\s+/, $data ];
425 # Response expected: yes. Ask the server to send back a Valid-requests
427 sub req_validrequests
429 my ( $cmd, $data ) = @_;
431 $log->debug("req_validrequests");
433 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
434 $log->debug("SEND : ok");
436 print "Valid-requests " . join(" ",keys %$methods) . "\n";
440 # Directory local-directory \n
441 # Additional data: repository \n. Response expected: no. Tell the server
442 # what directory to use. The repository should be a directory name from a
443 # previous server response. Note that this both gives a default for Entry
444 # and Modified and also for ci and the other commands; normal usage is to
445 # send Directory for each directory in which there will be an Entry or
446 # Modified, and then a final Directory for the original directory, then the
447 # command. The local-directory is relative to the top level at which the
448 # command is occurring (i.e. the last Directory which is sent before the
449 # command); to indicate that top level, `.' should be sent for
453 my ( $cmd, $data ) = @_;
455 my $repository = <STDIN>;
459 $state->{localdir} = $data;
460 $state->{repository} = $repository;
461 $state->{path} = $repository;
462 $state->{path} =~ s/^\Q$state->{CVSROOT}\E\///;
463 $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
464 $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
466 $state->{directory} = $state->{localdir};
467 $state->{directory} = "" if ( $state->{directory} eq "." );
468 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
470 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
472 $log->info("Setting prepend to '$state->{path}'");
473 $state->{prependdir} = $state->{path};
475 foreach my $entry ( keys %{$state->{entries}} )
477 $entries{$state->{prependdir} . $entry} = $state->{entries}{$entry};
479 $state->{entries}=\%entries;
482 foreach my $dir ( keys %{$state->{dirMap}} )
484 $dirMap{$state->{prependdir} . $dir} = $state->{dirMap}{$dir};
486 $state->{dirMap}=\%dirMap;
489 if ( defined ( $state->{prependdir} ) )
491 $log->debug("Prepending '$state->{prependdir}' to state|directory");
492 $state->{directory} = $state->{prependdir} . $state->{directory}
495 if ( ! defined($state->{dirMap}{$state->{directory}}) )
497 $state->{dirMap}{$state->{directory}} =
504 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
508 # Response expected: no. Tell the server that the directory most
509 # recently specified with Directory has a sticky tag or date
510 # tagspec. The first character of tagspec is T for a tag, D for
511 # a date, or some other character supplied by a Set-sticky
512 # response from a previous request to the server. The remainder
513 # of tagspec contains the actual tag or date, again as supplied
515 # The server should remember Static-directory and Sticky requests
516 # for a particular directory; the client need not resend them each
517 # time it sends a Directory request for a given directory. However,
518 # the server is not obliged to remember them beyond the context
519 # of a single command.
522 my ( $cmd, $tagspec ) = @_;
529 elsif($tagspec=~/^T([^ ]+)\s*$/)
531 $stickyInfo = { 'tag' => $1 };
533 elsif($tagspec=~/^D([0-9.]+)\s*$/)
535 $stickyInfo= { 'date' => $1 };
539 die "Unknown tag_or_date format\n";
541 $state->{dirMap}{$state->{directory}}{stickyInfo}=$stickyInfo;
543 $log->debug("req_Sticky : tagspec=$tagspec repository=$state->{repository}"
544 . " path=$state->{path} directory=$state->{directory}"
545 . " module=$state->{module}");
548 # Entry entry-line \n
549 # Response expected: no. Tell the server what version of a file is on the
550 # local machine. The name in entry-line is a name relative to the directory
551 # most recently specified with Directory. If the user is operating on only
552 # some files in a directory, Entry requests for only those files need be
553 # included. If an Entry request is sent without Modified, Is-modified, or
554 # Unchanged, it means the file is lost (does not exist in the working
555 # directory). If both Entry and one of Modified, Is-modified, or Unchanged
556 # are sent for the same file, Entry must be sent first. For a given file,
557 # one can send Modified, Is-modified, or Unchanged, but not more than one
561 my ( $cmd, $data ) = @_;
563 #$log->debug("req_Entry : $data");
565 my @data = split(/\//, $data, -1);
567 $state->{entries}{$state->{directory}.$data[1]} = {
568 revision => $data[2],
569 conflict => $data[3],
571 tag_or_date => $data[5],
574 $state->{dirMap}{$state->{directory}}{names}{$data[1]} = 'F';
576 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
579 # Questionable filename \n
580 # Response expected: no. Additional data: no. Tell the server to check
581 # whether filename should be ignored, and if not, next time the server
582 # sends responses, send (in a M response) `?' followed by the directory and
583 # filename. filename must not contain `/'; it needs to be a file in the
584 # directory named by the most recent Directory request.
587 my ( $cmd, $data ) = @_;
589 $log->debug("req_Questionable : $data");
590 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
594 # Response expected: yes. Add a file or directory. This uses any previous
595 # Argument, Directory, Entry, or Modified requests, if they have been sent.
596 # The last Directory sent specifies the working directory at the time of
597 # the operation. To add a directory, send the directory to be added using
598 # Directory and Argument requests.
601 my ( $cmd, $data ) = @_;
605 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
610 foreach my $filename ( @{$state->{args}} )
612 $filename = filecleanup($filename);
614 my $meta = $updater->getmeta($filename);
615 my $wrev = revparse($filename);
617 if ($wrev && $meta && ($wrev=~/^-/))
619 # previously removed file, add back
620 $log->info("added file $filename was previously removed, send $meta->{revision}");
622 print "MT +updated\n";
623 print "MT text U \n";
624 print "MT fname $filename\n";
625 print "MT newline\n";
626 print "MT -updated\n";
628 unless ( $state->{globaloptions}{-n} )
630 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
632 print "Created $dirpart\n";
633 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
635 # this is an "entries" line
636 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
637 $log->debug("/$filepart/$meta->{revision}//$kopts/");
638 print "/$filepart/$meta->{revision}//$kopts/\n";
640 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
641 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
643 transmitfile($meta->{filehash});
649 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
651 print "E cvs add: nothing known about `$filename'\n";
654 # TODO : check we're not squashing an already existing file
655 if ( defined ( $state->{entries}{$filename}{revision} ) )
657 print "E cvs add: `$filename' has already been entered\n";
661 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
663 print "E cvs add: scheduling file `$filename' for addition\n";
665 print "Checked-in $dirpart\n";
667 my $kopts = kopts_from_path($filename,"file",
668 $state->{entries}{$filename}{modified_filename});
669 print "/$filepart/0//$kopts/\n";
671 my $requestedKopts = $state->{opt}{k};
672 if(defined($requestedKopts))
674 $requestedKopts = "-k$requestedKopts";
678 $requestedKopts = "";
680 if( $kopts ne $requestedKopts )
682 $log->warn("Ignoring requested -k='$requestedKopts'"
683 . " for '$filename'; detected -k='$kopts' instead");
684 #TODO: Also have option to send warning to user?
690 if ( $addcount == 1 )
692 print "E cvs add: use `cvs commit' to add this file permanently\n";
694 elsif ( $addcount > 1 )
696 print "E cvs add: use `cvs commit' to add these files permanently\n";
703 # Response expected: yes. Remove a file. This uses any previous Argument,
704 # Directory, Entry, or Modified requests, if they have been sent. The last
705 # Directory sent specifies the working directory at the time of the
706 # operation. Note that this request does not actually do anything to the
707 # repository; the only effect of a successful remove request is to supply
708 # the client with a new entries line containing `-' to indicate a removed
709 # file. In fact, the client probably could perform this operation without
710 # contacting the server, although using remove may cause the server to
711 # perform a few more checks. The client sends a subsequent ci request to
712 # actually record the removal in the repository.
715 my ( $cmd, $data ) = @_;
719 # Grab a handle to the SQLite db and do any necessary updates
720 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
723 #$log->debug("add state : " . Dumper($state));
727 foreach my $filename ( @{$state->{args}} )
729 $filename = filecleanup($filename);
731 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
733 print "E cvs remove: file `$filename' still in working directory\n";
737 my $meta = $updater->getmeta($filename);
738 my $wrev = revparse($filename);
740 unless ( defined ( $wrev ) )
742 print "E cvs remove: nothing known about `$filename'\n";
746 if ( defined($wrev) and ($wrev=~/^-/) )
748 print "E cvs remove: file `$filename' already scheduled for removal\n";
752 unless ( $wrev eq $meta->{revision} )
754 # TODO : not sure if the format of this message is quite correct.
755 print "E cvs remove: Up to date check failed for `$filename'\n";
760 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
762 print "E cvs remove: scheduling `$filename' for removal\n";
764 print "Checked-in $dirpart\n";
766 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
767 print "/$filepart/-$wrev//$kopts/\n";
774 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
776 elsif ( $rmcount > 1 )
778 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
784 # Modified filename \n
785 # Response expected: no. Additional data: mode, \n, file transmission. Send
786 # the server a copy of one locally modified file. filename is a file within
787 # the most recent directory sent with Directory; it must not contain `/'.
788 # If the user is operating on only some files in a directory, only those
789 # files need to be included. This can also be sent without Entry, if there
790 # is no entry for the file.
793 my ( $cmd, $data ) = @_;
797 or (print "E end of file reading mode for $data\n"), return;
801 or (print "E end of file reading size of $data\n"), return;
804 # Grab config information
805 my $blocksize = 8192;
806 my $bytesleft = $size;
809 # Get a filehandle/name to write it to
810 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
812 # Loop over file data writing out to temporary file.
815 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
816 read STDIN, $tmp, $blocksize;
818 $bytesleft -= $blocksize;
822 or (print "E failed to write temporary, $filename: $!\n"), return;
824 # Ensure we have something sensible for the file mode
825 if ( $mode =~ /u=(\w+)/ )
832 # Save the file data in $state
833 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
834 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
835 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git hash-object $filename`;
836 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
838 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
841 # Unchanged filename \n
842 # Response expected: no. Tell the server that filename has not been
843 # modified in the checked out directory. The filename is a file within the
844 # most recent directory sent with Directory; it must not contain `/'.
847 my ( $cmd, $data ) = @_;
849 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
851 #$log->debug("req_Unchanged : $data");
855 # Response expected: no. Save argument for use in a subsequent command.
856 # Arguments accumulate until an argument-using command is given, at which
857 # point they are forgotten.
859 # Response expected: no. Append \n followed by text to the current argument
863 my ( $cmd, $data ) = @_;
865 # Argumentx means: append to last Argument (with a newline in front)
867 $log->debug("$cmd : $data");
869 if ( $cmd eq 'Argumentx') {
870 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
872 push @{$state->{arguments}}, $data;
877 # Response expected: yes. Expand the modules which are specified in the
878 # arguments. Returns the data in Module-expansion responses. Note that the
879 # server can assume that this is checkout or export, not rtag or rdiff; the
880 # latter do not access the working directory and thus have no need to
881 # expand modules on the client side. Expand may not be the best word for
882 # what this request does. It does not necessarily tell you all the files
883 # contained in a module, for example. Basically it is a way of telling you
884 # which working directories the server needs to know about in order to
885 # handle a checkout of the specified modules. For example, suppose that the
886 # server has a module defined by
887 # aliasmodule -a 1dir
888 # That is, one can check out aliasmodule and it will take 1dir in the
889 # repository and check it out to 1dir in the working directory. Now suppose
890 # the client already has this module checked out and is planning on using
891 # the co request to update it. Without using expand-modules, the client
892 # would have two bad choices: it could either send information about all
893 # working directories under the current directory, which could be
894 # unnecessarily slow, or it could be ignorant of the fact that aliasmodule
895 # stands for 1dir, and neglect to send information for 1dir, which would
896 # lead to incorrect operation. With expand-modules, the client would first
897 # ask for the module to be expanded:
898 sub req_expandmodules
900 my ( $cmd, $data ) = @_;
904 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
906 unless ( ref $state->{arguments} eq "ARRAY" )
912 foreach my $module ( @{$state->{arguments}} )
914 $log->debug("SEND : Module-expansion $module");
915 print "Module-expansion $module\n";
923 # Response expected: yes. Get files from the repository. This uses any
924 # previous Argument, Directory, Entry, or Modified requests, if they have
925 # been sent. Arguments to this command are module names; the client cannot
926 # know what directories they correspond to except by (1) just sending the
927 # co request, and then seeing what directory names the server sends back in
928 # its responses, and (2) the expand-modules request.
931 my ( $cmd, $data ) = @_;
935 # Provide list of modules, if -c was used.
936 if (exists $state->{opt}{c}) {
937 my $showref = `git show-ref --heads`;
938 for my $line (split '\n', $showref) {
939 if ( $line =~ m% refs/heads/(.*)$% ) {
947 my $module = $state->{args}[0];
948 $state->{module} = $module;
949 my $checkout_path = $module;
951 # use the user specified directory if we're given it
952 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
954 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
956 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
958 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
960 # Grab a handle to the SQLite db and do any necessary updates
961 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
964 $checkout_path =~ s|/$||; # get rid of trailing slashes
966 # Eclipse seems to need the Clear-sticky command
967 # to prepare the 'Entries' file for the new directory.
968 print "Clear-sticky $checkout_path/\n";
969 print $state->{CVSROOT} . "/$module/\n";
970 print "Clear-static-directory $checkout_path/\n";
971 print $state->{CVSROOT} . "/$module/\n";
972 print "Clear-sticky $checkout_path/\n"; # yes, twice
973 print $state->{CVSROOT} . "/$module/\n";
974 print "Template $checkout_path/\n";
975 print $state->{CVSROOT} . "/$module/\n";
978 # instruct the client that we're checking out to $checkout_path
979 print "E cvs checkout: Updating $checkout_path\n";
986 my ($dir, $repodir, $remotedir, $seendirs) = @_;
987 my $parent = dirname($dir);
990 $remotedir =~ s|/+$||;
992 $log->debug("announcedir $dir, $repodir, $remotedir" );
994 if ($parent eq '.' || $parent eq './') {
997 # recurse to announce unseen parents first
998 if (length($parent) && !exists($seendirs->{$parent})) {
999 prepdir($parent, $repodir, $remotedir, $seendirs);
1001 # Announce that we are going to modify at the parent level
1003 print "E cvs checkout: Updating $remotedir/$parent\n";
1005 print "E cvs checkout: Updating $remotedir\n";
1007 print "Clear-sticky $remotedir/$parent/\n";
1008 print "$repodir/$parent/\n";
1010 print "Clear-static-directory $remotedir/$dir/\n";
1011 print "$repodir/$dir/\n";
1012 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
1013 print "$repodir/$parent/\n";
1014 print "Template $remotedir/$dir/\n";
1015 print "$repodir/$dir/\n";
1018 $seendirs->{$dir} = 1;
1021 foreach my $git ( @{$updater->gethead} )
1023 # Don't want to check out deleted files
1024 next if ( $git->{filehash} eq "deleted" );
1026 my $fullName = $git->{name};
1027 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
1029 if (length($git->{dir}) && $git->{dir} ne './'
1030 && $git->{dir} ne $lastdir ) {
1031 unless (exists($seendirs{$git->{dir}})) {
1032 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
1033 $checkout_path, \%seendirs);
1034 $lastdir = $git->{dir};
1035 $seendirs{$git->{dir}} = 1;
1037 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
1040 # modification time of this file
1041 print "Mod-time $git->{modified}\n";
1043 # print some information to the client
1044 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
1046 print "M U $checkout_path/$git->{dir}$git->{name}\n";
1048 print "M U $checkout_path/$git->{name}\n";
1051 # instruct client we're sending a file to put in this path
1052 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
1054 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
1056 # this is an "entries" line
1057 my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
1058 print "/$git->{name}/$git->{revision}//$kopts/\n";
1060 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
1063 transmitfile($git->{filehash});
1072 # Response expected: yes. Actually do a cvs update command. This uses any
1073 # previous Argument, Directory, Entry, or Modified requests, if they have
1074 # been sent. The last Directory sent specifies the working directory at the
1075 # time of the operation. The -I option is not used--files which the client
1076 # can decide whether to ignore are not mentioned and the client sends the
1077 # Questionable request for others.
1080 my ( $cmd, $data ) = @_;
1082 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
1087 # It may just be a client exploring the available heads/modules
1088 # in that case, list them as top level directories and leave it
1089 # at that. Eclipse uses this technique to offer you a list of
1090 # projects (heads in this case) to checkout.
1092 if ($state->{module} eq '') {
1093 my $showref = `git show-ref --heads`;
1094 print "E cvs update: Updating .\n";
1095 for my $line (split '\n', $showref) {
1096 if ( $line =~ m% refs/heads/(.*)$% ) {
1097 print "E cvs update: New directory `$1'\n";
1105 # Grab a handle to the SQLite db and do any necessary updates
1106 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1110 argsfromdir($updater);
1112 #$log->debug("update state : " . Dumper($state));
1114 my $last_dirname = "///";
1116 # foreach file specified on the command line ...
1117 foreach my $filename ( @{$state->{args}} )
1119 $filename = filecleanup($filename);
1121 $log->debug("Processing file $filename");
1123 unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
1125 my $cur_dirname = dirname($filename);
1126 if ( $cur_dirname ne $last_dirname )
1128 $last_dirname = $cur_dirname;
1129 if ( $cur_dirname eq "" )
1133 print "E cvs update: Updating $cur_dirname\n";
1137 # if we have a -C we should pretend we never saw modified stuff
1138 if ( exists ( $state->{opt}{C} ) )
1140 delete $state->{entries}{$filename}{modified_hash};
1141 delete $state->{entries}{$filename}{modified_filename};
1142 $state->{entries}{$filename}{unchanged} = 1;
1146 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
1148 $meta = $updater->getmeta($filename, $1);
1150 $meta = $updater->getmeta($filename);
1153 # If -p was given, "print" the contents of the requested revision.
1154 if ( exists ( $state->{opt}{p} ) ) {
1155 if ( defined ( $meta->{revision} ) ) {
1156 $log->info("Printing '$filename' revision " . $meta->{revision});
1158 transmitfile($meta->{filehash}, { print => 1 });
1164 if ( ! defined $meta )
1173 my $oldmeta = $meta;
1175 my $wrev = revparse($filename);
1177 # If the working copy is an old revision, lets get that version too for comparison.
1178 if ( defined($wrev) and $wrev ne $meta->{revision} )
1180 $oldmeta = $updater->getmeta($filename, $wrev);
1183 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
1185 # Files are up to date if the working copy and repo copy have the same revision,
1186 # and the working copy is unmodified _and_ the user hasn't specified -C
1187 next if ( defined ( $wrev )
1188 and defined($meta->{revision})
1189 and $wrev eq $meta->{revision}
1190 and $state->{entries}{$filename}{unchanged}
1191 and not exists ( $state->{opt}{C} ) );
1193 # If the working copy and repo copy have the same revision,
1194 # but the working copy is modified, tell the client it's modified
1195 if ( defined ( $wrev )
1196 and defined($meta->{revision})
1197 and $wrev eq $meta->{revision}
1198 and defined($state->{entries}{$filename}{modified_hash})
1199 and not exists ( $state->{opt}{C} ) )
1201 $log->info("Tell the client the file is modified");
1202 print "MT text M \n";
1203 print "MT fname $filename\n";
1204 print "MT newline\n";
1208 if ( $meta->{filehash} eq "deleted" )
1210 # TODO: If it has been modified in the sandbox, error out
1211 # with the appropriate message, rather than deleting a modified
1214 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1216 $log->info("Removing '$filename' from working copy (no longer in the repo)");
1218 print "E cvs update: `$filename' is no longer in the repository\n";
1219 # Don't want to actually _DO_ the update if -n specified
1220 unless ( $state->{globaloptions}{-n} ) {
1221 print "Removed $dirpart\n";
1222 print "$filepart\n";
1225 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
1226 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1227 or $meta->{filehash} eq 'added' )
1229 # normal update, just send the new revision (either U=Update,
1230 # or A=Add, or R=Remove)
1231 if ( defined($wrev) && ($wrev=~/^-/) )
1233 $log->info("Tell the client the file is scheduled for removal");
1234 print "MT text R \n";
1235 print "MT fname $filename\n";
1236 print "MT newline\n";
1239 elsif ( (!defined($wrev) || $wrev eq '0') &&
1240 (!defined($meta->{revision}) || $meta->{revision} eq '0') )
1242 $log->info("Tell the client the file is scheduled for addition");
1243 print "MT text A \n";
1244 print "MT fname $filename\n";
1245 print "MT newline\n";
1250 $log->info("UpdatingX3 '$filename' to ".$meta->{revision});
1251 print "MT +updated\n";
1252 print "MT text U \n";
1253 print "MT fname $filename\n";
1254 print "MT newline\n";
1255 print "MT -updated\n";
1258 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1260 # Don't want to actually _DO_ the update if -n specified
1261 unless ( $state->{globaloptions}{-n} )
1263 if ( defined ( $wrev ) )
1265 # instruct client we're sending a file to put in this path as a replacement
1266 print "Update-existing $dirpart\n";
1267 $log->debug("Updating existing file 'Update-existing $dirpart'");
1269 # instruct client we're sending a file to put in this path as a new file
1270 print "Clear-static-directory $dirpart\n";
1271 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1272 print "Clear-sticky $dirpart\n";
1273 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1275 $log->debug("Creating new file 'Created $dirpart'");
1276 print "Created $dirpart\n";
1278 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1280 # this is an "entries" line
1281 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1282 $log->debug("/$filepart/$meta->{revision}//$kopts/");
1283 print "/$filepart/$meta->{revision}//$kopts/\n";
1286 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1287 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1290 transmitfile($meta->{filehash});
1293 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
1295 my $mergeDir = setupTmpDir();
1297 my $file_local = $filepart . ".mine";
1298 my $mergedFile = "$mergeDir/$file_local";
1299 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1300 my $file_old = $filepart . "." . $oldmeta->{revision};
1301 transmitfile($oldmeta->{filehash}, { targetfile => $file_old });
1302 my $file_new = $filepart . "." . $meta->{revision};
1303 transmitfile($meta->{filehash}, { targetfile => $file_new });
1305 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1306 $log->info("Merging $file_local, $file_old, $file_new");
1307 print "M Merging differences between $oldmeta->{revision} and $meta->{revision} into $filename\n";
1309 $log->debug("Temporary directory for merge is $mergeDir");
1311 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1318 $log->info("Merged successfully");
1319 print "M M $filename\n";
1320 $log->debug("Merged $dirpart");
1322 # Don't want to actually _DO_ the update if -n specified
1323 unless ( $state->{globaloptions}{-n} )
1325 print "Merged $dirpart\n";
1326 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1327 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1328 my $kopts = kopts_from_path("$dirpart/$filepart",
1329 "file",$mergedFile);
1330 $log->debug("/$filepart/$meta->{revision}//$kopts/");
1331 print "/$filepart/$meta->{revision}//$kopts/\n";
1334 elsif ( $return == 1 )
1336 $log->info("Merged with conflicts");
1337 print "E cvs update: conflicts found in $filename\n";
1338 print "M C $filename\n";
1340 # Don't want to actually _DO_ the update if -n specified
1341 unless ( $state->{globaloptions}{-n} )
1343 print "Merged $dirpart\n";
1344 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1345 my $kopts = kopts_from_path("$dirpart/$filepart",
1346 "file",$mergedFile);
1347 print "/$filepart/$meta->{revision}/+/$kopts/\n";
1352 $log->warn("Merge failed");
1356 # Don't want to actually _DO_ the update if -n specified
1357 unless ( $state->{globaloptions}{-n} )
1360 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1361 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1363 # transmit file, format is single integer on a line by itself (file
1364 # size) followed by the file contents
1365 # TODO : we should copy files in blocks
1366 my $data = `cat $mergedFile`;
1367 $log->debug("File size : " . length($data));
1368 print length($data) . "\n";
1380 my ( $cmd, $data ) = @_;
1384 #$log->debug("State : " . Dumper($state));
1386 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1388 if ( $state->{method} eq 'pserver' and $state->{user} eq 'anonymous' )
1390 print "error 1 anonymous user cannot commit via pserver\n";
1395 if ( -e $state->{CVSROOT} . "/index" )
1397 $log->warn("file 'index' already exists in the git repository");
1398 print "error 1 Index already exists in git repo\n";
1403 # Grab a handle to the SQLite db and do any necessary updates
1404 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1407 # Remember where the head was at the beginning.
1408 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1410 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1411 print "error 1 pserver cannot find the current HEAD of module";
1416 setupWorkTree($parenthash);
1418 $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1420 $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
1422 my @committedfiles = ();
1425 # foreach file specified on the command line ...
1426 foreach my $filename ( @{$state->{args}} )
1428 my $committedfile = $filename;
1429 $filename = filecleanup($filename);
1431 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1433 my $meta = $updater->getmeta($filename);
1434 $oldmeta{$filename} = $meta;
1436 my $wrev = revparse($filename);
1438 my ( $filepart, $dirpart ) = filenamesplit($filename);
1440 # do a checkout of the file if it is part of this tree
1442 system('git', 'checkout-index', '-f', '-u', $filename);
1444 die "Error running git-checkout-index -f -u $filename : $!";
1450 $rmflag = 1 if ( defined($wrev) and ($wrev=~/^-/) );
1451 $addflag = 1 unless ( -e $filename );
1453 # Do up to date checking
1454 unless ( $addflag or $wrev eq $meta->{revision} or
1455 ( $rmflag and $wrev eq "-$meta->{revision}" ) )
1457 # fail everything if an up to date check fails
1458 print "error 1 Up to date check failed for $filename\n";
1463 push @committedfiles, $committedfile;
1464 $log->info("Committing $filename");
1466 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1470 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1471 rename $state->{entries}{$filename}{modified_filename},$filename;
1473 # Calculate modes to remove
1475 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1477 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1478 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1483 $log->info("Removing file '$filename'");
1485 system("git", "update-index", "--remove", $filename);
1489 $log->info("Adding file '$filename'");
1490 system("git", "update-index", "--add", $filename);
1492 $log->info("UpdatingX2 file '$filename'");
1493 system("git", "update-index", $filename);
1497 unless ( scalar(@committedfiles) > 0 )
1499 print "E No files to commit\n";
1505 my $treehash = `git write-tree`;
1508 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1510 # write our commit message out if we have one ...
1511 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1512 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1513 if ( defined ( $cfg->{gitcvs}{commitmsgannotation} ) ) {
1514 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/ ) {
1515 print $msg_fh "\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"
1518 print $msg_fh "\n\nvia git-CVS emulator\n";
1522 my $commithash = `git commit-tree $treehash -p $parenthash < $msg_filename`;
1524 $log->info("Commit hash : $commithash");
1526 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1528 $log->warn("Commit failed (Invalid commit hash)");
1529 print "error 1 Commit failed (unknown reason)\n";
1534 ### Emulate git-receive-pack by running hooks/update
1535 my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1536 $parenthash, $commithash );
1538 unless( system( @hook ) == 0 )
1540 $log->warn("Commit failed (update hook declined to update ref)");
1541 print "error 1 Commit failed (update hook declined)\n";
1548 if (system(qw(git update-ref -m), "cvsserver ci",
1549 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1550 $log->warn("update-ref for $state->{module} failed.");
1551 print "error 1 Cannot commit -- update first\n";
1556 ### Emulate git-receive-pack by running hooks/post-receive
1557 my $hook = $ENV{GIT_DIR}.'hooks/post-receive';
1559 open(my $pipe, "| $hook") || die "can't fork $!";
1561 local $SIG{PIPE} = sub { die 'pipe broke' };
1563 print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
1565 close $pipe || die "bad pipe: $! $?";
1570 ### Then hooks/post-update
1571 $hook = $ENV{GIT_DIR}.'hooks/post-update';
1573 system($hook, "refs/heads/$state->{module}");
1576 # foreach file specified on the command line ...
1577 foreach my $filename ( @committedfiles )
1579 $filename = filecleanup($filename);
1581 my $meta = $updater->getmeta($filename);
1582 unless (defined $meta->{revision}) {
1583 $meta->{revision} = "1.1";
1586 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
1588 $log->debug("Checked-in $dirpart : $filename");
1590 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
1591 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
1593 print "M new revision: delete; previous revision: $oldmeta{$filename}{revision}\n";
1594 print "Remove-entry $dirpart\n";
1595 print "$filename\n";
1597 if ($meta->{revision} eq "1.1") {
1598 print "M initial revision: 1.1\n";
1600 print "M new revision: $meta->{revision}; previous revision: $oldmeta{$filename}{revision}\n";
1602 print "Checked-in $dirpart\n";
1603 print "$filename\n";
1604 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1605 print "/$filepart/$meta->{revision}//$kopts/\n";
1615 my ( $cmd, $data ) = @_;
1619 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1620 #$log->debug("status state : " . Dumper($state));
1622 # Grab a handle to the SQLite db and do any necessary updates
1624 $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1627 # if no files were specified, we need to work out what files we should
1628 # be providing status on ...
1629 argsfromdir($updater);
1631 # foreach file specified on the command line ...
1632 foreach my $filename ( @{$state->{args}} )
1634 $filename = filecleanup($filename);
1636 if ( exists($state->{opt}{l}) &&
1637 index($filename, '/', length($state->{prependdir})) >= 0 )
1642 my $meta = $updater->getmeta($filename);
1643 my $oldmeta = $meta;
1645 my $wrev = revparse($filename);
1647 # If the working copy is an old revision, lets get that
1648 # version too for comparison.
1649 if ( defined($wrev) and $wrev ne $meta->{revision} )
1651 $oldmeta = $updater->getmeta($filename, $wrev);
1654 # TODO : All possible statuses aren't yet implemented
1656 # Files are up to date if the working copy and repo copy have
1657 # the same revision, and the working copy is unmodified
1658 if ( defined ( $wrev ) and defined($meta->{revision}) and
1659 $wrev eq $meta->{revision} and
1660 ( ( $state->{entries}{$filename}{unchanged} and
1661 ( not defined ( $state->{entries}{$filename}{conflict} ) or
1662 $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
1663 ( defined($state->{entries}{$filename}{modified_hash}) and
1664 $state->{entries}{$filename}{modified_hash} eq
1665 $meta->{filehash} ) ) )
1667 $status = "Up-to-date"
1670 # Need checkout if the working copy has a different (usually
1671 # older) revision than the repo copy, and the working copy is
1673 if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
1674 $meta->{revision} ne $wrev and
1675 ( $state->{entries}{$filename}{unchanged} or
1676 ( defined($state->{entries}{$filename}{modified_hash}) and
1677 $state->{entries}{$filename}{modified_hash} eq
1678 $oldmeta->{filehash} ) ) )
1680 $status ||= "Needs Checkout";
1683 # Need checkout if it exists in the repo but doesn't have a working
1685 if ( not defined ( $wrev ) and defined ( $meta->{revision} ) )
1687 $status ||= "Needs Checkout";
1690 # Locally modified if working copy and repo copy have the
1691 # same revision but there are local changes
1692 if ( defined ( $wrev ) and defined($meta->{revision}) and
1693 $wrev eq $meta->{revision} and
1694 $state->{entries}{$filename}{modified_filename} )
1696 $status ||= "Locally Modified";
1699 # Needs Merge if working copy revision is different
1700 # (usually older) than repo copy and there are local changes
1701 if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
1702 $meta->{revision} ne $wrev and
1703 $state->{entries}{$filename}{modified_filename} )
1705 $status ||= "Needs Merge";
1708 if ( defined ( $state->{entries}{$filename}{revision} ) and
1709 not defined ( $meta->{revision} ) )
1711 $status ||= "Locally Added";
1713 if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
1714 $wrev eq "-$meta->{revision}" )
1716 $status ||= "Locally Removed";
1718 if ( defined ( $state->{entries}{$filename}{conflict} ) and
1719 $state->{entries}{$filename}{conflict} =~ /^\+=/ )
1721 $status ||= "Unresolved Conflict";
1725 $status ||= "File had conflicts on merge";
1728 $status ||= "Unknown";
1730 my ($filepart) = filenamesplit($filename);
1732 print "M =======" . ( "=" x 60 ) . "\n";
1733 print "M File: $filepart\tStatus: $status\n";
1734 if ( defined($state->{entries}{$filename}{revision}) )
1736 print "M Working revision:\t" .
1737 $state->{entries}{$filename}{revision} . "\n";
1739 print "M Working revision:\tNo entry for $filename\n";
1741 if ( defined($meta->{revision}) )
1743 print "M Repository revision:\t" .
1745 "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1746 my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
1747 my($tag)=($tagOrDate=~m/^T(.+)$/);
1748 if( !defined($tag) )
1752 print "M Sticky Tag:\t\t$tag\n";
1753 my($date)=($tagOrDate=~m/^D(.+)$/);
1754 if( !defined($date) )
1758 print "M Sticky Date:\t\t$date\n";
1759 my($options)=$state->{entries}{$filename}{options};
1760 if( $options eq "" )
1764 print "M Sticky Options:\t\t$options\n";
1766 print "M Repository revision:\tNo revision control file\n";
1776 my ( $cmd, $data ) = @_;
1780 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1781 #$log->debug("status state : " . Dumper($state));
1783 my ($revision1, $revision2);
1784 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1786 $revision1 = $state->{opt}{r}[0];
1787 $revision2 = $state->{opt}{r}[1];
1789 $revision1 = $state->{opt}{r};
1792 $log->debug("Diffing revisions " .
1793 ( defined($revision1) ? $revision1 : "[NULL]" ) .
1794 " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1796 # Grab a handle to the SQLite db and do any necessary updates
1798 $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1801 # if no files were specified, we need to work out what files we should
1802 # be providing status on ...
1803 argsfromdir($updater);
1805 # foreach file specified on the command line ...
1806 foreach my $filename ( @{$state->{args}} )
1808 $filename = filecleanup($filename);
1810 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1812 my $wrev = revparse($filename);
1814 # We need _something_ to diff against
1815 next unless ( defined ( $wrev ) );
1817 # if we have a -r switch, use it
1818 if ( defined ( $revision1 ) )
1820 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1821 $meta1 = $updater->getmeta($filename, $revision1);
1822 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1824 print "E File $filename at revision $revision1 doesn't exist\n";
1827 transmitfile($meta1->{filehash}, { targetfile => $file1 });
1829 # otherwise we just use the working copy revision
1832 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1833 $meta1 = $updater->getmeta($filename, $wrev);
1834 transmitfile($meta1->{filehash}, { targetfile => $file1 });
1837 # if we have a second -r switch, use it too
1838 if ( defined ( $revision2 ) )
1840 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1841 $meta2 = $updater->getmeta($filename, $revision2);
1843 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1845 print "E File $filename at revision $revision2 doesn't exist\n";
1849 transmitfile($meta2->{filehash}, { targetfile => $file2 });
1851 # otherwise we just use the working copy
1854 $file2 = $state->{entries}{$filename}{modified_filename};
1857 # if we have been given -r, and we don't have a $file2 yet, lets
1859 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1861 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1862 $meta2 = $updater->getmeta($filename, $wrev);
1863 transmitfile($meta2->{filehash}, { targetfile => $file2 });
1866 # We need to have retrieved something useful
1867 next unless ( defined ( $meta1 ) );
1869 # Files to date if the working copy and repo copy have the same
1870 # revision, and the working copy is unmodified
1871 if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
1872 ( ( $state->{entries}{$filename}{unchanged} and
1873 ( not defined ( $state->{entries}{$filename}{conflict} ) or
1874 $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
1875 ( defined($state->{entries}{$filename}{modified_hash}) and
1876 $state->{entries}{$filename}{modified_hash} eq
1877 $meta1->{filehash} ) ) )
1882 # Apparently we only show diffs for locally modified files
1883 unless ( defined($meta2) or
1884 defined ( $state->{entries}{$filename}{modified_filename} ) )
1889 print "M Index: $filename\n";
1890 print "M =======" . ( "=" x 60 ) . "\n";
1891 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1892 if ( defined ( $meta1 ) )
1894 print "M retrieving revision $meta1->{revision}\n"
1896 if ( defined ( $meta2 ) )
1898 print "M retrieving revision $meta2->{revision}\n"
1901 foreach my $opt ( keys %{$state->{opt}} )
1903 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1905 foreach my $value ( @{$state->{opt}{$opt}} )
1907 print "-$opt $value ";
1911 if ( defined ( $state->{opt}{$opt} ) )
1913 print "$state->{opt}{$opt} "
1917 print "$filename\n";
1919 $log->info("Diffing $filename -r $meta1->{revision} -r " .
1920 ( $meta2->{revision} or "workingcopy" ));
1922 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1924 if ( exists $state->{opt}{u} )
1926 system("diff -u -L '$filename revision $meta1->{revision}'" .
1928 ( defined($meta2->{revision}) ?
1929 "revision $meta2->{revision}" :
1931 "' $file1 $file2 > $filediff" );
1933 system("diff $file1 $file2 > $filediff");
1948 my ( $cmd, $data ) = @_;
1952 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1953 #$log->debug("log state : " . Dumper($state));
1956 if ( defined ( $state->{opt}{r} ) )
1958 $revFilter = $state->{opt}{r};
1961 # Grab a handle to the SQLite db and do any necessary updates
1963 $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1966 # if no files were specified, we need to work out what files we
1967 # should be providing status on ...
1968 argsfromdir($updater);
1970 # foreach file specified on the command line ...
1971 foreach my $filename ( @{$state->{args}} )
1973 $filename = filecleanup($filename);
1975 my $headmeta = $updater->getmeta($filename);
1977 my ($revisions,$totalrevisions) = $updater->getlog($filename,
1980 next unless ( scalar(@$revisions) );
1983 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1984 print "M Working file: $filename\n";
1985 print "M head: $headmeta->{revision}\n";
1986 print "M branch:\n";
1987 print "M locks: strict\n";
1988 print "M access list:\n";
1989 print "M symbolic names:\n";
1990 print "M keyword substitution: kv\n";
1991 print "M total revisions: $totalrevisions;\tselected revisions: " .
1992 scalar(@$revisions) . "\n";
1993 print "M description:\n";
1995 foreach my $revision ( @$revisions )
1997 print "M ----------------------------\n";
1998 print "M revision $revision->{revision}\n";
1999 # reformat the date for log output
2000 if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and
2001 defined($DATE_LIST->{$2}) )
2003 $revision->{modified} = sprintf('%04d/%02d/%02d %s',
2004 $3, $DATE_LIST->{$2}, $1, $4 );
2006 $revision->{author} = cvs_author($revision->{author});
2007 print "M date: $revision->{modified};" .
2008 " author: $revision->{author}; state: " .
2009 ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) .
2012 $commitmessage = $updater->commitmessage($revision->{commithash});
2013 $commitmessage =~ s/^/M /mg;
2014 print $commitmessage . "\n";
2016 print "M =======" . ( "=" x 70 ) . "\n";
2024 my ( $cmd, $data ) = @_;
2026 argsplit("annotate");
2028 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
2029 #$log->debug("status state : " . Dumper($state));
2031 # Grab a handle to the SQLite db and do any necessary updates
2032 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
2035 # if no files were specified, we need to work out what files we should be providing annotate on ...
2036 argsfromdir($updater);
2038 # we'll need a temporary checkout dir
2041 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");
2043 # foreach file specified on the command line ...
2044 foreach my $filename ( @{$state->{args}} )
2046 $filename = filecleanup($filename);
2048 my $meta = $updater->getmeta($filename);
2050 next unless ( $meta->{revision} );
2052 # get all the commits that this file was in
2053 # in dense format -- aka skip dead revisions
2054 my $revisions = $updater->gethistorydense($filename);
2055 my $lastseenin = $revisions->[0][2];
2057 # populate the temporary index based on the latest commit were we saw
2058 # the file -- but do it cheaply without checking out any files
2059 # TODO: if we got a revision from the client, use that instead
2060 # to look up the commithash in sqlite (still good to default to
2061 # the current head as we do now)
2062 system("git", "read-tree", $lastseenin);
2065 print "E error running git-read-tree $lastseenin $ENV{GIT_INDEX_FILE} $!\n";
2068 $log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit $lastseenin - exit status $?");
2070 # do a checkout of the file
2071 system('git', 'checkout-index', '-f', '-u', $filename);
2073 print "E error running git-checkout-index -f -u $filename : $!\n";
2077 $log->info("Annotate $filename");
2079 # Prepare a file with the commits from the linearized
2080 # history that annotate should know about. This prevents
2081 # git-jsannotate telling us about commits we are hiding
2084 my $a_hints = "$work->{workDir}/.annotate_hints";
2085 if (!open(ANNOTATEHINTS, '>', $a_hints)) {
2086 print "E failed to open '$a_hints' for writing: $!\n";
2089 for (my $i=0; $i < @$revisions; $i++)
2091 print ANNOTATEHINTS $revisions->[$i][2];
2092 if ($i+1 < @$revisions) { # have we got a parent?
2093 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
2095 print ANNOTATEHINTS "\n";
2098 print ANNOTATEHINTS "\n";
2100 or (print "E failed to write $a_hints: $!\n"), return;
2102 my @cmd = (qw(git annotate -l -S), $a_hints, $filename);
2103 if (!open(ANNOTATE, "-|", @cmd)) {
2104 print "E error invoking ". join(' ',@cmd) .": $!\n";
2108 print "E Annotations for $filename\n";
2109 print "E ***************\n";
2110 while ( <ANNOTATE> )
2112 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
2114 my $commithash = $1;
2116 unless ( defined ( $metadata->{$commithash} ) )
2118 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
2119 $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
2120 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
2122 printf("M %-7s (%-8s %10s): %s\n",
2123 $metadata->{$commithash}{revision},
2124 $metadata->{$commithash}{author},
2125 $metadata->{$commithash}{modified},
2129 $log->warn("Error in annotate output! LINE: $_");
2130 print "E Annotate error \n";
2137 # done; get out of the tempdir
2144 # This method takes the state->{arguments} array and produces two new arrays.
2145 # The first is $state->{args} which is everything before the '--' argument, and
2146 # the second is $state->{files} which is everything after it.
2149 $state->{args} = [];
2150 $state->{files} = [];
2153 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
2157 if ( defined($type) )
2160 $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" );
2161 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
2162 $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" );
2163 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
2164 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
2165 $opt = { k => 1, m => 1 } if ( $type eq "add" );
2166 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
2167 $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" );
2170 while ( scalar ( @{$state->{arguments}} ) > 0 )
2172 my $arg = shift @{$state->{arguments}};
2174 next if ( $arg eq "--" );
2175 next unless ( $arg =~ /\S/ );
2177 # if the argument looks like a switch
2178 if ( $arg =~ /^-(\w)(.*)/ )
2180 # if it's a switch that takes an argument
2183 # If this switch has already been provided
2184 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
2186 $state->{opt}{$1} = [ $state->{opt}{$1} ];
2187 if ( length($2) > 0 )
2189 push @{$state->{opt}{$1}},$2;
2191 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
2194 # if there's extra data in the arg, use that as the argument for the switch
2195 if ( length($2) > 0 )
2197 $state->{opt}{$1} = $2;
2199 $state->{opt}{$1} = shift @{$state->{arguments}};
2203 $state->{opt}{$1} = undef;
2208 push @{$state->{args}}, $arg;
2216 foreach my $value ( @{$state->{arguments}} )
2218 if ( $value eq "--" )
2223 push @{$state->{args}}, $value if ( $mode == 0 );
2224 push @{$state->{files}}, $value if ( $mode == 1 );
2229 # Used by argsfromdir
2232 my ($updater,$outNameMap,$outDirMap,$path,$isDir) = @_;
2234 my $fullPath = filecleanup($path);
2236 # Is it a directory?
2237 if( defined($state->{dirMap}{$fullPath}) ||
2238 defined($state->{dirMap}{"$fullPath/"}) )
2240 # It is a directory in the user's sandbox.
2243 if(defined($state->{entries}{$fullPath}))
2245 $log->fatal("Inconsistent file/dir type");
2246 die "Inconsistent file/dir type";
2249 elsif(defined($state->{entries}{$fullPath}))
2251 # It is a file in the user's sandbox.
2254 my($revDirMap,$otherRevDirMap);
2255 if(!defined($isDir) || $isDir)
2257 # Resolve version tree for sticky tag:
2258 # (for now we only want list of files for the version, not
2259 # particular versions of those files: assume it is a directory
2260 # for the moment; ignore Entry's stick tag)
2262 # Order of precedence of sticky tags:
2265 # [file entry sticky tag, but that is only relevant to files]
2266 # [the tag specified in dir req_Sticky]
2267 # [the tag specified in a parent dir req_Sticky]
2269 # Also, -r may appear twice (for diff).
2271 # FUTURE: When/if -j (merges) are supported, we also
2272 # need to add relevant files from one or two
2273 # versions specified with -j.
2275 if(exists($state->{opt}{A}))
2277 $revDirMap=$updater->getRevisionDirMap();
2279 elsif( defined($state->{opt}{r}) and
2280 ref $state->{opt}{r} eq "ARRAY" )
2282 $revDirMap=$updater->getRevisionDirMap($state->{opt}{r}[0]);
2283 $otherRevDirMap=$updater->getRevisionDirMap($state->{opt}{r}[1]);
2285 elsif(defined($state->{opt}{r}))
2287 $revDirMap=$updater->getRevisionDirMap($state->{opt}{r});
2291 my($sticky)=getDirStickyInfo($fullPath);
2292 $revDirMap=$updater->getRevisionDirMap($sticky->{tag});
2295 # Is it a directory?
2296 if( defined($revDirMap->{$fullPath}) ||
2297 defined($otherRevDirMap->{$fullPath}) )
2303 # What to do with it?
2306 $outNameMap->{$fullPath}=1;
2310 $outDirMap->{$fullPath}=1;
2312 if(defined($revDirMap->{$fullPath}))
2314 addDirMapFiles($updater,$outNameMap,$outDirMap,
2315 $revDirMap->{$fullPath});
2317 if( defined($otherRevDirMap) &&
2318 defined($otherRevDirMap->{$fullPath}) )
2320 addDirMapFiles($updater,$outNameMap,$outDirMap,
2321 $otherRevDirMap->{$fullPath});
2326 # Used by argsfromdir
2327 # Add entries from dirMap to outNameMap. Also recurse into entries
2328 # that are subdirectories.
2331 my($updater,$outNameMap,$outDirMap,$dirMap)=@_;
2334 foreach $fullName (keys(%$dirMap))
2336 my $cleanName=$fullName;
2337 if(defined($state->{prependdir}))
2339 if(!($cleanName=~s/^\Q$state->{prependdir}\E//))
2341 $log->fatal("internal error stripping prependdir");
2342 die "internal error stripping prependdir";
2346 if($dirMap->{$fullName} eq "F")
2348 $outNameMap->{$cleanName}=1;
2350 elsif($dirMap->{$fullName} eq "D")
2352 if(!$state->{opt}{l})
2354 expandArg($updater,$outNameMap,$outDirMap,$cleanName,1);
2359 $log->fatal("internal error in addDirMapFiles");
2360 die "internal error in addDirMapFiles";
2365 # This method replaces $state->{args} with a directory-expanded
2366 # list of all relevant filenames (recursively unless -d), based
2367 # on $state->{entries}, and the "current" list of files in
2368 # each directory. "Current" files as determined by
2369 # either the requested (-r/-A) or "req_Sticky" version of
2371 # Both the input args and the new output args are relative
2372 # to the cvs-client's CWD, although some of the internal
2373 # computations are relative to the top of the project.
2376 my $updater = shift;
2378 # Notes about requirements for specific callers:
2379 # update # "standard" case (entries; a single -r/-A/default; -l)
2380 # # Special case: -d for create missing directories.
2381 # diff # 0 or 1 -r's: "standard" case.
2382 # # 2 -r's: We could ignore entries (just use the two -r's),
2383 # # but it doesn't really matter.
2384 # annotate # "standard" case
2385 # log # Punting: log -r has a more complex non-"standard"
2386 # # meaning, and we don't currently try to support log'ing
2387 # # branches at all (need a lot of work to
2388 # # support CVS-consistent branch relative version
2390 #HERE: But we still want to expand directories. Maybe we should
2391 # essentially force "-A".
2392 # status # "standard", except that -r/-A/default are not possible.
2393 # # Mostly only used to expand entries only)
2395 # Don't use argsfromdir at all:
2396 # add # Explicit arguments required. Directory args imply add
2397 # # the directory itself, not the files in it.
2398 # co # Obtain list directly.
2399 # remove # HERE: TEST: MAYBE client does the recursion for us,
2400 # # since it only makes sense to remove stuff already in
2402 # ci # HERE: Similar to remove...
2403 # # Don't try to implement the confusing/weird
2404 # # ci -r bug er.."feature".
2406 if(scalar(@{$state->{args}})==0)
2408 $state->{args} = [ "." ];
2412 for my $file (@{$state->{args}})
2414 expandArg($updater,\%allArgs,\%allDirs,$file);
2417 # Include any entries from sandbox. Generally client won't
2418 # send entries that shouldn't be used.
2419 foreach my $file (keys %{$state->{entries}})
2421 $allArgs{remove_prependdir($file)} = 1;
2424 $state->{dirArgs} = \%allDirs;
2427 # Sort priority: by directory depth, then actual file name:
2428 my @piecesA=split('/',$a);
2429 my @piecesB=split('/',$b);
2431 my $count=scalar(@piecesA);
2432 my $tmp=scalar(@piecesB);
2433 return $count<=>$tmp if($count!=$tmp);
2435 for($tmp=0;$tmp<$count;$tmp++)
2437 if($piecesA[$tmp] ne $piecesB[$tmp])
2439 return $piecesA[$tmp] cmp $piecesB[$tmp]
2446 ## look up directory sticky tag, of either fullPath or a parent:
2447 sub getDirStickyInfo
2452 while($fullPath ne "" && !defined($state->{dirMap}{"$fullPath/"}))
2454 $fullPath=~s%/?[^/]*$%%;
2457 if( !defined($state->{dirMap}{"$fullPath/"}) &&
2458 ( $fullPath eq "" ||
2459 $fullPath eq "." ) )
2461 return $state->{dirMap}{""}{stickyInfo};
2465 return $state->{dirMap}{"$fullPath/"}{stickyInfo};
2469 # Resolve precedence of various ways of specifying which version of
2470 # a file you want. Returns undef (for default head), or a ref to a hash
2471 # that contains "tag" and/or "date" keys.
2472 sub resolveStickyInfo
2474 my($filename,$stickyTag,$stickyDate,$reset) = @_;
2476 # Order of precedence of sticky tags:
2479 # [file entry sticky tag]
2480 # [the tag specified in dir req_Sticky]
2481 # [the tag specified in a parent dir req_Sticky]
2489 elsif( defined($stickyTag) && $stickyTag ne "" )
2490 # || ( defined($stickyDate) && $stickyDate ne "" ) # TODO
2492 $result={ 'tag' => (defined($stickyTag)?$stickyTag:undef) };
2494 # TODO: Convert -D value into the form 2011.04.10.04.46.57,
2495 # similar to an entry line's sticky date, without the D prefix.
2496 # It sometimes (always?) arrives as something more like
2497 # '10 Apr 2011 04:46:57 -0000'...
2498 # $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
2500 elsif( defined($state->{entries}{$filename}) &&
2501 defined($state->{entries}{$filename}{tag_or_date}) &&
2502 $state->{entries}{$filename}{tag_or_date} ne "" )
2504 my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
2505 if($tagOrDate=~/^T([^ ]+)\s*$/)
2507 $result = { 'tag' => $1 };
2509 elsif($tagOrDate=~/^D([0-9.]+)\s*$/)
2511 $result= { 'date' => $1 };
2515 die "Unknown tag_or_date format\n";
2520 $result=getDirStickyInfo($filename);
2526 # Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into
2527 # a form appropriate for the sticky tag field of an Entries
2528 # line (field index 5, 0-based).
2529 sub getStickyTagOrDate
2534 if(defined($stickyInfo) && defined($stickyInfo->{tag}))
2536 $result="T$stickyInfo->{tag}";
2538 # TODO: When/if we actually pick versions by {date} properly,
2539 # also handle it here:
2540 # "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").
2549 # This method cleans up the $state variable after a command that uses arguments has run
2552 $state->{files} = [];
2553 $state->{dirArgs} = {};
2554 $state->{args} = [];
2555 $state->{arguments} = [];
2556 $state->{entries} = {};
2557 $state->{dirMap} = {};
2560 # Return working directory CVS revision "1.X" out
2561 # of the the working directory "entries" state, for the given filename.
2562 # This is prefixed with a dash if the file is scheduled for removal
2563 # when it is committed.
2566 my $filename = shift;
2568 return $state->{entries}{$filename}{revision};
2571 # This method takes a file hash and does a CVS "file transfer". Its
2572 # exact behaviour depends on a second, optional hash table argument:
2573 # - If $options->{targetfile}, dump the contents to that file;
2574 # - If $options->{print}, use M/MT to transmit the contents one line
2576 # - Otherwise, transmit the size of the file, followed by the file
2580 my $filehash = shift;
2581 my $options = shift;
2583 if ( defined ( $filehash ) and $filehash eq "deleted" )
2585 $log->warn("filehash is 'deleted'");
2589 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2591 my $type = `git cat-file -t $filehash`;
2594 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2596 my $size = `git cat-file -s $filehash`;
2599 $log->debug("transmitfile($filehash) size=$size, type=$type");
2601 if ( open my $fh, '-|', "git", "cat-file", "blob", $filehash )
2603 if ( defined ( $options->{targetfile} ) )
2605 my $targetfile = $options->{targetfile};
2606 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2607 print NEWFILE $_ while ( <$fh> );
2608 close NEWFILE or die("Failed to write '$targetfile': $!");
2609 } elsif ( defined ( $options->{print} ) && $options->{print} ) {
2614 print 'MT text ', $_, "\n";
2619 print while ( <$fh> );
2621 close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
2623 die("Couldn't execute git-cat-file");
2627 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2628 # refers to the directory portion and the file portion of the filename
2632 my $filename = shift;
2633 my $fixforlocaldir = shift;
2635 my ( $filepart, $dirpart ) = ( $filename, "." );
2636 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2639 if ( $fixforlocaldir )
2641 $dirpart =~ s/^$state->{prependdir}//;
2644 return ( $filepart, $dirpart );
2647 # Cleanup various junk in filename (try to canonicalize it), and
2648 # add prependdir to accomodate running CVS client from a
2649 # subdirectory (so the output is relative to top directory of the project).
2652 my $filename = shift;
2654 return undef unless(defined($filename));
2655 if ( $filename =~ /^\// )
2657 print "E absolute filenames '$filename' not supported by server\n";
2661 if($filename eq ".")
2665 $filename =~ s/^\.\///g;
2666 $filename =~ s%/+%/%g;
2667 $filename = $state->{prependdir} . $filename;
2668 $filename =~ s%/$%%;
2672 # Remove prependdir from the path, so that is is relative to the directory
2673 # the CVS client was started from, rather than the top of the project.
2674 # Essentially the inverse of filecleanup().
2675 sub remove_prependdir
2678 if(defined($state->{prependdir}) && $state->{prependdir} ne "")
2680 my($pre)=$state->{prependdir};
2682 if(!($path=~s%^\Q$pre\E/?%%))
2684 $log->fatal("internal error missing prependdir");
2685 die("internal error missing prependdir");
2693 if( !defined($state->{CVSROOT}) )
2695 print "error 1 CVSROOT not specified\n";
2699 if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') )
2701 print "error 1 Internally inconsistent CVSROOT\n";
2707 # Setup working directory in a work tree with the requested version
2708 # loaded in the index.
2715 if( ( defined($work->{state}) && $work->{state} != 1 ) ||
2716 defined($work->{tmpDir}) )
2718 $log->warn("Bad work tree state management");
2719 print "error 1 Internal setup multiple work trees without cleanup\n";
2724 $work->{workDir} = tempdir ( DIR => $TEMP_DIR );
2726 if( !defined($work->{index}) )
2728 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2731 chdir $work->{workDir} or
2732 die "Unable to chdir to $work->{workDir}\n";
2734 $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");
2736 $ENV{GIT_WORK_TREE} = ".";
2737 $ENV{GIT_INDEX_FILE} = $work->{index};
2742 system("git","read-tree",$ver);
2745 $log->warn("Error running git-read-tree");
2746 die "Error running git-read-tree $ver in $work->{workDir} $!\n";
2749 # else # req_annotate reads tree for each file
2752 # Ensure current directory is in some kind of working directory,
2753 # with a recent version loaded in the index.
2756 if( defined($work->{tmpDir}) )
2758 $log->warn("Bad work tree state management [ensureWorkTree()]");
2759 print "error 1 Internal setup multiple dirs without cleanup\n";
2763 if( $work->{state} )
2770 if( !defined($work->{emptyDir}) )
2772 $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0);
2774 chdir $work->{emptyDir} or
2775 die "Unable to chdir to $work->{emptyDir}\n";
2777 my $ver = `git show-ref -s refs/heads/$state->{module}`;
2779 if ($ver !~ /^[0-9a-f]{40}$/)
2781 $log->warn("Error from git show-ref -s refs/head$state->{module}");
2782 print "error 1 cannot find the current HEAD of module";
2787 if( !defined($work->{index}) )
2789 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2792 $ENV{GIT_WORK_TREE} = ".";
2793 $ENV{GIT_INDEX_FILE} = $work->{index};
2796 system("git","read-tree",$ver);
2799 die "Error running git-read-tree $ver $!\n";
2803 # Cleanup working directory that is not needed any longer.
2806 if( ! $work->{state} )
2811 chdir "/" or die "Unable to chdir '/'\n";
2813 if( defined($work->{workDir}) )
2815 rmtree( $work->{workDir} );
2816 undef $work->{workDir};
2818 undef $work->{state};
2821 # Setup a temporary directory (not a working tree), typically for
2822 # merging dirty state as in req_update.
2825 $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR );
2826 chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n";
2828 return $work->{tmpDir};
2831 # Clean up a previously setupTmpDir. Restore previous work tree if
2835 if ( !defined($work->{tmpDir}) )
2837 $log->warn("cleanup tmpdir that has not been setup");
2838 die "Cleanup tmpDir that has not been setup\n";
2840 if( defined($work->{state}) )
2842 if( $work->{state} == 1 )
2844 chdir $work->{emptyDir} or
2845 die "Unable to chdir to $work->{emptyDir}\n";
2847 elsif( $work->{state} == 2 )
2849 chdir $work->{workDir} or
2850 die "Unable to chdir to $work->{emptyDir}\n";
2854 $log->warn("Inconsistent work dir state");
2855 die "Inconsistent work dir state\n";
2860 chdir "/" or die "Unable to chdir '/'\n";
2864 # Given a path, this function returns a string containing the kopts
2865 # that should go into that path's Entries line. For example, a binary
2866 # file should get -kb.
2869 my ($path, $srcType, $name) = @_;
2871 if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and
2872 $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i )
2874 my ($val) = check_attr( "text", $path );
2875 if ( $val eq "unspecified" )
2877 $val = check_attr( "crlf", $path );
2879 if ( $val eq "unset" )
2883 elsif ( check_attr( "eol", $path ) ne "unspecified" ||
2884 $val eq "set" || $val eq "input" )
2890 $log->info("Unrecognized check_attr crlf $path : $val");
2894 if ( defined ( $cfg->{gitcvs}{allbinary} ) )
2896 if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) )
2900 elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
2902 if( is_binary($srcType,$name) )
2904 $log->debug("... as binary");
2909 $log->debug("... as text");
2913 # Return "" to give no special treatment to any path
2919 my ($attr,$path) = @_;
2921 if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path )
2925 $val =~ s/.*: ([^:\r\n]*)\s*$/$1/;
2934 # This should have the same heuristics as convert.c:is_binary() and related.
2935 # Note that the bare CR test is done by callers in convert.c.
2938 my ($srcType,$name) = @_;
2939 $log->debug("is_binary($srcType,$name)");
2941 # Minimize amount of interpreted code run in the inner per-character
2942 # loop for large files, by totalling each character value and
2943 # then analyzing the totals.
2946 for($i=0;$i<256;$i++)
2951 my $fh = open_blob_or_die($srcType,$name);
2953 while( defined($line=<$fh>) )
2955 # Any '\0' and bare CR are considered binary.
2956 if( $line =~ /\0|(\r[^\n])/ )
2962 # Count up each character in the line:
2963 my $len=length($line);
2964 for($i=0;$i<$len;$i++)
2966 $counts[ord(substr($line,$i,1))]++;
2971 # Don't count CR and LF as either printable/nonprintable
2972 $counts[ord("\n")]=0;
2973 $counts[ord("\r")]=0;
2975 # Categorize individual character count into printable and nonprintable:
2978 for($i=0;$i<256;$i++)
2986 $nonprintable+=$counts[$i];
2988 elsif( $i==127 ) # DEL
2990 $nonprintable+=$counts[$i];
2994 $printable+=$counts[$i];
2998 return ($printable >> 7) < $nonprintable;
3001 # Returns open file handle. Possible invocations:
3002 # - open_blob_or_die("file",$filename);
3003 # - open_blob_or_die("sha1",$filehash);
3004 sub open_blob_or_die
3006 my ($srcType,$name) = @_;
3008 if( $srcType eq "file" )
3010 if( !open $fh,"<",$name )
3012 $log->warn("Unable to open file $name: $!");
3013 die "Unable to open file $name: $!\n";
3016 elsif( $srcType eq "sha1" )
3018 unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
3020 $log->warn("Need filehash");
3021 die "Need filehash\n";
3024 my $type = `git cat-file -t $name`;
3027 unless ( defined ( $type ) and $type eq "blob" )
3029 $log->warn("Invalid type '$type' for '$name'");
3030 die ( "Invalid type '$type' (expected 'blob')" )
3033 my $size = `git cat-file -s $name`;
3036 $log->debug("open_blob_or_die($name) size=$size, type=$type");
3038 unless( open $fh, '-|', "git", "cat-file", "blob", $name )
3040 $log->warn("Unable to open sha1 $name");
3041 die "Unable to open sha1 $name\n";
3046 $log->warn("Unknown type of blob source: $srcType");
3047 die "Unknown type of blob source: $srcType\n";
3052 # Generate a CVS author name from Git author information, by taking the local
3053 # part of the email address and replacing characters not in the Portable
3054 # Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS
3055 # Login names are Unix login names, which should be restricted to this
3059 my $author_line = shift;
3060 (my $author) = $author_line =~ /<([^@>]*)/;
3062 $author =~ s/[^-a-zA-Z0-9_.]/_/g;
3071 # This table is from src/scramble.c in the CVS source
3073 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
3074 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
3075 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87,
3076 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105,
3077 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35,
3078 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56,
3079 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48,
3080 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223,
3081 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,
3082 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,
3083 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,
3084 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,
3085 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,
3086 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,
3087 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,
3088 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152
3092 # This should never happen, the same password format (A) has been
3093 # used by CVS since the beginning of time
3095 my $fmt = substr($str, 0, 1);
3096 die "invalid password format `$fmt'" unless $fmt eq 'A';
3099 my @str = unpack "C*", substr($str, 1);
3100 my $ret = join '', map { chr $SHIFTS[$_] } @str;
3105 package GITCVS::log;
3108 #### Copyright The Open University UK - 2006.
3110 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
3111 #### Martin Langhoff <martin@laptop.org>
3124 This module provides very crude logging with a similar interface to
3133 Creates a new log object, optionally you can specify a filename here to
3134 indicate the file to log to. If no log file is specified, you can specify one
3135 later with method setfile, or indicate you no longer want logging with method
3138 Until one of these methods is called, all log calls will buffer messages ready
3145 my $filename = shift;
3149 bless $self, $class;
3151 if ( defined ( $filename ) )
3153 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
3161 This methods takes a filename, and attempts to open that file as the log file.
3162 If successful, all buffered data is written out to the file, and any further
3163 logging is written directly to the file.
3169 my $filename = shift;
3171 if ( defined ( $filename ) )
3173 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
3176 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
3178 while ( my $line = shift @{$self->{buffer}} )
3180 print {$self->{fh}} $line;
3186 This method indicates no logging is going to be used. It flushes any entries in
3187 the internal buffer, and sets a flag to ensure no further data is put there.
3196 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
3198 $self->{buffer} = [];
3203 Internal method. Returns true if the log file is open, false otherwise.
3210 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
3214 =head2 debug info warn fatal
3216 These four methods are wrappers to _log. They provide the actual interface for
3220 sub debug { my $self = shift; $self->_log("debug", @_); }
3221 sub info { my $self = shift; $self->_log("info" , @_); }
3222 sub warn { my $self = shift; $self->_log("warn" , @_); }
3223 sub fatal { my $self = shift; $self->_log("fatal", @_); }
3227 This is an internal method called by the logging functions. It generates a
3228 timestamp and pushes the logged line either to file, or internal buffer.
3236 return if ( $self->{nolog} );
3238 my @time = localtime;
3239 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
3249 if ( $self->_logopen )
3251 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
3253 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
3259 This method simply closes the file handle if one is open
3266 if ( $self->_logopen )
3272 package GITCVS::updater;
3275 #### Copyright The Open University UK - 2006.
3277 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
3278 #### Martin Langhoff <martin@laptop.org>
3300 die "Need to specify a git repository" unless ( defined($config) and -d $config );
3301 die "Need to specify a module" unless ( defined($module) );
3303 $class = ref($class) || $class;
3307 bless $self, $class;
3309 $self->{valid_tables} = {'revision' => 1,
3310 'revision_ix1' => 1,
3311 'revision_ix2' => 1,
3317 $self->{module} = $module;
3318 $self->{git_path} = $config . "/";
3320 $self->{log} = $log;
3322 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
3324 # Stores full sha1's for various branch/tag names, abbreviations, etc:
3325 $self->{commitRefCache} = {};
3327 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
3328 $cfg->{gitcvs}{dbdriver} || "SQLite";
3329 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
3330 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
3331 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
3332 $cfg->{gitcvs}{dbuser} || "";
3333 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
3334 $cfg->{gitcvs}{dbpass} || "";
3335 $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||
3336 $cfg->{gitcvs}{dbtablenameprefix} || "";
3337 my %mapping = ( m => $module,
3338 a => $state->{method},
3339 u => getlogin || getpwuid($<) || $<,
3340 G => $self->{git_path},
3341 g => mangle_dirname($self->{git_path}),
3343 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
3344 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
3345 $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg;
3346 $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});
3348 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
3349 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
3350 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
3353 die "Error connecting to database\n" unless defined $self->{dbh};
3355 $self->{tables} = {};
3356 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
3358 $self->{tables}{$table} = 1;
3361 # Construct the revision table if required
3362 # The revision table stores an entry for each file, each time that file
3364 # numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )
3365 # This is not sufficient to support "-r {commithash}" for any
3366 # files except files that were modified by that commit (also,
3367 # some places in the code ignore/effectively strip out -r in
3368 # some cases, before it gets passed to getmeta()).
3369 # The "filehash" field typically has a git blob hash, but can also
3370 # be set to "dead" to indicate that the given version of the file
3371 # should not exist in the sandbox.
3372 unless ( $self->{tables}{$self->tablename("revision")} )
3374 my $tablename = $self->tablename("revision");
3375 my $ix1name = $self->tablename("revision_ix1");
3376 my $ix2name = $self->tablename("revision_ix2");
3378 CREATE TABLE $tablename (
3380 revision INTEGER NOT NULL,
3381 filehash TEXT NOT NULL,
3382 commithash TEXT NOT NULL,
3383 author TEXT NOT NULL,
3384 modified TEXT NOT NULL,
3389 CREATE INDEX $ix1name
3390 ON $tablename (name,revision)
3393 CREATE INDEX $ix2name
3394 ON $tablename (name,commithash)
3398 # Construct the head table if required
3399 # The head table (along with the "last_commit" entry in the property
3400 # table) is the persisted working state of the "sub update" subroutine.
3401 # All of it's data is read entirely first, and completely recreated
3402 # last, every time "sub update" runs.
3403 # This is also used by "sub getmeta" when it is asked for the latest
3404 # version of a file (as opposed to some specific version).
3405 # Another way of thinking about it is as a single slice out of
3406 # "revisions", giving just the most recent revision information for
3408 unless ( $self->{tables}{$self->tablename("head")} )
3410 my $tablename = $self->tablename("head");
3411 my $ix1name = $self->tablename("head_ix1");
3413 CREATE TABLE $tablename (
3415 revision INTEGER NOT NULL,
3416 filehash TEXT NOT NULL,
3417 commithash TEXT NOT NULL,
3418 author TEXT NOT NULL,
3419 modified TEXT NOT NULL,
3424 CREATE INDEX $ix1name
3425 ON $tablename (name)
3429 # Construct the properties table if required
3430 # - "last_commit" - Used by "sub update".
3431 unless ( $self->{tables}{$self->tablename("properties")} )
3433 my $tablename = $self->tablename("properties");
3435 CREATE TABLE $tablename (
3436 key TEXT NOT NULL PRIMARY KEY,
3442 # Construct the commitmsgs table if required
3443 # The commitmsgs table is only used for merge commits, since
3444 # "sub update" will only keep one branch of parents. Shortlogs
3445 # for ignored commits (i.e. not on the chosen branch) will be used
3446 # to construct a replacement "collapsed" merge commit message,
3447 # which will be stored in this table. See also "sub commitmessage".
3448 unless ( $self->{tables}{$self->tablename("commitmsgs")} )
3450 my $tablename = $self->tablename("commitmsgs");
3452 CREATE TABLE $tablename (
3453 key TEXT NOT NULL PRIMARY KEY,
3470 if (exists $self->{valid_tables}{$name}) {
3471 return $self->{dbtablenameprefix} . $name;
3479 Bring the database up to date with the latest changes from
3482 Internal working state is read out of the "head" table and the
3483 "last_commit" property, then it updates "revisions" based on that, and
3484 finally it writes the new internal state back to the "head" table
3485 so it can be used as a starting point the next time update is called.
3492 # first lets get the commit list
3493 $ENV{GIT_DIR} = $self->{git_path};
3495 my $commitsha1 = `git rev-parse $self->{module}`;
3498 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
3499 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
3501 die("Invalid module '$self->{module}'");
3506 my $lastcommit = $self->_get_prop("last_commit");
3508 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
3512 # Start exclusive lock here...
3513 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
3515 # TODO: log processing is memory bound
3516 # if we can parse into a 2nd file that is in reverse order
3517 # we can probably do something really efficient
3518 my @git_log_params = ('--pretty', '--parents', '--topo-order');
3520 if (defined $lastcommit) {
3521 push @git_log_params, "$lastcommit..$self->{module}";
3523 push @git_log_params, $self->{module};
3525 # git-rev-list is the backend / plumbing version of git-log
3526 open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
3527 or die "Cannot call git-rev-list: $!";
3528 my @commits=readCommits($gitLogPipe);
3531 # Now all the commits are in the @commits bucket
3532 # ordered by time DESC. for each commit that needs processing,
3533 # determine whether it's following the last head we've seen or if
3534 # it's on its own branch, grab a file list, and add whatever's changed
3535 # NOTE: $lastcommit refers to the last commit from previous run
3536 # $lastpicked is the last commit we picked in this run
3539 if (defined $lastcommit) {
3540 $lastpicked = $lastcommit;
3543 my $committotal = scalar(@commits);
3544 my $commitcount = 0;
3546 # Load the head table into $head (for cached lookups during the update process)
3547 foreach my $file ( @{$self->gethead(1)} )
3549 $head->{$file->{name}} = $file;
3552 foreach my $commit ( @commits )
3554 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
3555 if (defined $lastpicked)
3557 if (!in_array($lastpicked, @{$commit->{parents}}))
3559 # skip, we'll see this delta
3560 # as part of a merge later
3561 # warn "skipping off-track $commit->{hash}\n";
3563 } elsif (@{$commit->{parents}} > 1) {
3564 # it is a merge commit, for each parent that is
3565 # not $lastpicked (not given a CVS revision number),
3566 # see if we can get a log
3567 # from the merge-base to that parent to put it
3568 # in the message as a merge summary.
3569 my @parents = @{$commit->{parents}};
3570 foreach my $parent (@parents) {
3571 if ($parent eq $lastpicked) {
3574 # git-merge-base can potentially (but rarely) throw
3575 # several candidate merge bases. let's assume
3576 # that the first one is the best one.
3578 safe_pipe_capture('git', 'merge-base',
3579 $lastpicked, $parent);
3581 # The two branches may not be related at all,
3582 # in which case merge base simply fails to find
3583 # any, but that's Ok.
3589 # print "want to log between $base $parent \n";
3590 open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
3591 or die "Cannot call git-log: $!";
3595 if (!defined $mergedhash) {
3596 if (m/^commit\s+(.+)$/) {
3602 # grab the first line that looks non-rfc822
3603 # aka has content after leading space
3604 if (m/^\s+(\S.*)$/) {
3606 $title = substr($title,0,100); # truncate
3607 unshift @merged, "$mergedhash $title";
3614 $commit->{mergemsg} = $commit->{message};
3615 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
3616 foreach my $summary (@merged) {
3617 $commit->{mergemsg} .= "\t$summary\n";
3619 $commit->{mergemsg} .= "\n\n";
3620 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
3627 # convert the date to CVS-happy format
3628 my $cvsDate = convertToCvsDate($commit->{date});
3630 if ( defined ( $lastpicked ) )
3632 my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
3634 while ( <FILELIST> )
3637 unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
3639 die("Couldn't process git-diff-tree line : $_");
3641 my ($mode, $hash, $change) = ($1, $2, $3);
3642 my $name = <FILELIST>;
3645 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3647 my $dbMode = convertToDbMode($mode);
3649 if ( $change eq "D" )
3651 #$log->debug("DELETE $name");
3654 revision => $head->{$name}{revision} + 1,
3655 filehash => "deleted",
3656 commithash => $commit->{hash},
3657 modified => $cvsDate,
3658 author => $commit->{author},
3661 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3663 elsif ( $change eq "M" || $change eq "T" )
3665 #$log->debug("MODIFIED $name");
3668 revision => $head->{$name}{revision} + 1,
3670 commithash => $commit->{hash},
3671 modified => $cvsDate,
3672 author => $commit->{author},
3675 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3677 elsif ( $change eq "A" )
3679 #$log->debug("ADDED $name");
3682 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
3684 commithash => $commit->{hash},
3685 modified => $cvsDate,
3686 author => $commit->{author},
3689 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3693 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3699 # this is used to detect files removed from the repo
3700 my $seen_files = {};
3702 my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
3704 while ( <FILELIST> )
3707 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3709 die("Couldn't process git-ls-tree line : $_");
3712 my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
3714 $seen_files->{$git_filename} = 1;
3716 my ( $oldhash, $oldrevision, $oldmode ) = (
3717 $head->{$git_filename}{filehash},
3718 $head->{$git_filename}{revision},
3719 $head->{$git_filename}{mode}
3722 my $dbMode = convertToDbMode($mode);
3724 # unless the file exists with the same hash, we need to update it ...
3725 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
3727 my $newrevision = ( $oldrevision or 0 ) + 1;
3729 $head->{$git_filename} = {
3730 name => $git_filename,
3731 revision => $newrevision,
3732 filehash => $git_hash,
3733 commithash => $commit->{hash},
3734 modified => $cvsDate,
3735 author => $commit->{author},
3740 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3745 # Detect deleted files
3746 foreach my $file ( keys %$head )
3748 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
3750 $head->{$file}{revision}++;
3751 $head->{$file}{filehash} = "deleted";
3752 $head->{$file}{commithash} = $commit->{hash};
3753 $head->{$file}{modified} = $cvsDate;
3754 $head->{$file}{author} = $commit->{author};
3756 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
3759 # END : "Detect deleted files"
3763 if (exists $commit->{mergemsg})
3765 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3768 $lastpicked = $commit->{hash};
3770 $self->_set_prop("last_commit", $commit->{hash});
3773 $self->delete_head();
3774 foreach my $file ( keys %$head )
3778 $head->{$file}{revision},
3779 $head->{$file}{filehash},
3780 $head->{$file}{commithash},
3781 $head->{$file}{modified},
3782 $head->{$file}{author},
3783 $head->{$file}{mode},
3786 # invalidate the gethead cache
3787 $self->clearCommitRefCaches();
3790 # Ending exclusive lock here
3791 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
3796 my $pipeHandle = shift;
3801 while ( <$pipeHandle> )
3804 if (m/^commit\s+(.*)$/) {
3805 # on ^commit lines put the just seen commit in the stack
3806 # and prime things for the next one
3809 unshift @commits, \%copy;
3812 my @parents = split(m/\s+/, $1);
3813 $commit{hash} = shift @parents;
3814 $commit{parents} = \@parents;
3815 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
3816 # on rfc822-like lines seen before we see any message,
3817 # lowercase the entry and put it in the hash as key-value
3818 $commit{lc($1)} = $2;
3820 # message lines - skip initial empty line
3821 # and trim whitespace
3822 if (!exists($commit{message}) && m/^\s*$/) {
3823 # define it to mark the end of headers
3824 $commit{message} = '';
3827 s/^\s+//; s/\s+$//; # trim ws
3828 $commit{message} .= $_ . "\n";
3832 unshift @commits, \%commit if ( keys %commit );
3837 sub convertToCvsDate
3840 # Convert from: "git rev-list --pretty" formatted date
3841 # Convert to: "the format specified by RFC822 as modified by RFC1123."
3842 # Example: 26 May 1997 13:01:40 -0400
3843 if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
3845 $date = "$2 $1 $4 $3 $5";
3855 # NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
3856 # but the database "mode" column historically (and currently)
3857 # only stores the "rw" (for user) part of the string.
3858 # FUTURE: It might make more sense to persist the raw
3859 # octal mode (or perhaps the final full CVS form) instead of
3860 # this half-converted form, but it isn't currently worth the
3861 # backwards compatibility headaches.
3863 $mode=~/^\d\d(\d)\d{3}$/;
3867 $dbMode .= "r" if ( $userBits & 4 );
3868 $dbMode .= "w" if ( $userBits & 2 );
3869 $dbMode .= "x" if ( $userBits & 1 );
3870 $dbMode = "rw" if ( $dbMode eq "" );
3879 my $revision = shift;
3880 my $filehash = shift;
3881 my $commithash = shift;
3882 my $modified = shift;
3885 my $tablename = $self->tablename("revision");
3887 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3888 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3896 my $tablename = $self->tablename("commitmsgs");
3898 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3899 $insert_mergelog->execute($key, $value);
3905 my $tablename = $self->tablename("head");
3907 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1);
3908 $delete_head->execute();
3915 my $revision = shift;
3916 my $filehash = shift;
3917 my $commithash = shift;
3918 my $modified = shift;
3921 my $tablename = $self->tablename("head");
3923 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3924 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3931 my $tablename = $self->tablename("properties");
3933 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3934 $db_query->execute($key);
3935 my ( $value ) = $db_query->fetchrow_array;
3945 my $tablename = $self->tablename("properties");
3947 my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1);
3948 $db_query->execute($value, $key);
3950 unless ( $db_query->rows )
3952 $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3953 $db_query->execute($key, $value);
3967 my $tablename = $self->tablename("head");
3969 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
3971 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1);
3972 $db_query->execute();
3975 while ( my $file = $db_query->fetchrow_hashref )
3979 $file->{revision} = "1.$file->{revision}"
3984 $self->{gethead_cache} = $tree;
3991 Returns a reference to an array of getmeta structures, one
3992 per file in the specified tree hash.
3998 my ($self,$hash) = @_;
4002 return $self->gethead();
4007 open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
4008 or die("Cannot call git-ls-tree : $!");
4016 foreach $line (@files)
4019 unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
4021 die("Couldn't process git-ls-tree line : $_");
4024 my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
4025 push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
4031 =head2 getRevisionDirMap
4033 A "revision dir map" contains all the plain-file filenames associated
4034 with a particular revision (treeish), organized by directory:
4036 $type = $out->{$dir}{$fullName}
4038 The type of each is "F" (for ordinary file) or "D" (for directory,
4039 for which the map $out->{$fullName} will also exist).
4043 sub getRevisionDirMap
4047 if(!defined($self->{revisionDirMapCache}))
4049 $self->{revisionDirMapCache}={};
4052 # Get file list (previously cached results are dependent on HEAD,
4053 # but are early in each case):
4056 if( !defined($ver) || $ver eq "" )
4059 if( defined($self->{revisionDirMapCache}{$cacheKey}) )
4061 return $self->{revisionDirMapCache}{$cacheKey};
4064 my @head = @{$self->gethead()};
4065 foreach my $file ( @head )
4067 next if ( $file->{filehash} eq "deleted" );
4069 push @fileList,$file->{name};
4074 my ($hash)=$self->lookupCommitRef($ver);
4075 if( !defined($hash) )
4081 if( defined($self->{revisionDirMapCache}{$cacheKey}) )
4083 return $self->{revisionDirMapCache}{$cacheKey};
4086 open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
4087 or die("Cannot call git-ls-tree : $!");
4089 while ( <$filePipe> )
4092 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
4094 die("Couldn't process git-ls-tree line : $_");
4097 my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
4099 push @fileList, $git_filename;
4104 # Convert to normalized form:
4107 foreach $file (@fileList)
4109 my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
4110 $dir='' if(!defined($dir));
4112 # parent directories:
4113 # ... create empty dir maps for parent dirs:
4115 while(!defined($revMap{$td}))
4119 my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
4120 $tp='' if(!defined($tp));
4123 # ... add children to parent maps (now that they exist):
4127 my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
4128 $tp='' if(!defined($tp));
4130 if(defined($revMap{$tp}{$td}))
4132 if($revMap{$tp}{$td} ne 'D')
4134 die "Weird file/directory inconsistency in $cacheKey";
4138 $revMap{$tp}{$td}='D';
4144 $revMap{$dir}{$file}='F';
4148 $self->{revisionDirMapCache}{$cacheKey}=\%revMap;
4149 return $self->{revisionDirMapCache}{$cacheKey};
4154 See also gethistorydense().
4161 my $filename = shift;
4162 my $revFilter = shift;
4164 my $tablename = $self->tablename("revision");
4167 # TODO: date, state, or by specific logins filters?
4168 # TODO: Handle comma-separated list of revFilter items, each item
4169 # can be a range [only case currently handled] or individual
4170 # rev or branch or "branch.".
4171 # TODO: Adjust $db_query WHERE clause based on revFilter, instead of
4172 # manually filtering the results of the query?
4173 my ( $minrev, $maxrev );
4174 if( defined($revFilter) and
4175 $state->{opt}{r} =~ /^(1.(\d+))?(::?)(1.(\d.+))?$/ )
4180 $minrev++ if ( defined($minrev) and $control eq "::" );
4183 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
4184 $db_query->execute($filename);
4188 while ( my $file = $db_query->fetchrow_hashref )
4191 if( defined($minrev) and $file->{revision} < $minrev )
4195 if( defined($maxrev) and $file->{revision} > $maxrev )
4200 $file->{revision} = "1." . $file->{revision};
4204 return ($tree,$totalRevs);
4209 This function takes a filename (with path) argument and returns a hashref of
4210 metadata for that file.
4212 There are several ways $revision can be specified:
4214 - A reference to hash that contains a "tag" that is the
4215 actual revision (one of the below). TODO: Also allow it to
4216 specify a "date" in the hash.
4217 - undef, to refer to the latest version on the main branch.
4218 - Full CVS client revision number (mapped to integer in DB, without the
4220 - Complex CVS-compatible "special" revision number for
4221 non-linear history (see comment below)
4222 - git commit sha1 hash
4223 - branch or tag name
4230 my $filename = shift;
4231 my $revision = shift;
4232 my $tablename_rev = $self->tablename("revision");
4233 my $tablename_head = $self->tablename("head");
4235 if ( ref($revision) eq "HASH" )
4237 $revision = $revision->{tag};
4240 # Overview of CVS revision numbers:
4242 # General CVS numbering scheme:
4243 # - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.
4244 # - Result of "cvs checkin -r" (possible, but not really
4245 # recommended): "2.1", "2.2", etc
4246 # - Branch tag: "1.2.0.n", where "1.2" is revision it was branched
4247 # from, "0" is a magic placeholder that identifies it as a
4248 # branch tag instead of a version tag, and n is 2 times the
4249 # branch number off of "1.2", starting with "2".
4250 # - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"
4251 # is branch number off of "1.2" (like n above), and "x" is
4252 # the version number on the branch.
4253 # - Branches can branch off of branches: "1.3.2.7.4.1" (even number
4255 # - Odd "n"s are used by "vendor branches" that result
4256 # from "cvs import". Vendor branches have additional
4257 # strangeness in the sense that the main rcs "head" of the main
4258 # branch will (temporarily until first normal commit) point
4259 # to the version on the vendor branch, rather than the actual
4260 # main branch. (FUTURE: This may provide an opportunity
4261 # to use "strange" revision numbers for fast-forward-merged
4262 # branch tip when CVS client is asking for the main branch.)
4264 # git-cvsserver CVS-compatible special numbering schemes:
4265 # - Currently git-cvsserver only tries to be identical to CVS for
4266 # simple "1.x" numbers on the "main" branch (as identified
4267 # by the module name that was originally cvs checkout'ed).
4268 # - The database only stores the "x" part, for historical reasons.
4269 # But most of the rest of the cvsserver preserves
4270 # and thinks using the full revision number.
4271 # - To handle non-linear history, it uses a version of the form
4272 # "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely
4273 # identify this as a special revision number, and there are
4274 # 20 b's that together encode the sha1 git commit from which
4275 # this version of this file originated. Each b is
4276 # the numerical value of the corresponding byte plus
4278 # - "plus 100" avoids "0"s, and also reduces the
4279 # likelyhood of a collision in the case that someone someday
4280 # writes an import tool that tries to preserve original
4281 # CVS revision numbers, and the original CVS data had done
4282 # lots of branches off of branches and other strangeness to
4283 # end up with a real version number that just happens to look
4284 # like this special revision number form. Also, if needed
4285 # there are several ways to extend/identify alternative encodings
4286 # within the "2.1.1.2000" part if necessary.
4287 # - Unlike real CVS revisions, you can't really reconstruct what
4288 # relation a revision of this form has to other revisions.
4289 # - FUTURE: TODO: Rework database somehow to make up and remember
4290 # fully-CVS-compatible branches and branch version numbers.
4293 if ( defined($revision) )
4295 if ( $revision =~ /^1\.(\d+)$/ )
4299 $db_query = $self->{dbh}->prepare_cached(
4300 "SELECT * FROM $tablename_rev WHERE name=? AND revision=?",
4302 $db_query->execute($filename, $intRev);
4303 $meta = $db_query->fetchrow_hashref;
4305 elsif ( $revision =~ /^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/ )
4307 my ($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);
4308 $commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;
4309 if($commitHash=~/^[0-9a-f]{40}$/)
4311 return $self->getMetaFromCommithash($filename,$commitHash);
4314 # error recovery: fall back on head version below
4315 print "E Failed to find $filename version=$revision or commit=$commitHash\n";
4316 $log->warning("failed get $revision with commithash=$commitHash");
4319 elsif ( $revision =~ /^[0-9a-f]{40}$/ )
4321 # Try DB first. This is mostly only useful for req_annotate(),
4322 # which only calls this for stuff that should already be in
4323 # the DB. It is fairly likely to be a waste of time
4324 # in most other cases [unless the file happened to be
4325 # modified in $revision specifically], but
4326 # it is probably in the noise compared to how long
4327 # getMetaFromCommithash() will take.
4329 $db_query = $self->{dbh}->prepare_cached(
4330 "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
4332 $db_query->execute($filename, $revision);
4333 $meta = $db_query->fetchrow_hashref;
4337 my($revCommit)=$self->lookupCommitRef($revision);
4338 if($revCommit=~/^[0-9a-f]{40}$/)
4340 return $self->getMetaFromCommithash($filename,$revCommit);
4343 # error recovery: nothing found:
4344 print "E Failed to find $filename version=$revision\n";
4345 $log->warning("failed get $revision");
4351 my($revCommit)=$self->lookupCommitRef($revision);
4352 if($revCommit=~/^[0-9a-f]{40}$/)
4354 return $self->getMetaFromCommithash($filename,$revCommit);
4357 # error recovery: fall back on head version below
4358 print "E Failed to find $filename version=$revision\n";
4359 $log->warning("failed get $revision");
4360 undef $revision; # Allow fallback
4364 if(!defined($revision))
4367 $db_query = $self->{dbh}->prepare_cached(
4368 "SELECT * FROM $tablename_head WHERE name=?",{},1);
4369 $db_query->execute($filename);
4370 $meta = $db_query->fetchrow_hashref;
4375 $meta->{revision} = "1.$meta->{revision}";
4380 sub getMetaFromCommithash
4383 my $filename = shift;
4384 my $revCommit = shift;
4386 # NOTE: This function doesn't scale well (lots of forks), especially
4387 # if you have many files that have not been modified for many commits
4388 # (each git-rev-parse redoes a lot of work for each file
4389 # that theoretically could be done in parallel by smarter
4392 # TODO: Possible optimization strategies:
4393 # - Solve the issue of assigning and remembering "real" CVS
4394 # revision numbers for branches, and ensure the
4395 # data structure can do this efficiently. Perhaps something
4396 # similar to "git notes", and carefully structured to take
4397 # advantage same-sha1-is-same-contents, to roll the same
4398 # unmodified subdirectory data onto multiple commits?
4399 # - Write and use a C tool that is like git-blame, but
4400 # operates on multiple files with file granularity, instead
4401 # of one file with line granularity. Cache
4402 # most-recently-modified in $self->{commitRefCache}{$revCommit}.
4403 # Try to be intelligent about how many files we do with
4404 # one fork (perhaps one directory at a time, without recursion,
4405 # and/or include directory as one line item, recurse from here
4406 # instead of in C tool?).
4407 # - Perhaps we could ask the DB for (filename,fileHash),
4408 # and just guess that it is correct (that the file hadn't
4409 # changed between $revCommit and the found commit, then
4410 # changed back, confusing anything trying to interpret
4411 # history). Probably need to add another index to revisions
4412 # DB table for this.
4413 # - NOTE: Trying to store all (commit,file) keys in DB [to
4414 # find "lastModfiedCommit] (instead of
4415 # just files that changed in each commit as we do now) is
4416 # probably not practical from a disk space perspective.
4418 # Does the file exist in $revCommit?
4419 # TODO: Include file hash in dirmap cache.
4420 my($dirMap)=$self->getRevisionDirMap($revCommit);
4421 my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
4426 if( !defined($dirMap->{$dir}) ||
4427 !defined($dirMap->{$dir}{$filename}) )
4429 my($fileHash)="deleted";
4432 $retVal->{name}=$filename;
4433 $retVal->{filehash}=$fileHash;
4435 # not needed and difficult to compute:
4436 $retVal->{revision}="0"; # $revision;
4437 $retVal->{commithash}=$revCommit;
4438 #$retVal->{author}=$commit->{author};
4439 #$retVal->{modified}=convertToCvsDate($commit->{date});
4440 #$retVal->{mode}=convertToDbMode($mode);
4445 my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
4447 if(!($fileHash=~/^[0-9a-f]{40}$/))
4449 die "Invalid fileHash '$fileHash' looking up"
4450 ." '$revCommit:$filename'\n";
4453 # information about most recent commit to modify $filename:
4454 open(my $gitLogPipe, '-|', 'git', 'rev-list',
4455 '--max-count=1', '--pretty', '--parents',
4456 $revCommit, '--', $filename)
4457 or die "Cannot call git-rev-list: $!";
4458 my @commits=readCommits($gitLogPipe);
4460 if(scalar(@commits)!=1)
4462 die "Can't find most recent commit changing $filename\n";
4464 my($commit)=$commits[0];
4465 if( !defined($commit) || !defined($commit->{hash}) )
4470 # does this (commit,file) have a real assigned CVS revision number?
4471 my $tablename_rev = $self->tablename("revision");
4473 $db_query = $self->{dbh}->prepare_cached(
4474 "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
4476 $db_query->execute($filename, $commit->{hash});
4477 my($meta)=$db_query->fetchrow_hashref;
4480 $meta->{revision} = "1.$meta->{revision}";
4484 # fall back on special revision number
4485 my($revision)=$commit->{hash};
4486 $revision=~s/(..)/'.' . (hex($1)+100)/eg;
4487 $revision="2.1.1.2000$revision";
4489 # meta data about $filename:
4490 open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
4491 $commit->{hash}, '--', $filename)
4492 or die("Cannot call git-ls-tree : $!");
4496 if(defined(<$filePipe>))
4498 die "Expected only a single file for git-ls-tree $filename\n";
4503 unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
4505 die("Couldn't process git-ls-tree line : $line\n");
4507 my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
4511 $retVal->{name}=$filename;
4512 $retVal->{revision}=$revision;
4513 $retVal->{filehash}=$fileHash;
4514 $retVal->{commithash}=$revCommit;
4515 $retVal->{author}=$commit->{author};
4516 $retVal->{modified}=convertToCvsDate($commit->{date});
4517 $retVal->{mode}=convertToDbMode($mode);
4522 =head2 lookupCommitRef
4524 Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches
4525 the result so looking it up again is fast.
4534 my $commitHash = $self->{commitRefCache}{$ref};
4535 if(defined($commitHash))
4540 $commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
4541 $self->unescapeRefName($ref));
4542 $commitHash=~s/\s*$//;
4543 if(!($commitHash=~/^[0-9a-f]{40}$/))
4548 if( defined($commitHash) )
4550 my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
4551 if( ! ($type=~/^commit\s*$/ ) )
4556 if(defined($commitHash))
4558 $self->{commitRefCache}{$ref}=$commitHash;
4563 =head2 clearCommitRefCaches
4565 Clears cached commit cache (sha1's for various tags/abbeviations/etc),
4570 sub clearCommitRefCaches
4573 $self->{commitRefCache} = {};
4574 $self->{revisionDirMapCache} = undef;
4575 $self->{gethead_cache} = undef;
4578 =head2 commitmessage
4580 this function takes a commithash and returns the commit message for that commit
4586 my $commithash = shift;
4587 my $tablename = $self->tablename("commitmsgs");
4589 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
4592 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
4593 $db_query->execute($commithash);
4595 my ( $message ) = $db_query->fetchrow_array;
4597 if ( defined ( $message ) )
4599 $message .= " " if ( $message =~ /\n$/ );
4603 my @lines = safe_pipe_capture("git", "cat-file", "commit", $commithash);
4604 shift @lines while ( $lines[0] =~ /\S/ );
4605 $message = join("",@lines);
4606 $message .= " " if ( $message =~ /\n$/ );
4610 =head2 gethistorydense
4612 This function takes a filename (with path) argument and returns an arrayofarrays
4613 containing revision,filehash,commithash ordered by revision descending.
4615 This version of gethistory skips deleted entries -- so it is useful for annotate.
4616 The 'dense' part is a reference to a '--dense' option available for git-rev-list
4617 and other git tools that depend on it.
4625 my $filename = shift;
4626 my $tablename = $self->tablename("revision");
4629 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
4630 $db_query->execute($filename);
4632 my $result = $db_query->fetchall_arrayref;
4635 for($i=0 ; $i<scalar(@$result) ; $i++)
4637 $result->[$i][0]="1." . $result->[$i][0];
4643 =head2 escapeRefName
4645 Apply an escape mechanism to compensate for characters that
4646 git ref names can have that CVS tags can not.
4651 my($self,$refName)=@_;
4653 # CVS officially only allows [-_A-Za-z0-9] in tag names (or in
4654 # many contexts it can also be a CVS revision number).
4656 # Git tags commonly use '/' and '.' as well, but also handle
4657 # anything else just in case:
4661 # = "_-u-" For underscore, in case someone wants a literal "_-" in
4663 # = "_-xx-" Where "xx" is the hexadecimal representation of the
4664 # desired ASCII character byte. (for anything else)
4666 if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
4668 $refName=~s/_-/_-u--/g;
4669 $refName=~s/\./_-p-/g;
4670 $refName=~s%/%_-s-%g;
4671 $refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
4675 =head2 unescapeRefName
4677 Undo an escape mechanism to compensate for characters that
4678 git ref names can have that CVS tags can not.
4683 my($self,$refName)=@_;
4685 # see escapeRefName() for description of escape mechanism.
4687 $refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
4690 # TODO: Perhaps use git check-ref-format, with an in-process cache of
4692 if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
4693 ( $refName=~m%[/.]$% ) ||
4694 ( $refName=~/\.lock$/ ) ||
4695 ( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) ) # matching }
4698 $log->warn("illegal refName: $refName");
4704 sub unescapeRefNameChar
4720 elsif($char=~/^[0-9a-f][0-9a-f]$/)
4722 $char=chr(hex($char));
4726 # Error case: Maybe it has come straight from user, and
4727 # wasn't supposed to be escaped? Restore it the way we got it:
4736 from Array::PAT - mimics the in_array() function
4737 found in PHP. Yuck but works for small arrays.
4742 my ($check, @array) = @_;
4744 foreach my $test (@array){
4745 if($check eq $test){
4752 =head2 safe_pipe_capture
4754 an alternative to `command` that allows input to be passed as an array
4755 to work around shell problems with weird characters in arguments
4758 sub safe_pipe_capture {
4762 if (my $pid = open my $child, '-|') {
4763 @output = (<$child>);
4764 close $child or die join(' ',@_).": $! $?";
4766 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
4768 return wantarray ? @output : join('',@output);
4771 =head2 mangle_dirname
4773 create a string from a directory name that is suitable to use as
4774 part of a filename, mainly by converting all chars except \w.- to _
4777 sub mangle_dirname {
4778 my $dirname = shift;
4779 return unless defined $dirname;
4781 $dirname =~ s/[^\w.-]/_/g;
4786 =head2 mangle_tablename
4788 create a string from a that is suitable to use as part of an SQL table
4789 name, mainly by converting all chars except \w to _
4792 sub mangle_tablename {
4793 my $tablename = shift;
4794 return unless defined $tablename;
4796 $tablename =~ s/[^\w_]/_/g;