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 # This method uses $state->{directory} to populate $state->{args} with a list of filenames
2232 my $updater = shift;
2234 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
2236 return if ( scalar ( @{$state->{args}} ) > 1 );
2238 my @gethead = @{$updater->gethead};
2241 foreach my $file (keys %{$state->{entries}}) {
2242 if ( exists $state->{entries}{$file}{revision} &&
2243 $state->{entries}{$file}{revision} eq '0' )
2245 push @gethead, { name => $file, filehash => 'added' };
2249 if ( scalar(@{$state->{args}}) == 1 )
2251 my $arg = $state->{args}[0];
2252 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
2254 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
2256 foreach my $file ( @gethead )
2258 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2259 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
2260 push @{$state->{args}}, $file->{name};
2263 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
2265 $log->info("Only one arg specified, populating file list automatically");
2267 $state->{args} = [];
2269 foreach my $file ( @gethead )
2271 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2272 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
2273 push @{$state->{args}}, $file->{name};
2279 ## look up directory sticky tag, of either fullPath or a parent:
2280 sub getDirStickyInfo
2285 while($fullPath ne "" && !defined($state->{dirMap}{"$fullPath/"}))
2287 $fullPath=~s%/?[^/]*$%%;
2290 if( !defined($state->{dirMap}{"$fullPath/"}) &&
2291 ( $fullPath eq "" ||
2292 $fullPath eq "." ) )
2294 return $state->{dirMap}{""}{stickyInfo};
2298 return $state->{dirMap}{"$fullPath/"}{stickyInfo};
2302 # Resolve precedence of various ways of specifying which version of
2303 # a file you want. Returns undef (for default head), or a ref to a hash
2304 # that contains "tag" and/or "date" keys.
2305 sub resolveStickyInfo
2307 my($filename,$stickyTag,$stickyDate,$reset) = @_;
2309 # Order of precedence of sticky tags:
2312 # [file entry sticky tag]
2313 # [the tag specified in dir req_Sticky]
2314 # [the tag specified in a parent dir req_Sticky]
2322 elsif( defined($stickyTag) && $stickyTag ne "" )
2323 # || ( defined($stickyDate) && $stickyDate ne "" ) # TODO
2325 $result={ 'tag' => (defined($stickyTag)?$stickyTag:undef) };
2327 # TODO: Convert -D value into the form 2011.04.10.04.46.57,
2328 # similar to an entry line's sticky date, without the D prefix.
2329 # It sometimes (always?) arrives as something more like
2330 # '10 Apr 2011 04:46:57 -0000'...
2331 # $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
2333 elsif( defined($state->{entries}{$filename}) &&
2334 defined($state->{entries}{$filename}{tag_or_date}) &&
2335 $state->{entries}{$filename}{tag_or_date} ne "" )
2337 my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
2338 if($tagOrDate=~/^T([^ ]+)\s*$/)
2340 $result = { 'tag' => $1 };
2342 elsif($tagOrDate=~/^D([0-9.]+)\s*$/)
2344 $result= { 'date' => $1 };
2348 die "Unknown tag_or_date format\n";
2353 $result=getDirStickyInfo($filename);
2359 # Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into
2360 # a form appropriate for the sticky tag field of an Entries
2361 # line (field index 5, 0-based).
2362 sub getStickyTagOrDate
2367 if(defined($stickyInfo) && defined($stickyInfo->{tag}))
2369 $result="T$stickyInfo->{tag}";
2371 # TODO: When/if we actually pick versions by {date} properly,
2372 # also handle it here:
2373 # "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").
2382 # This method cleans up the $state variable after a command that uses arguments has run
2385 $state->{files} = [];
2386 $state->{args} = [];
2387 $state->{arguments} = [];
2388 $state->{entries} = {};
2389 $state->{dirMap} = {};
2392 # Return working directory CVS revision "1.X" out
2393 # of the the working directory "entries" state, for the given filename.
2394 # This is prefixed with a dash if the file is scheduled for removal
2395 # when it is committed.
2398 my $filename = shift;
2400 return $state->{entries}{$filename}{revision};
2403 # This method takes a file hash and does a CVS "file transfer". Its
2404 # exact behaviour depends on a second, optional hash table argument:
2405 # - If $options->{targetfile}, dump the contents to that file;
2406 # - If $options->{print}, use M/MT to transmit the contents one line
2408 # - Otherwise, transmit the size of the file, followed by the file
2412 my $filehash = shift;
2413 my $options = shift;
2415 if ( defined ( $filehash ) and $filehash eq "deleted" )
2417 $log->warn("filehash is 'deleted'");
2421 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2423 my $type = `git cat-file -t $filehash`;
2426 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2428 my $size = `git cat-file -s $filehash`;
2431 $log->debug("transmitfile($filehash) size=$size, type=$type");
2433 if ( open my $fh, '-|', "git", "cat-file", "blob", $filehash )
2435 if ( defined ( $options->{targetfile} ) )
2437 my $targetfile = $options->{targetfile};
2438 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2439 print NEWFILE $_ while ( <$fh> );
2440 close NEWFILE or die("Failed to write '$targetfile': $!");
2441 } elsif ( defined ( $options->{print} ) && $options->{print} ) {
2446 print 'MT text ', $_, "\n";
2451 print while ( <$fh> );
2453 close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
2455 die("Couldn't execute git-cat-file");
2459 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2460 # refers to the directory portion and the file portion of the filename
2464 my $filename = shift;
2465 my $fixforlocaldir = shift;
2467 my ( $filepart, $dirpart ) = ( $filename, "." );
2468 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2471 if ( $fixforlocaldir )
2473 $dirpart =~ s/^$state->{prependdir}//;
2476 return ( $filepart, $dirpart );
2479 # Cleanup various junk in filename (try to canonicalize it), and
2480 # add prependdir to accomodate running CVS client from a
2481 # subdirectory (so the output is relative to top directory of the project).
2484 my $filename = shift;
2486 return undef unless(defined($filename));
2487 if ( $filename =~ /^\// )
2489 print "E absolute filenames '$filename' not supported by server\n";
2493 if($filename eq ".")
2497 $filename =~ s/^\.\///g;
2498 $filename =~ s%/+%/%g;
2499 $filename = $state->{prependdir} . $filename;
2500 $filename =~ s%/$%%;
2504 # Remove prependdir from the path, so that is is relative to the directory
2505 # the CVS client was started from, rather than the top of the project.
2506 # Essentially the inverse of filecleanup().
2507 sub remove_prependdir
2510 if(defined($state->{prependdir}) && $state->{prependdir} ne "")
2512 my($pre)=$state->{prependdir};
2514 if(!($path=~s%^\Q$pre\E/?%%))
2516 $log->fatal("internal error missing prependdir");
2517 die("internal error missing prependdir");
2525 if( !defined($state->{CVSROOT}) )
2527 print "error 1 CVSROOT not specified\n";
2531 if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') )
2533 print "error 1 Internally inconsistent CVSROOT\n";
2539 # Setup working directory in a work tree with the requested version
2540 # loaded in the index.
2547 if( ( defined($work->{state}) && $work->{state} != 1 ) ||
2548 defined($work->{tmpDir}) )
2550 $log->warn("Bad work tree state management");
2551 print "error 1 Internal setup multiple work trees without cleanup\n";
2556 $work->{workDir} = tempdir ( DIR => $TEMP_DIR );
2558 if( !defined($work->{index}) )
2560 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2563 chdir $work->{workDir} or
2564 die "Unable to chdir to $work->{workDir}\n";
2566 $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");
2568 $ENV{GIT_WORK_TREE} = ".";
2569 $ENV{GIT_INDEX_FILE} = $work->{index};
2574 system("git","read-tree",$ver);
2577 $log->warn("Error running git-read-tree");
2578 die "Error running git-read-tree $ver in $work->{workDir} $!\n";
2581 # else # req_annotate reads tree for each file
2584 # Ensure current directory is in some kind of working directory,
2585 # with a recent version loaded in the index.
2588 if( defined($work->{tmpDir}) )
2590 $log->warn("Bad work tree state management [ensureWorkTree()]");
2591 print "error 1 Internal setup multiple dirs without cleanup\n";
2595 if( $work->{state} )
2602 if( !defined($work->{emptyDir}) )
2604 $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0);
2606 chdir $work->{emptyDir} or
2607 die "Unable to chdir to $work->{emptyDir}\n";
2609 my $ver = `git show-ref -s refs/heads/$state->{module}`;
2611 if ($ver !~ /^[0-9a-f]{40}$/)
2613 $log->warn("Error from git show-ref -s refs/head$state->{module}");
2614 print "error 1 cannot find the current HEAD of module";
2619 if( !defined($work->{index}) )
2621 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2624 $ENV{GIT_WORK_TREE} = ".";
2625 $ENV{GIT_INDEX_FILE} = $work->{index};
2628 system("git","read-tree",$ver);
2631 die "Error running git-read-tree $ver $!\n";
2635 # Cleanup working directory that is not needed any longer.
2638 if( ! $work->{state} )
2643 chdir "/" or die "Unable to chdir '/'\n";
2645 if( defined($work->{workDir}) )
2647 rmtree( $work->{workDir} );
2648 undef $work->{workDir};
2650 undef $work->{state};
2653 # Setup a temporary directory (not a working tree), typically for
2654 # merging dirty state as in req_update.
2657 $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR );
2658 chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n";
2660 return $work->{tmpDir};
2663 # Clean up a previously setupTmpDir. Restore previous work tree if
2667 if ( !defined($work->{tmpDir}) )
2669 $log->warn("cleanup tmpdir that has not been setup");
2670 die "Cleanup tmpDir that has not been setup\n";
2672 if( defined($work->{state}) )
2674 if( $work->{state} == 1 )
2676 chdir $work->{emptyDir} or
2677 die "Unable to chdir to $work->{emptyDir}\n";
2679 elsif( $work->{state} == 2 )
2681 chdir $work->{workDir} or
2682 die "Unable to chdir to $work->{emptyDir}\n";
2686 $log->warn("Inconsistent work dir state");
2687 die "Inconsistent work dir state\n";
2692 chdir "/" or die "Unable to chdir '/'\n";
2696 # Given a path, this function returns a string containing the kopts
2697 # that should go into that path's Entries line. For example, a binary
2698 # file should get -kb.
2701 my ($path, $srcType, $name) = @_;
2703 if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and
2704 $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i )
2706 my ($val) = check_attr( "text", $path );
2707 if ( $val eq "unspecified" )
2709 $val = check_attr( "crlf", $path );
2711 if ( $val eq "unset" )
2715 elsif ( check_attr( "eol", $path ) ne "unspecified" ||
2716 $val eq "set" || $val eq "input" )
2722 $log->info("Unrecognized check_attr crlf $path : $val");
2726 if ( defined ( $cfg->{gitcvs}{allbinary} ) )
2728 if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) )
2732 elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
2734 if( is_binary($srcType,$name) )
2736 $log->debug("... as binary");
2741 $log->debug("... as text");
2745 # Return "" to give no special treatment to any path
2751 my ($attr,$path) = @_;
2753 if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path )
2757 $val =~ s/.*: ([^:\r\n]*)\s*$/$1/;
2766 # This should have the same heuristics as convert.c:is_binary() and related.
2767 # Note that the bare CR test is done by callers in convert.c.
2770 my ($srcType,$name) = @_;
2771 $log->debug("is_binary($srcType,$name)");
2773 # Minimize amount of interpreted code run in the inner per-character
2774 # loop for large files, by totalling each character value and
2775 # then analyzing the totals.
2778 for($i=0;$i<256;$i++)
2783 my $fh = open_blob_or_die($srcType,$name);
2785 while( defined($line=<$fh>) )
2787 # Any '\0' and bare CR are considered binary.
2788 if( $line =~ /\0|(\r[^\n])/ )
2794 # Count up each character in the line:
2795 my $len=length($line);
2796 for($i=0;$i<$len;$i++)
2798 $counts[ord(substr($line,$i,1))]++;
2803 # Don't count CR and LF as either printable/nonprintable
2804 $counts[ord("\n")]=0;
2805 $counts[ord("\r")]=0;
2807 # Categorize individual character count into printable and nonprintable:
2810 for($i=0;$i<256;$i++)
2818 $nonprintable+=$counts[$i];
2820 elsif( $i==127 ) # DEL
2822 $nonprintable+=$counts[$i];
2826 $printable+=$counts[$i];
2830 return ($printable >> 7) < $nonprintable;
2833 # Returns open file handle. Possible invocations:
2834 # - open_blob_or_die("file",$filename);
2835 # - open_blob_or_die("sha1",$filehash);
2836 sub open_blob_or_die
2838 my ($srcType,$name) = @_;
2840 if( $srcType eq "file" )
2842 if( !open $fh,"<",$name )
2844 $log->warn("Unable to open file $name: $!");
2845 die "Unable to open file $name: $!\n";
2848 elsif( $srcType eq "sha1" )
2850 unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
2852 $log->warn("Need filehash");
2853 die "Need filehash\n";
2856 my $type = `git cat-file -t $name`;
2859 unless ( defined ( $type ) and $type eq "blob" )
2861 $log->warn("Invalid type '$type' for '$name'");
2862 die ( "Invalid type '$type' (expected 'blob')" )
2865 my $size = `git cat-file -s $name`;
2868 $log->debug("open_blob_or_die($name) size=$size, type=$type");
2870 unless( open $fh, '-|', "git", "cat-file", "blob", $name )
2872 $log->warn("Unable to open sha1 $name");
2873 die "Unable to open sha1 $name\n";
2878 $log->warn("Unknown type of blob source: $srcType");
2879 die "Unknown type of blob source: $srcType\n";
2884 # Generate a CVS author name from Git author information, by taking the local
2885 # part of the email address and replacing characters not in the Portable
2886 # Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS
2887 # Login names are Unix login names, which should be restricted to this
2891 my $author_line = shift;
2892 (my $author) = $author_line =~ /<([^@>]*)/;
2894 $author =~ s/[^-a-zA-Z0-9_.]/_/g;
2903 # This table is from src/scramble.c in the CVS source
2905 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
2906 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2907 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87,
2908 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105,
2909 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35,
2910 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56,
2911 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48,
2912 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223,
2913 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,
2914 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,
2915 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,
2916 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,
2917 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,
2918 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,
2919 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,
2920 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152
2924 # This should never happen, the same password format (A) has been
2925 # used by CVS since the beginning of time
2927 my $fmt = substr($str, 0, 1);
2928 die "invalid password format `$fmt'" unless $fmt eq 'A';
2931 my @str = unpack "C*", substr($str, 1);
2932 my $ret = join '', map { chr $SHIFTS[$_] } @str;
2937 package GITCVS::log;
2940 #### Copyright The Open University UK - 2006.
2942 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2943 #### Martin Langhoff <martin@laptop.org>
2956 This module provides very crude logging with a similar interface to
2965 Creates a new log object, optionally you can specify a filename here to
2966 indicate the file to log to. If no log file is specified, you can specify one
2967 later with method setfile, or indicate you no longer want logging with method
2970 Until one of these methods is called, all log calls will buffer messages ready
2977 my $filename = shift;
2981 bless $self, $class;
2983 if ( defined ( $filename ) )
2985 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2993 This methods takes a filename, and attempts to open that file as the log file.
2994 If successful, all buffered data is written out to the file, and any further
2995 logging is written directly to the file.
3001 my $filename = shift;
3003 if ( defined ( $filename ) )
3005 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
3008 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
3010 while ( my $line = shift @{$self->{buffer}} )
3012 print {$self->{fh}} $line;
3018 This method indicates no logging is going to be used. It flushes any entries in
3019 the internal buffer, and sets a flag to ensure no further data is put there.
3028 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
3030 $self->{buffer} = [];
3035 Internal method. Returns true if the log file is open, false otherwise.
3042 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
3046 =head2 debug info warn fatal
3048 These four methods are wrappers to _log. They provide the actual interface for
3052 sub debug { my $self = shift; $self->_log("debug", @_); }
3053 sub info { my $self = shift; $self->_log("info" , @_); }
3054 sub warn { my $self = shift; $self->_log("warn" , @_); }
3055 sub fatal { my $self = shift; $self->_log("fatal", @_); }
3059 This is an internal method called by the logging functions. It generates a
3060 timestamp and pushes the logged line either to file, or internal buffer.
3068 return if ( $self->{nolog} );
3070 my @time = localtime;
3071 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
3081 if ( $self->_logopen )
3083 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
3085 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
3091 This method simply closes the file handle if one is open
3098 if ( $self->_logopen )
3104 package GITCVS::updater;
3107 #### Copyright The Open University UK - 2006.
3109 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
3110 #### Martin Langhoff <martin@laptop.org>
3132 die "Need to specify a git repository" unless ( defined($config) and -d $config );
3133 die "Need to specify a module" unless ( defined($module) );
3135 $class = ref($class) || $class;
3139 bless $self, $class;
3141 $self->{valid_tables} = {'revision' => 1,
3142 'revision_ix1' => 1,
3143 'revision_ix2' => 1,
3149 $self->{module} = $module;
3150 $self->{git_path} = $config . "/";
3152 $self->{log} = $log;
3154 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
3156 # Stores full sha1's for various branch/tag names, abbreviations, etc:
3157 $self->{commitRefCache} = {};
3159 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
3160 $cfg->{gitcvs}{dbdriver} || "SQLite";
3161 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
3162 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
3163 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
3164 $cfg->{gitcvs}{dbuser} || "";
3165 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
3166 $cfg->{gitcvs}{dbpass} || "";
3167 $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||
3168 $cfg->{gitcvs}{dbtablenameprefix} || "";
3169 my %mapping = ( m => $module,
3170 a => $state->{method},
3171 u => getlogin || getpwuid($<) || $<,
3172 G => $self->{git_path},
3173 g => mangle_dirname($self->{git_path}),
3175 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
3176 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
3177 $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg;
3178 $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});
3180 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
3181 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
3182 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
3185 die "Error connecting to database\n" unless defined $self->{dbh};
3187 $self->{tables} = {};
3188 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
3190 $self->{tables}{$table} = 1;
3193 # Construct the revision table if required
3194 # The revision table stores an entry for each file, each time that file
3196 # numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )
3197 # This is not sufficient to support "-r {commithash}" for any
3198 # files except files that were modified by that commit (also,
3199 # some places in the code ignore/effectively strip out -r in
3200 # some cases, before it gets passed to getmeta()).
3201 # The "filehash" field typically has a git blob hash, but can also
3202 # be set to "dead" to indicate that the given version of the file
3203 # should not exist in the sandbox.
3204 unless ( $self->{tables}{$self->tablename("revision")} )
3206 my $tablename = $self->tablename("revision");
3207 my $ix1name = $self->tablename("revision_ix1");
3208 my $ix2name = $self->tablename("revision_ix2");
3210 CREATE TABLE $tablename (
3212 revision INTEGER NOT NULL,
3213 filehash TEXT NOT NULL,
3214 commithash TEXT NOT NULL,
3215 author TEXT NOT NULL,
3216 modified TEXT NOT NULL,
3221 CREATE INDEX $ix1name
3222 ON $tablename (name,revision)
3225 CREATE INDEX $ix2name
3226 ON $tablename (name,commithash)
3230 # Construct the head table if required
3231 # The head table (along with the "last_commit" entry in the property
3232 # table) is the persisted working state of the "sub update" subroutine.
3233 # All of it's data is read entirely first, and completely recreated
3234 # last, every time "sub update" runs.
3235 # This is also used by "sub getmeta" when it is asked for the latest
3236 # version of a file (as opposed to some specific version).
3237 # Another way of thinking about it is as a single slice out of
3238 # "revisions", giving just the most recent revision information for
3240 unless ( $self->{tables}{$self->tablename("head")} )
3242 my $tablename = $self->tablename("head");
3243 my $ix1name = $self->tablename("head_ix1");
3245 CREATE TABLE $tablename (
3247 revision INTEGER NOT NULL,
3248 filehash TEXT NOT NULL,
3249 commithash TEXT NOT NULL,
3250 author TEXT NOT NULL,
3251 modified TEXT NOT NULL,
3256 CREATE INDEX $ix1name
3257 ON $tablename (name)
3261 # Construct the properties table if required
3262 # - "last_commit" - Used by "sub update".
3263 unless ( $self->{tables}{$self->tablename("properties")} )
3265 my $tablename = $self->tablename("properties");
3267 CREATE TABLE $tablename (
3268 key TEXT NOT NULL PRIMARY KEY,
3274 # Construct the commitmsgs table if required
3275 # The commitmsgs table is only used for merge commits, since
3276 # "sub update" will only keep one branch of parents. Shortlogs
3277 # for ignored commits (i.e. not on the chosen branch) will be used
3278 # to construct a replacement "collapsed" merge commit message,
3279 # which will be stored in this table. See also "sub commitmessage".
3280 unless ( $self->{tables}{$self->tablename("commitmsgs")} )
3282 my $tablename = $self->tablename("commitmsgs");
3284 CREATE TABLE $tablename (
3285 key TEXT NOT NULL PRIMARY KEY,
3302 if (exists $self->{valid_tables}{$name}) {
3303 return $self->{dbtablenameprefix} . $name;
3311 Bring the database up to date with the latest changes from
3314 Internal working state is read out of the "head" table and the
3315 "last_commit" property, then it updates "revisions" based on that, and
3316 finally it writes the new internal state back to the "head" table
3317 so it can be used as a starting point the next time update is called.
3324 # first lets get the commit list
3325 $ENV{GIT_DIR} = $self->{git_path};
3327 my $commitsha1 = `git rev-parse $self->{module}`;
3330 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
3331 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
3333 die("Invalid module '$self->{module}'");
3338 my $lastcommit = $self->_get_prop("last_commit");
3340 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
3344 # Start exclusive lock here...
3345 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
3347 # TODO: log processing is memory bound
3348 # if we can parse into a 2nd file that is in reverse order
3349 # we can probably do something really efficient
3350 my @git_log_params = ('--pretty', '--parents', '--topo-order');
3352 if (defined $lastcommit) {
3353 push @git_log_params, "$lastcommit..$self->{module}";
3355 push @git_log_params, $self->{module};
3357 # git-rev-list is the backend / plumbing version of git-log
3358 open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
3359 or die "Cannot call git-rev-list: $!";
3360 my @commits=readCommits($gitLogPipe);
3363 # Now all the commits are in the @commits bucket
3364 # ordered by time DESC. for each commit that needs processing,
3365 # determine whether it's following the last head we've seen or if
3366 # it's on its own branch, grab a file list, and add whatever's changed
3367 # NOTE: $lastcommit refers to the last commit from previous run
3368 # $lastpicked is the last commit we picked in this run
3371 if (defined $lastcommit) {
3372 $lastpicked = $lastcommit;
3375 my $committotal = scalar(@commits);
3376 my $commitcount = 0;
3378 # Load the head table into $head (for cached lookups during the update process)
3379 foreach my $file ( @{$self->gethead(1)} )
3381 $head->{$file->{name}} = $file;
3384 foreach my $commit ( @commits )
3386 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
3387 if (defined $lastpicked)
3389 if (!in_array($lastpicked, @{$commit->{parents}}))
3391 # skip, we'll see this delta
3392 # as part of a merge later
3393 # warn "skipping off-track $commit->{hash}\n";
3395 } elsif (@{$commit->{parents}} > 1) {
3396 # it is a merge commit, for each parent that is
3397 # not $lastpicked (not given a CVS revision number),
3398 # see if we can get a log
3399 # from the merge-base to that parent to put it
3400 # in the message as a merge summary.
3401 my @parents = @{$commit->{parents}};
3402 foreach my $parent (@parents) {
3403 if ($parent eq $lastpicked) {
3406 # git-merge-base can potentially (but rarely) throw
3407 # several candidate merge bases. let's assume
3408 # that the first one is the best one.
3410 safe_pipe_capture('git', 'merge-base',
3411 $lastpicked, $parent);
3413 # The two branches may not be related at all,
3414 # in which case merge base simply fails to find
3415 # any, but that's Ok.
3421 # print "want to log between $base $parent \n";
3422 open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
3423 or die "Cannot call git-log: $!";
3427 if (!defined $mergedhash) {
3428 if (m/^commit\s+(.+)$/) {
3434 # grab the first line that looks non-rfc822
3435 # aka has content after leading space
3436 if (m/^\s+(\S.*)$/) {
3438 $title = substr($title,0,100); # truncate
3439 unshift @merged, "$mergedhash $title";
3446 $commit->{mergemsg} = $commit->{message};
3447 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
3448 foreach my $summary (@merged) {
3449 $commit->{mergemsg} .= "\t$summary\n";
3451 $commit->{mergemsg} .= "\n\n";
3452 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
3459 # convert the date to CVS-happy format
3460 my $cvsDate = convertToCvsDate($commit->{date});
3462 if ( defined ( $lastpicked ) )
3464 my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
3466 while ( <FILELIST> )
3469 unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
3471 die("Couldn't process git-diff-tree line : $_");
3473 my ($mode, $hash, $change) = ($1, $2, $3);
3474 my $name = <FILELIST>;
3477 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3479 my $dbMode = convertToDbMode($mode);
3481 if ( $change eq "D" )
3483 #$log->debug("DELETE $name");
3486 revision => $head->{$name}{revision} + 1,
3487 filehash => "deleted",
3488 commithash => $commit->{hash},
3489 modified => $cvsDate,
3490 author => $commit->{author},
3493 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3495 elsif ( $change eq "M" || $change eq "T" )
3497 #$log->debug("MODIFIED $name");
3500 revision => $head->{$name}{revision} + 1,
3502 commithash => $commit->{hash},
3503 modified => $cvsDate,
3504 author => $commit->{author},
3507 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3509 elsif ( $change eq "A" )
3511 #$log->debug("ADDED $name");
3514 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
3516 commithash => $commit->{hash},
3517 modified => $cvsDate,
3518 author => $commit->{author},
3521 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3525 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3531 # this is used to detect files removed from the repo
3532 my $seen_files = {};
3534 my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
3536 while ( <FILELIST> )
3539 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3541 die("Couldn't process git-ls-tree line : $_");
3544 my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
3546 $seen_files->{$git_filename} = 1;
3548 my ( $oldhash, $oldrevision, $oldmode ) = (
3549 $head->{$git_filename}{filehash},
3550 $head->{$git_filename}{revision},
3551 $head->{$git_filename}{mode}
3554 my $dbMode = convertToDbMode($mode);
3556 # unless the file exists with the same hash, we need to update it ...
3557 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
3559 my $newrevision = ( $oldrevision or 0 ) + 1;
3561 $head->{$git_filename} = {
3562 name => $git_filename,
3563 revision => $newrevision,
3564 filehash => $git_hash,
3565 commithash => $commit->{hash},
3566 modified => $cvsDate,
3567 author => $commit->{author},
3572 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
3577 # Detect deleted files
3578 foreach my $file ( keys %$head )
3580 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
3582 $head->{$file}{revision}++;
3583 $head->{$file}{filehash} = "deleted";
3584 $head->{$file}{commithash} = $commit->{hash};
3585 $head->{$file}{modified} = $cvsDate;
3586 $head->{$file}{author} = $commit->{author};
3588 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
3591 # END : "Detect deleted files"
3595 if (exists $commit->{mergemsg})
3597 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3600 $lastpicked = $commit->{hash};
3602 $self->_set_prop("last_commit", $commit->{hash});
3605 $self->delete_head();
3606 foreach my $file ( keys %$head )
3610 $head->{$file}{revision},
3611 $head->{$file}{filehash},
3612 $head->{$file}{commithash},
3613 $head->{$file}{modified},
3614 $head->{$file}{author},
3615 $head->{$file}{mode},
3618 # invalidate the gethead cache
3619 $self->clearCommitRefCaches();
3622 # Ending exclusive lock here
3623 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
3628 my $pipeHandle = shift;
3633 while ( <$pipeHandle> )
3636 if (m/^commit\s+(.*)$/) {
3637 # on ^commit lines put the just seen commit in the stack
3638 # and prime things for the next one
3641 unshift @commits, \%copy;
3644 my @parents = split(m/\s+/, $1);
3645 $commit{hash} = shift @parents;
3646 $commit{parents} = \@parents;
3647 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
3648 # on rfc822-like lines seen before we see any message,
3649 # lowercase the entry and put it in the hash as key-value
3650 $commit{lc($1)} = $2;
3652 # message lines - skip initial empty line
3653 # and trim whitespace
3654 if (!exists($commit{message}) && m/^\s*$/) {
3655 # define it to mark the end of headers
3656 $commit{message} = '';
3659 s/^\s+//; s/\s+$//; # trim ws
3660 $commit{message} .= $_ . "\n";
3664 unshift @commits, \%commit if ( keys %commit );
3669 sub convertToCvsDate
3672 # Convert from: "git rev-list --pretty" formatted date
3673 # Convert to: "the format specified by RFC822 as modified by RFC1123."
3674 # Example: 26 May 1997 13:01:40 -0400
3675 if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
3677 $date = "$2 $1 $4 $3 $5";
3687 # NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
3688 # but the database "mode" column historically (and currently)
3689 # only stores the "rw" (for user) part of the string.
3690 # FUTURE: It might make more sense to persist the raw
3691 # octal mode (or perhaps the final full CVS form) instead of
3692 # this half-converted form, but it isn't currently worth the
3693 # backwards compatibility headaches.
3695 $mode=~/^\d\d(\d)\d{3}$/;
3699 $dbMode .= "r" if ( $userBits & 4 );
3700 $dbMode .= "w" if ( $userBits & 2 );
3701 $dbMode .= "x" if ( $userBits & 1 );
3702 $dbMode = "rw" if ( $dbMode eq "" );
3711 my $revision = shift;
3712 my $filehash = shift;
3713 my $commithash = shift;
3714 my $modified = shift;
3717 my $tablename = $self->tablename("revision");
3719 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3720 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3728 my $tablename = $self->tablename("commitmsgs");
3730 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3731 $insert_mergelog->execute($key, $value);
3737 my $tablename = $self->tablename("head");
3739 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1);
3740 $delete_head->execute();
3747 my $revision = shift;
3748 my $filehash = shift;
3749 my $commithash = shift;
3750 my $modified = shift;
3753 my $tablename = $self->tablename("head");
3755 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3756 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3763 my $tablename = $self->tablename("properties");
3765 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3766 $db_query->execute($key);
3767 my ( $value ) = $db_query->fetchrow_array;
3777 my $tablename = $self->tablename("properties");
3779 my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1);
3780 $db_query->execute($value, $key);
3782 unless ( $db_query->rows )
3784 $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3785 $db_query->execute($key, $value);
3799 my $tablename = $self->tablename("head");
3801 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
3803 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1);
3804 $db_query->execute();
3807 while ( my $file = $db_query->fetchrow_hashref )
3811 $file->{revision} = "1.$file->{revision}"
3816 $self->{gethead_cache} = $tree;
3823 Returns a reference to an array of getmeta structures, one
3824 per file in the specified tree hash.
3830 my ($self,$hash) = @_;
3834 return $self->gethead();
3839 open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
3840 or die("Cannot call git-ls-tree : $!");
3848 foreach $line (@files)
3851 unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3853 die("Couldn't process git-ls-tree line : $_");
3856 my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
3857 push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
3863 =head2 getRevisionDirMap
3865 A "revision dir map" contains all the plain-file filenames associated
3866 with a particular revision (treeish), organized by directory:
3868 $type = $out->{$dir}{$fullName}
3870 The type of each is "F" (for ordinary file) or "D" (for directory,
3871 for which the map $out->{$fullName} will also exist).
3875 sub getRevisionDirMap
3879 if(!defined($self->{revisionDirMapCache}))
3881 $self->{revisionDirMapCache}={};
3884 # Get file list (previously cached results are dependent on HEAD,
3885 # but are early in each case):
3888 if( !defined($ver) || $ver eq "" )
3891 if( defined($self->{revisionDirMapCache}{$cacheKey}) )
3893 return $self->{revisionDirMapCache}{$cacheKey};
3896 my @head = @{$self->gethead()};
3897 foreach my $file ( @head )
3899 next if ( $file->{filehash} eq "deleted" );
3901 push @fileList,$file->{name};
3906 my ($hash)=$self->lookupCommitRef($ver);
3907 if( !defined($hash) )
3913 if( defined($self->{revisionDirMapCache}{$cacheKey}) )
3915 return $self->{revisionDirMapCache}{$cacheKey};
3918 open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
3919 or die("Cannot call git-ls-tree : $!");
3921 while ( <$filePipe> )
3924 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3926 die("Couldn't process git-ls-tree line : $_");
3929 my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
3931 push @fileList, $git_filename;
3936 # Convert to normalized form:
3939 foreach $file (@fileList)
3941 my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
3942 $dir='' if(!defined($dir));
3944 # parent directories:
3945 # ... create empty dir maps for parent dirs:
3947 while(!defined($revMap{$td}))
3951 my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
3952 $tp='' if(!defined($tp));
3955 # ... add children to parent maps (now that they exist):
3959 my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
3960 $tp='' if(!defined($tp));
3962 if(defined($revMap{$tp}{$td}))
3964 if($revMap{$tp}{$td} ne 'D')
3966 die "Weird file/directory inconsistency in $cacheKey";
3970 $revMap{$tp}{$td}='D';
3976 $revMap{$dir}{$file}='F';
3980 $self->{revisionDirMapCache}{$cacheKey}=\%revMap;
3981 return $self->{revisionDirMapCache}{$cacheKey};
3986 See also gethistorydense().
3993 my $filename = shift;
3994 my $revFilter = shift;
3996 my $tablename = $self->tablename("revision");
3999 # TODO: date, state, or by specific logins filters?
4000 # TODO: Handle comma-separated list of revFilter items, each item
4001 # can be a range [only case currently handled] or individual
4002 # rev or branch or "branch.".
4003 # TODO: Adjust $db_query WHERE clause based on revFilter, instead of
4004 # manually filtering the results of the query?
4005 my ( $minrev, $maxrev );
4006 if( defined($revFilter) and
4007 $state->{opt}{r} =~ /^(1.(\d+))?(::?)(1.(\d.+))?$/ )
4012 $minrev++ if ( defined($minrev) and $control eq "::" );
4015 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
4016 $db_query->execute($filename);
4020 while ( my $file = $db_query->fetchrow_hashref )
4023 if( defined($minrev) and $file->{revision} < $minrev )
4027 if( defined($maxrev) and $file->{revision} > $maxrev )
4032 $file->{revision} = "1." . $file->{revision};
4036 return ($tree,$totalRevs);
4041 This function takes a filename (with path) argument and returns a hashref of
4042 metadata for that file.
4044 There are several ways $revision can be specified:
4046 - A reference to hash that contains a "tag" that is the
4047 actual revision (one of the below). TODO: Also allow it to
4048 specify a "date" in the hash.
4049 - undef, to refer to the latest version on the main branch.
4050 - Full CVS client revision number (mapped to integer in DB, without the
4052 - Complex CVS-compatible "special" revision number for
4053 non-linear history (see comment below)
4054 - git commit sha1 hash
4055 - branch or tag name
4062 my $filename = shift;
4063 my $revision = shift;
4064 my $tablename_rev = $self->tablename("revision");
4065 my $tablename_head = $self->tablename("head");
4067 if ( ref($revision) eq "HASH" )
4069 $revision = $revision->{tag};
4072 # Overview of CVS revision numbers:
4074 # General CVS numbering scheme:
4075 # - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.
4076 # - Result of "cvs checkin -r" (possible, but not really
4077 # recommended): "2.1", "2.2", etc
4078 # - Branch tag: "1.2.0.n", where "1.2" is revision it was branched
4079 # from, "0" is a magic placeholder that identifies it as a
4080 # branch tag instead of a version tag, and n is 2 times the
4081 # branch number off of "1.2", starting with "2".
4082 # - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"
4083 # is branch number off of "1.2" (like n above), and "x" is
4084 # the version number on the branch.
4085 # - Branches can branch off of branches: "1.3.2.7.4.1" (even number
4087 # - Odd "n"s are used by "vendor branches" that result
4088 # from "cvs import". Vendor branches have additional
4089 # strangeness in the sense that the main rcs "head" of the main
4090 # branch will (temporarily until first normal commit) point
4091 # to the version on the vendor branch, rather than the actual
4092 # main branch. (FUTURE: This may provide an opportunity
4093 # to use "strange" revision numbers for fast-forward-merged
4094 # branch tip when CVS client is asking for the main branch.)
4096 # git-cvsserver CVS-compatible special numbering schemes:
4097 # - Currently git-cvsserver only tries to be identical to CVS for
4098 # simple "1.x" numbers on the "main" branch (as identified
4099 # by the module name that was originally cvs checkout'ed).
4100 # - The database only stores the "x" part, for historical reasons.
4101 # But most of the rest of the cvsserver preserves
4102 # and thinks using the full revision number.
4103 # - To handle non-linear history, it uses a version of the form
4104 # "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely
4105 # identify this as a special revision number, and there are
4106 # 20 b's that together encode the sha1 git commit from which
4107 # this version of this file originated. Each b is
4108 # the numerical value of the corresponding byte plus
4110 # - "plus 100" avoids "0"s, and also reduces the
4111 # likelyhood of a collision in the case that someone someday
4112 # writes an import tool that tries to preserve original
4113 # CVS revision numbers, and the original CVS data had done
4114 # lots of branches off of branches and other strangeness to
4115 # end up with a real version number that just happens to look
4116 # like this special revision number form. Also, if needed
4117 # there are several ways to extend/identify alternative encodings
4118 # within the "2.1.1.2000" part if necessary.
4119 # - Unlike real CVS revisions, you can't really reconstruct what
4120 # relation a revision of this form has to other revisions.
4121 # - FUTURE: TODO: Rework database somehow to make up and remember
4122 # fully-CVS-compatible branches and branch version numbers.
4125 if ( defined($revision) )
4127 if ( $revision =~ /^1\.(\d+)$/ )
4131 $db_query = $self->{dbh}->prepare_cached(
4132 "SELECT * FROM $tablename_rev WHERE name=? AND revision=?",
4134 $db_query->execute($filename, $intRev);
4135 $meta = $db_query->fetchrow_hashref;
4137 elsif ( $revision =~ /^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/ )
4139 my ($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);
4140 $commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;
4141 if($commitHash=~/^[0-9a-f]{40}$/)
4143 return $self->getMetaFromCommithash($filename,$commitHash);
4146 # error recovery: fall back on head version below
4147 print "E Failed to find $filename version=$revision or commit=$commitHash\n";
4148 $log->warning("failed get $revision with commithash=$commitHash");
4151 elsif ( $revision =~ /^[0-9a-f]{40}$/ )
4153 # Try DB first. This is mostly only useful for req_annotate(),
4154 # which only calls this for stuff that should already be in
4155 # the DB. It is fairly likely to be a waste of time
4156 # in most other cases [unless the file happened to be
4157 # modified in $revision specifically], but
4158 # it is probably in the noise compared to how long
4159 # getMetaFromCommithash() will take.
4161 $db_query = $self->{dbh}->prepare_cached(
4162 "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
4164 $db_query->execute($filename, $revision);
4165 $meta = $db_query->fetchrow_hashref;
4169 my($revCommit)=$self->lookupCommitRef($revision);
4170 if($revCommit=~/^[0-9a-f]{40}$/)
4172 return $self->getMetaFromCommithash($filename,$revCommit);
4175 # error recovery: nothing found:
4176 print "E Failed to find $filename version=$revision\n";
4177 $log->warning("failed get $revision");
4183 my($revCommit)=$self->lookupCommitRef($revision);
4184 if($revCommit=~/^[0-9a-f]{40}$/)
4186 return $self->getMetaFromCommithash($filename,$revCommit);
4189 # error recovery: fall back on head version below
4190 print "E Failed to find $filename version=$revision\n";
4191 $log->warning("failed get $revision");
4192 undef $revision; # Allow fallback
4196 if(!defined($revision))
4199 $db_query = $self->{dbh}->prepare_cached(
4200 "SELECT * FROM $tablename_head WHERE name=?",{},1);
4201 $db_query->execute($filename);
4202 $meta = $db_query->fetchrow_hashref;
4207 $meta->{revision} = "1.$meta->{revision}";
4212 sub getMetaFromCommithash
4215 my $filename = shift;
4216 my $revCommit = shift;
4218 # NOTE: This function doesn't scale well (lots of forks), especially
4219 # if you have many files that have not been modified for many commits
4220 # (each git-rev-parse redoes a lot of work for each file
4221 # that theoretically could be done in parallel by smarter
4224 # TODO: Possible optimization strategies:
4225 # - Solve the issue of assigning and remembering "real" CVS
4226 # revision numbers for branches, and ensure the
4227 # data structure can do this efficiently. Perhaps something
4228 # similar to "git notes", and carefully structured to take
4229 # advantage same-sha1-is-same-contents, to roll the same
4230 # unmodified subdirectory data onto multiple commits?
4231 # - Write and use a C tool that is like git-blame, but
4232 # operates on multiple files with file granularity, instead
4233 # of one file with line granularity. Cache
4234 # most-recently-modified in $self->{commitRefCache}{$revCommit}.
4235 # Try to be intelligent about how many files we do with
4236 # one fork (perhaps one directory at a time, without recursion,
4237 # and/or include directory as one line item, recurse from here
4238 # instead of in C tool?).
4239 # - Perhaps we could ask the DB for (filename,fileHash),
4240 # and just guess that it is correct (that the file hadn't
4241 # changed between $revCommit and the found commit, then
4242 # changed back, confusing anything trying to interpret
4243 # history). Probably need to add another index to revisions
4244 # DB table for this.
4245 # - NOTE: Trying to store all (commit,file) keys in DB [to
4246 # find "lastModfiedCommit] (instead of
4247 # just files that changed in each commit as we do now) is
4248 # probably not practical from a disk space perspective.
4250 # Does the file exist in $revCommit?
4251 # TODO: Include file hash in dirmap cache.
4252 my($dirMap)=$self->getRevisionDirMap($revCommit);
4253 my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
4258 if( !defined($dirMap->{$dir}) ||
4259 !defined($dirMap->{$dir}{$filename}) )
4261 my($fileHash)="deleted";
4264 $retVal->{name}=$filename;
4265 $retVal->{filehash}=$fileHash;
4267 # not needed and difficult to compute:
4268 $retVal->{revision}="0"; # $revision;
4269 $retVal->{commithash}=$revCommit;
4270 #$retVal->{author}=$commit->{author};
4271 #$retVal->{modified}=convertToCvsDate($commit->{date});
4272 #$retVal->{mode}=convertToDbMode($mode);
4277 my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
4279 if(!($fileHash=~/^[0-9a-f]{40}$/))
4281 die "Invalid fileHash '$fileHash' looking up"
4282 ." '$revCommit:$filename'\n";
4285 # information about most recent commit to modify $filename:
4286 open(my $gitLogPipe, '-|', 'git', 'rev-list',
4287 '--max-count=1', '--pretty', '--parents',
4288 $revCommit, '--', $filename)
4289 or die "Cannot call git-rev-list: $!";
4290 my @commits=readCommits($gitLogPipe);
4292 if(scalar(@commits)!=1)
4294 die "Can't find most recent commit changing $filename\n";
4296 my($commit)=$commits[0];
4297 if( !defined($commit) || !defined($commit->{hash}) )
4302 # does this (commit,file) have a real assigned CVS revision number?
4303 my $tablename_rev = $self->tablename("revision");
4305 $db_query = $self->{dbh}->prepare_cached(
4306 "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
4308 $db_query->execute($filename, $commit->{hash});
4309 my($meta)=$db_query->fetchrow_hashref;
4312 $meta->{revision} = "1.$meta->{revision}";
4316 # fall back on special revision number
4317 my($revision)=$commit->{hash};
4318 $revision=~s/(..)/'.' . (hex($1)+100)/eg;
4319 $revision="2.1.1.2000$revision";
4321 # meta data about $filename:
4322 open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
4323 $commit->{hash}, '--', $filename)
4324 or die("Cannot call git-ls-tree : $!");
4328 if(defined(<$filePipe>))
4330 die "Expected only a single file for git-ls-tree $filename\n";
4335 unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
4337 die("Couldn't process git-ls-tree line : $line\n");
4339 my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
4343 $retVal->{name}=$filename;
4344 $retVal->{revision}=$revision;
4345 $retVal->{filehash}=$fileHash;
4346 $retVal->{commithash}=$revCommit;
4347 $retVal->{author}=$commit->{author};
4348 $retVal->{modified}=convertToCvsDate($commit->{date});
4349 $retVal->{mode}=convertToDbMode($mode);
4354 =head2 lookupCommitRef
4356 Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches
4357 the result so looking it up again is fast.
4366 my $commitHash = $self->{commitRefCache}{$ref};
4367 if(defined($commitHash))
4372 $commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
4373 $self->unescapeRefName($ref));
4374 $commitHash=~s/\s*$//;
4375 if(!($commitHash=~/^[0-9a-f]{40}$/))
4380 if( defined($commitHash) )
4382 my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
4383 if( ! ($type=~/^commit\s*$/ ) )
4388 if(defined($commitHash))
4390 $self->{commitRefCache}{$ref}=$commitHash;
4395 =head2 clearCommitRefCaches
4397 Clears cached commit cache (sha1's for various tags/abbeviations/etc),
4402 sub clearCommitRefCaches
4405 $self->{commitRefCache} = {};
4406 $self->{revisionDirMapCache} = undef;
4407 $self->{gethead_cache} = undef;
4410 =head2 commitmessage
4412 this function takes a commithash and returns the commit message for that commit
4418 my $commithash = shift;
4419 my $tablename = $self->tablename("commitmsgs");
4421 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
4424 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
4425 $db_query->execute($commithash);
4427 my ( $message ) = $db_query->fetchrow_array;
4429 if ( defined ( $message ) )
4431 $message .= " " if ( $message =~ /\n$/ );
4435 my @lines = safe_pipe_capture("git", "cat-file", "commit", $commithash);
4436 shift @lines while ( $lines[0] =~ /\S/ );
4437 $message = join("",@lines);
4438 $message .= " " if ( $message =~ /\n$/ );
4442 =head2 gethistorydense
4444 This function takes a filename (with path) argument and returns an arrayofarrays
4445 containing revision,filehash,commithash ordered by revision descending.
4447 This version of gethistory skips deleted entries -- so it is useful for annotate.
4448 The 'dense' part is a reference to a '--dense' option available for git-rev-list
4449 and other git tools that depend on it.
4457 my $filename = shift;
4458 my $tablename = $self->tablename("revision");
4461 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
4462 $db_query->execute($filename);
4464 my $result = $db_query->fetchall_arrayref;
4467 for($i=0 ; $i<scalar(@$result) ; $i++)
4469 $result->[$i][0]="1." . $result->[$i][0];
4475 =head2 escapeRefName
4477 Apply an escape mechanism to compensate for characters that
4478 git ref names can have that CVS tags can not.
4483 my($self,$refName)=@_;
4485 # CVS officially only allows [-_A-Za-z0-9] in tag names (or in
4486 # many contexts it can also be a CVS revision number).
4488 # Git tags commonly use '/' and '.' as well, but also handle
4489 # anything else just in case:
4493 # = "_-u-" For underscore, in case someone wants a literal "_-" in
4495 # = "_-xx-" Where "xx" is the hexadecimal representation of the
4496 # desired ASCII character byte. (for anything else)
4498 if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
4500 $refName=~s/_-/_-u--/g;
4501 $refName=~s/\./_-p-/g;
4502 $refName=~s%/%_-s-%g;
4503 $refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
4507 =head2 unescapeRefName
4509 Undo an escape mechanism to compensate for characters that
4510 git ref names can have that CVS tags can not.
4515 my($self,$refName)=@_;
4517 # see escapeRefName() for description of escape mechanism.
4519 $refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
4522 # TODO: Perhaps use git check-ref-format, with an in-process cache of
4524 if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
4525 ( $refName=~m%[/.]$% ) ||
4526 ( $refName=~/\.lock$/ ) ||
4527 ( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) ) # matching }
4530 $log->warn("illegal refName: $refName");
4536 sub unescapeRefNameChar
4552 elsif($char=~/^[0-9a-f][0-9a-f]$/)
4554 $char=chr(hex($char));
4558 # Error case: Maybe it has come straight from user, and
4559 # wasn't supposed to be escaped? Restore it the way we got it:
4568 from Array::PAT - mimics the in_array() function
4569 found in PHP. Yuck but works for small arrays.
4574 my ($check, @array) = @_;
4576 foreach my $test (@array){
4577 if($check eq $test){
4584 =head2 safe_pipe_capture
4586 an alternative to `command` that allows input to be passed as an array
4587 to work around shell problems with weird characters in arguments
4590 sub safe_pipe_capture {
4594 if (my $pid = open my $child, '-|') {
4595 @output = (<$child>);
4596 close $child or die join(' ',@_).": $! $?";
4598 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
4600 return wantarray ? @output : join('',@output);
4603 =head2 mangle_dirname
4605 create a string from a directory name that is suitable to use as
4606 part of a filename, mainly by converting all chars except \w.- to _
4609 sub mangle_dirname {
4610 my $dirname = shift;
4611 return unless defined $dirname;
4613 $dirname =~ s/[^\w.-]/_/g;
4618 =head2 mangle_tablename
4620 create a string from a that is suitable to use as part of an SQL table
4621 name, mainly by converting all chars except \w to _
4624 sub mangle_tablename {
4625 my $tablename = shift;
4626 return unless defined $tablename;
4628 $tablename =~ s/[^\w_]/_/g;