t3001: test ls-files -o ignored/dir
[git/gitweb.git] / git-cvsserver.perl
blob15da2789e011f76c848f4e453cdf41837d4a75f7
1 #!/usr/bin/perl
3 ####
4 #### This application is a CVS emulation layer for git.
5 #### It is intended for clients to connect over SSH.
6 #### See the documentation for more details.
7 ####
8 #### Copyright The Open University UK - 2006.
9 ####
10 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
11 #### Martin Langhoff <martin@catalyst.net.nz>
12 ####
13 ####
14 #### Released under the GNU Public License, version 2.
15 ####
16 ####
18 use strict;
19 use warnings;
20 use bytes;
22 use Fcntl;
23 use File::Temp qw/tempdir tempfile/;
24 use File::Path qw/rmtree/;
25 use File::Basename;
26 use Getopt::Long qw(:config require_order no_ignore_case);
28 my $VERSION = '@@GIT_VERSION@@';
30 my $log = GITCVS::log->new();
31 my $cfg;
33 my $DATE_LIST = {
34 Jan => "01",
35 Feb => "02",
36 Mar => "03",
37 Apr => "04",
38 May => "05",
39 Jun => "06",
40 Jul => "07",
41 Aug => "08",
42 Sep => "09",
43 Oct => "10",
44 Nov => "11",
45 Dec => "12",
48 # Enable autoflush for STDOUT (otherwise the whole thing falls apart)
49 $| = 1;
51 #### Definition and mappings of functions ####
53 my $methods = {
54 'Root' => \&req_Root,
55 'Valid-responses' => \&req_Validresponses,
56 'valid-requests' => \&req_validrequests,
57 'Directory' => \&req_Directory,
58 'Entry' => \&req_Entry,
59 'Modified' => \&req_Modified,
60 'Unchanged' => \&req_Unchanged,
61 'Questionable' => \&req_Questionable,
62 'Argument' => \&req_Argument,
63 'Argumentx' => \&req_Argument,
64 'expand-modules' => \&req_expandmodules,
65 'add' => \&req_add,
66 'remove' => \&req_remove,
67 'co' => \&req_co,
68 'update' => \&req_update,
69 'ci' => \&req_ci,
70 'diff' => \&req_diff,
71 'log' => \&req_log,
72 'rlog' => \&req_log,
73 'tag' => \&req_CATCHALL,
74 'status' => \&req_status,
75 'admin' => \&req_CATCHALL,
76 'history' => \&req_CATCHALL,
77 'watchers' => \&req_EMPTY,
78 'editors' => \&req_EMPTY,
79 'noop' => \&req_EMPTY,
80 'annotate' => \&req_annotate,
81 'Global_option' => \&req_Globaloption,
82 #'annotate' => \&req_CATCHALL,
85 ##############################################
88 # $state holds all the bits of information the clients sends us that could
89 # potentially be useful when it comes to actually _doing_ something.
90 my $state = { prependdir => '' };
92 # Work is for managing temporary working directory
93 my $work =
95 state => undef, # undef, 1 (empty), 2 (with stuff)
96 workDir => undef,
97 index => undef,
98 emptyDir => undef,
99 tmpDir => undef
102 $log->info("--------------- STARTING -----------------");
104 my $usage =
105 "Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n".
106 " --base-path <path> : Prepend to requested CVSROOT\n".
107 " --strict-paths : Don't allow recursing into subdirectories\n".
108 " --export-all : Don't check for gitcvs.enabled in config\n".
109 " --version, -V : Print version information and exit\n".
110 " --help, -h, -H : Print usage information and exit\n".
111 "\n".
112 "<directory> ... is a list of allowed directories. If no directories\n".
113 "are given, all are allowed. This is an additional restriction, gitcvs\n".
114 "access still needs to be enabled by the gitcvs.enabled config option.\n";
116 my @opts = ( 'help|h|H', 'version|V',
117 'base-path=s', 'strict-paths', 'export-all' );
118 GetOptions( $state, @opts )
119 or die $usage;
121 if ($state->{version}) {
122 print "git-cvsserver version $VERSION\n";
123 exit;
125 if ($state->{help}) {
126 print $usage;
127 exit;
130 my $TEMP_DIR = tempdir( CLEANUP => 1 );
131 $log->debug("Temporary directory is '$TEMP_DIR'");
133 $state->{method} = 'ext';
134 if (@ARGV) {
135 if ($ARGV[0] eq 'pserver') {
136 $state->{method} = 'pserver';
137 shift @ARGV;
138 } elsif ($ARGV[0] eq 'server') {
139 shift @ARGV;
143 # everything else is a directory
144 $state->{allowed_roots} = [ @ARGV ];
146 # don't export the whole system unless the users requests it
147 if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
148 die "--export-all can only be used together with an explicit whitelist\n";
151 # if we are called with a pserver argument,
152 # deal with the authentication cat before entering the
153 # main loop
154 if ($state->{method} eq 'pserver') {
155 my $line = <STDIN>; chomp $line;
156 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
157 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
159 my $request = $1;
160 $line = <STDIN>; chomp $line;
161 unless (req_Root('root', $line)) { # reuse Root
162 print "E Invalid root $line \n";
163 exit 1;
165 $line = <STDIN>; chomp $line;
166 unless ($line eq 'anonymous') {
167 print "E Only anonymous user allowed via pserver\n";
168 print "I HATE YOU\n";
169 exit 1;
171 $line = <STDIN>; chomp $line; # validate the password?
172 $line = <STDIN>; chomp $line;
173 unless ($line eq "END $request REQUEST") {
174 die "E Do not understand $line -- expecting END $request REQUEST\n";
176 print "I LOVE YOU\n";
177 exit if $request eq 'VERIFICATION'; # cvs login
178 # and now back to our regular programme...
181 # Keep going until the client closes the connection
182 while (<STDIN>)
184 chomp;
186 # Check to see if we've seen this method, and call appropriate function.
187 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
189 # use the $methods hash to call the appropriate sub for this command
190 #$log->info("Method : $1");
191 &{$methods->{$1}}($1,$2);
192 } else {
193 # log fatal because we don't understand this function. If this happens
194 # we're fairly screwed because we don't know if the client is expecting
195 # a response. If it is, the client will hang, we'll hang, and the whole
196 # thing will be custard.
197 $log->fatal("Don't understand command $_\n");
198 die("Unknown command $_");
202 $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
203 $log->info("--------------- FINISH -----------------");
205 chdir '/';
206 exit 0;
208 # Magic catchall method.
209 # This is the method that will handle all commands we haven't yet
210 # implemented. It simply sends a warning to the log file indicating a
211 # command that hasn't been implemented has been invoked.
212 sub req_CATCHALL
214 my ( $cmd, $data ) = @_;
215 $log->warn("Unhandled command : req_$cmd : $data");
218 # This method invariably succeeds with an empty response.
219 sub req_EMPTY
221 print "ok\n";
224 # Root pathname \n
225 # Response expected: no. Tell the server which CVSROOT to use. Note that
226 # pathname is a local directory and not a fully qualified CVSROOT variable.
227 # pathname must already exist; if creating a new root, use the init
228 # request, not Root. pathname does not include the hostname of the server,
229 # how to access the server, etc.; by the time the CVS protocol is in use,
230 # connection, authentication, etc., are already taken care of. The Root
231 # request must be sent only once, and it must be sent before any requests
232 # other than Valid-responses, valid-requests, UseUnchanged, Set or init.
233 sub req_Root
235 my ( $cmd, $data ) = @_;
236 $log->debug("req_Root : $data");
238 unless ($data =~ m#^/#) {
239 print "error 1 Root must be an absolute pathname\n";
240 return 0;
243 my $cvsroot = $state->{'base-path'} || '';
244 $cvsroot =~ s#/+$##;
245 $cvsroot .= $data;
247 if ($state->{CVSROOT}
248 && ($state->{CVSROOT} ne $cvsroot)) {
249 print "error 1 Conflicting roots specified\n";
250 return 0;
253 $state->{CVSROOT} = $cvsroot;
255 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
257 if (@{$state->{allowed_roots}}) {
258 my $allowed = 0;
259 foreach my $dir (@{$state->{allowed_roots}}) {
260 next unless $dir =~ m#^/#;
261 $dir =~ s#/+$##;
262 if ($state->{'strict-paths'}) {
263 if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
264 $allowed = 1;
265 last;
267 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
268 $allowed = 1;
269 last;
273 unless ($allowed) {
274 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
275 print "E \n";
276 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
277 return 0;
281 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
282 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
283 print "E \n";
284 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
285 return 0;
288 my @gitvars = `git config -l`;
289 if ($?) {
290 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
291 print "E \n";
292 print "error 1 - problem executing git-config\n";
293 return 0;
295 foreach my $line ( @gitvars )
297 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
298 unless ($2) {
299 $cfg->{$1}{$3} = $4;
300 } else {
301 $cfg->{$1}{$2}{$3} = $4;
305 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
306 || $cfg->{gitcvs}{enabled});
307 unless ($state->{'export-all'} ||
308 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
309 print "E GITCVS emulation needs to be enabled on this repo\n";
310 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
311 print "E \n";
312 print "error 1 GITCVS emulation disabled\n";
313 return 0;
316 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
317 if ( $logfile )
319 $log->setfile($logfile);
320 } else {
321 $log->nofile();
324 return 1;
327 # Global_option option \n
328 # Response expected: no. Transmit one of the global options `-q', `-Q',
329 # `-l', `-t', `-r', or `-n'. option must be one of those strings, no
330 # variations (such as combining of options) are allowed. For graceful
331 # handling of valid-requests, it is probably better to make new global
332 # options separate requests, rather than trying to add them to this
333 # request.
334 sub req_Globaloption
336 my ( $cmd, $data ) = @_;
337 $log->debug("req_Globaloption : $data");
338 $state->{globaloptions}{$data} = 1;
341 # Valid-responses request-list \n
342 # Response expected: no. Tell the server what responses the client will
343 # accept. request-list is a space separated list of tokens.
344 sub req_Validresponses
346 my ( $cmd, $data ) = @_;
347 $log->debug("req_Validresponses : $data");
349 # TODO : re-enable this, currently it's not particularly useful
350 #$state->{validresponses} = [ split /\s+/, $data ];
353 # valid-requests \n
354 # Response expected: yes. Ask the server to send back a Valid-requests
355 # response.
356 sub req_validrequests
358 my ( $cmd, $data ) = @_;
360 $log->debug("req_validrequests");
362 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
363 $log->debug("SEND : ok");
365 print "Valid-requests " . join(" ",keys %$methods) . "\n";
366 print "ok\n";
369 # Directory local-directory \n
370 # Additional data: repository \n. Response expected: no. Tell the server
371 # what directory to use. The repository should be a directory name from a
372 # previous server response. Note that this both gives a default for Entry
373 # and Modified and also for ci and the other commands; normal usage is to
374 # send Directory for each directory in which there will be an Entry or
375 # Modified, and then a final Directory for the original directory, then the
376 # command. The local-directory is relative to the top level at which the
377 # command is occurring (i.e. the last Directory which is sent before the
378 # command); to indicate that top level, `.' should be sent for
379 # local-directory.
380 sub req_Directory
382 my ( $cmd, $data ) = @_;
384 my $repository = <STDIN>;
385 chomp $repository;
388 $state->{localdir} = $data;
389 $state->{repository} = $repository;
390 $state->{path} = $repository;
391 $state->{path} =~ s/^$state->{CVSROOT}\///;
392 $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
393 $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
395 $state->{directory} = $state->{localdir};
396 $state->{directory} = "" if ( $state->{directory} eq "." );
397 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
399 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
401 $log->info("Setting prepend to '$state->{path}'");
402 $state->{prependdir} = $state->{path};
403 foreach my $entry ( keys %{$state->{entries}} )
405 $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
406 delete $state->{entries}{$entry};
410 if ( defined ( $state->{prependdir} ) )
412 $log->debug("Prepending '$state->{prependdir}' to state|directory");
413 $state->{directory} = $state->{prependdir} . $state->{directory}
415 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
418 # Entry entry-line \n
419 # Response expected: no. Tell the server what version of a file is on the
420 # local machine. The name in entry-line is a name relative to the directory
421 # most recently specified with Directory. If the user is operating on only
422 # some files in a directory, Entry requests for only those files need be
423 # included. If an Entry request is sent without Modified, Is-modified, or
424 # Unchanged, it means the file is lost (does not exist in the working
425 # directory). If both Entry and one of Modified, Is-modified, or Unchanged
426 # are sent for the same file, Entry must be sent first. For a given file,
427 # one can send Modified, Is-modified, or Unchanged, but not more than one
428 # of these three.
429 sub req_Entry
431 my ( $cmd, $data ) = @_;
433 #$log->debug("req_Entry : $data");
435 my @data = split(/\//, $data);
437 $state->{entries}{$state->{directory}.$data[1]} = {
438 revision => $data[2],
439 conflict => $data[3],
440 options => $data[4],
441 tag_or_date => $data[5],
444 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
447 # Questionable filename \n
448 # Response expected: no. Additional data: no. Tell the server to check
449 # whether filename should be ignored, and if not, next time the server
450 # sends responses, send (in a M response) `?' followed by the directory and
451 # filename. filename must not contain `/'; it needs to be a file in the
452 # directory named by the most recent Directory request.
453 sub req_Questionable
455 my ( $cmd, $data ) = @_;
457 $log->debug("req_Questionable : $data");
458 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
461 # add \n
462 # Response expected: yes. Add a file or directory. This uses any previous
463 # Argument, Directory, Entry, or Modified requests, if they have been sent.
464 # The last Directory sent specifies the working directory at the time of
465 # the operation. To add a directory, send the directory to be added using
466 # Directory and Argument requests.
467 sub req_add
469 my ( $cmd, $data ) = @_;
471 argsplit("add");
473 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
474 $updater->update();
476 argsfromdir($updater);
478 my $addcount = 0;
480 foreach my $filename ( @{$state->{args}} )
482 $filename = filecleanup($filename);
484 my $meta = $updater->getmeta($filename);
485 my $wrev = revparse($filename);
487 if ($wrev && $meta && ($wrev < 0))
489 # previously removed file, add back
490 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
492 print "MT +updated\n";
493 print "MT text U \n";
494 print "MT fname $filename\n";
495 print "MT newline\n";
496 print "MT -updated\n";
498 unless ( $state->{globaloptions}{-n} )
500 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
502 print "Created $dirpart\n";
503 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
505 # this is an "entries" line
506 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
507 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
508 print "/$filepart/1.$meta->{revision}//$kopts/\n";
509 # permissions
510 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
511 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
512 # transmit file
513 transmitfile($meta->{filehash});
516 next;
519 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
521 print "E cvs add: nothing known about `$filename'\n";
522 next;
524 # TODO : check we're not squashing an already existing file
525 if ( defined ( $state->{entries}{$filename}{revision} ) )
527 print "E cvs add: `$filename' has already been entered\n";
528 next;
531 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
533 print "E cvs add: scheduling file `$filename' for addition\n";
535 print "Checked-in $dirpart\n";
536 print "$filename\n";
537 my $kopts = kopts_from_path($filename,"file",
538 $state->{entries}{$filename}{modified_filename});
539 print "/$filepart/0//$kopts/\n";
541 my $requestedKopts = $state->{opt}{k};
542 if(defined($requestedKopts))
544 $requestedKopts = "-k$requestedKopts";
546 else
548 $requestedKopts = "";
550 if( $kopts ne $requestedKopts )
552 $log->warn("Ignoring requested -k='$requestedKopts'"
553 . " for '$filename'; detected -k='$kopts' instead");
554 #TODO: Also have option to send warning to user?
557 $addcount++;
560 if ( $addcount == 1 )
562 print "E cvs add: use `cvs commit' to add this file permanently\n";
564 elsif ( $addcount > 1 )
566 print "E cvs add: use `cvs commit' to add these files permanently\n";
569 print "ok\n";
572 # remove \n
573 # Response expected: yes. Remove a file. This uses any previous Argument,
574 # Directory, Entry, or Modified requests, if they have been sent. The last
575 # Directory sent specifies the working directory at the time of the
576 # operation. Note that this request does not actually do anything to the
577 # repository; the only effect of a successful remove request is to supply
578 # the client with a new entries line containing `-' to indicate a removed
579 # file. In fact, the client probably could perform this operation without
580 # contacting the server, although using remove may cause the server to
581 # perform a few more checks. The client sends a subsequent ci request to
582 # actually record the removal in the repository.
583 sub req_remove
585 my ( $cmd, $data ) = @_;
587 argsplit("remove");
589 # Grab a handle to the SQLite db and do any necessary updates
590 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
591 $updater->update();
593 #$log->debug("add state : " . Dumper($state));
595 my $rmcount = 0;
597 foreach my $filename ( @{$state->{args}} )
599 $filename = filecleanup($filename);
601 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
603 print "E cvs remove: file `$filename' still in working directory\n";
604 next;
607 my $meta = $updater->getmeta($filename);
608 my $wrev = revparse($filename);
610 unless ( defined ( $wrev ) )
612 print "E cvs remove: nothing known about `$filename'\n";
613 next;
616 if ( defined($wrev) and $wrev < 0 )
618 print "E cvs remove: file `$filename' already scheduled for removal\n";
619 next;
622 unless ( $wrev == $meta->{revision} )
624 # TODO : not sure if the format of this message is quite correct.
625 print "E cvs remove: Up to date check failed for `$filename'\n";
626 next;
630 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
632 print "E cvs remove: scheduling `$filename' for removal\n";
634 print "Checked-in $dirpart\n";
635 print "$filename\n";
636 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
637 print "/$filepart/-1.$wrev//$kopts/\n";
639 $rmcount++;
642 if ( $rmcount == 1 )
644 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
646 elsif ( $rmcount > 1 )
648 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
651 print "ok\n";
654 # Modified filename \n
655 # Response expected: no. Additional data: mode, \n, file transmission. Send
656 # the server a copy of one locally modified file. filename is a file within
657 # the most recent directory sent with Directory; it must not contain `/'.
658 # If the user is operating on only some files in a directory, only those
659 # files need to be included. This can also be sent without Entry, if there
660 # is no entry for the file.
661 sub req_Modified
663 my ( $cmd, $data ) = @_;
665 my $mode = <STDIN>;
666 defined $mode
667 or (print "E end of file reading mode for $data\n"), return;
668 chomp $mode;
669 my $size = <STDIN>;
670 defined $size
671 or (print "E end of file reading size of $data\n"), return;
672 chomp $size;
674 # Grab config information
675 my $blocksize = 8192;
676 my $bytesleft = $size;
677 my $tmp;
679 # Get a filehandle/name to write it to
680 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
682 # Loop over file data writing out to temporary file.
683 while ( $bytesleft )
685 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
686 read STDIN, $tmp, $blocksize;
687 print $fh $tmp;
688 $bytesleft -= $blocksize;
691 close $fh
692 or (print "E failed to write temporary, $filename: $!\n"), return;
694 # Ensure we have something sensible for the file mode
695 if ( $mode =~ /u=(\w+)/ )
697 $mode = $1;
698 } else {
699 $mode = "rw";
702 # Save the file data in $state
703 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
704 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
705 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git hash-object $filename`;
706 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
708 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
711 # Unchanged filename \n
712 # Response expected: no. Tell the server that filename has not been
713 # modified in the checked out directory. The filename is a file within the
714 # most recent directory sent with Directory; it must not contain `/'.
715 sub req_Unchanged
717 my ( $cmd, $data ) = @_;
719 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
721 #$log->debug("req_Unchanged : $data");
724 # Argument text \n
725 # Response expected: no. Save argument for use in a subsequent command.
726 # Arguments accumulate until an argument-using command is given, at which
727 # point they are forgotten.
728 # Argumentx text \n
729 # Response expected: no. Append \n followed by text to the current argument
730 # being saved.
731 sub req_Argument
733 my ( $cmd, $data ) = @_;
735 # Argumentx means: append to last Argument (with a newline in front)
737 $log->debug("$cmd : $data");
739 if ( $cmd eq 'Argumentx') {
740 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
741 } else {
742 push @{$state->{arguments}}, $data;
746 # expand-modules \n
747 # Response expected: yes. Expand the modules which are specified in the
748 # arguments. Returns the data in Module-expansion responses. Note that the
749 # server can assume that this is checkout or export, not rtag or rdiff; the
750 # latter do not access the working directory and thus have no need to
751 # expand modules on the client side. Expand may not be the best word for
752 # what this request does. It does not necessarily tell you all the files
753 # contained in a module, for example. Basically it is a way of telling you
754 # which working directories the server needs to know about in order to
755 # handle a checkout of the specified modules. For example, suppose that the
756 # server has a module defined by
757 # aliasmodule -a 1dir
758 # That is, one can check out aliasmodule and it will take 1dir in the
759 # repository and check it out to 1dir in the working directory. Now suppose
760 # the client already has this module checked out and is planning on using
761 # the co request to update it. Without using expand-modules, the client
762 # would have two bad choices: it could either send information about all
763 # working directories under the current directory, which could be
764 # unnecessarily slow, or it could be ignorant of the fact that aliasmodule
765 # stands for 1dir, and neglect to send information for 1dir, which would
766 # lead to incorrect operation. With expand-modules, the client would first
767 # ask for the module to be expanded:
768 sub req_expandmodules
770 my ( $cmd, $data ) = @_;
772 argsplit();
774 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
776 unless ( ref $state->{arguments} eq "ARRAY" )
778 print "ok\n";
779 return;
782 foreach my $module ( @{$state->{arguments}} )
784 $log->debug("SEND : Module-expansion $module");
785 print "Module-expansion $module\n";
788 print "ok\n";
789 statecleanup();
792 # co \n
793 # Response expected: yes. Get files from the repository. This uses any
794 # previous Argument, Directory, Entry, or Modified requests, if they have
795 # been sent. Arguments to this command are module names; the client cannot
796 # know what directories they correspond to except by (1) just sending the
797 # co request, and then seeing what directory names the server sends back in
798 # its responses, and (2) the expand-modules request.
799 sub req_co
801 my ( $cmd, $data ) = @_;
803 argsplit("co");
805 # Provide list of modules, if -c was used.
806 if (exists $state->{opt}{c}) {
807 my $showref = `git show-ref --heads`;
808 for my $line (split '\n', $showref) {
809 if ( $line =~ m% refs/heads/(.*)$% ) {
810 print "M $1\t$1\n";
813 print "ok\n";
814 return 1;
817 my $module = $state->{args}[0];
818 $state->{module} = $module;
819 my $checkout_path = $module;
821 # use the user specified directory if we're given it
822 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
824 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
826 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
828 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
830 # Grab a handle to the SQLite db and do any necessary updates
831 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
832 $updater->update();
834 $checkout_path =~ s|/$||; # get rid of trailing slashes
836 # Eclipse seems to need the Clear-sticky command
837 # to prepare the 'Entries' file for the new directory.
838 print "Clear-sticky $checkout_path/\n";
839 print $state->{CVSROOT} . "/$module/\n";
840 print "Clear-static-directory $checkout_path/\n";
841 print $state->{CVSROOT} . "/$module/\n";
842 print "Clear-sticky $checkout_path/\n"; # yes, twice
843 print $state->{CVSROOT} . "/$module/\n";
844 print "Template $checkout_path/\n";
845 print $state->{CVSROOT} . "/$module/\n";
846 print "0\n";
848 # instruct the client that we're checking out to $checkout_path
849 print "E cvs checkout: Updating $checkout_path\n";
851 my %seendirs = ();
852 my $lastdir ='';
854 # recursive
855 sub prepdir {
856 my ($dir, $repodir, $remotedir, $seendirs) = @_;
857 my $parent = dirname($dir);
858 $dir =~ s|/+$||;
859 $repodir =~ s|/+$||;
860 $remotedir =~ s|/+$||;
861 $parent =~ s|/+$||;
862 $log->debug("announcedir $dir, $repodir, $remotedir" );
864 if ($parent eq '.' || $parent eq './') {
865 $parent = '';
867 # recurse to announce unseen parents first
868 if (length($parent) && !exists($seendirs->{$parent})) {
869 prepdir($parent, $repodir, $remotedir, $seendirs);
871 # Announce that we are going to modify at the parent level
872 if ($parent) {
873 print "E cvs checkout: Updating $remotedir/$parent\n";
874 } else {
875 print "E cvs checkout: Updating $remotedir\n";
877 print "Clear-sticky $remotedir/$parent/\n";
878 print "$repodir/$parent/\n";
880 print "Clear-static-directory $remotedir/$dir/\n";
881 print "$repodir/$dir/\n";
882 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
883 print "$repodir/$parent/\n";
884 print "Template $remotedir/$dir/\n";
885 print "$repodir/$dir/\n";
886 print "0\n";
888 $seendirs->{$dir} = 1;
891 foreach my $git ( @{$updater->gethead} )
893 # Don't want to check out deleted files
894 next if ( $git->{filehash} eq "deleted" );
896 my $fullName = $git->{name};
897 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
899 if (length($git->{dir}) && $git->{dir} ne './'
900 && $git->{dir} ne $lastdir ) {
901 unless (exists($seendirs{$git->{dir}})) {
902 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
903 $checkout_path, \%seendirs);
904 $lastdir = $git->{dir};
905 $seendirs{$git->{dir}} = 1;
907 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
910 # modification time of this file
911 print "Mod-time $git->{modified}\n";
913 # print some information to the client
914 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
916 print "M U $checkout_path/$git->{dir}$git->{name}\n";
917 } else {
918 print "M U $checkout_path/$git->{name}\n";
921 # instruct client we're sending a file to put in this path
922 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
924 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
926 # this is an "entries" line
927 my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
928 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
929 # permissions
930 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
932 # transmit file
933 transmitfile($git->{filehash});
936 print "ok\n";
938 statecleanup();
941 # update \n
942 # Response expected: yes. Actually do a cvs update command. This uses any
943 # previous Argument, Directory, Entry, or Modified requests, if they have
944 # been sent. The last Directory sent specifies the working directory at the
945 # time of the operation. The -I option is not used--files which the client
946 # can decide whether to ignore are not mentioned and the client sends the
947 # Questionable request for others.
948 sub req_update
950 my ( $cmd, $data ) = @_;
952 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
954 argsplit("update");
957 # It may just be a client exploring the available heads/modules
958 # in that case, list them as top level directories and leave it
959 # at that. Eclipse uses this technique to offer you a list of
960 # projects (heads in this case) to checkout.
962 if ($state->{module} eq '') {
963 my $showref = `git show-ref --heads`;
964 print "E cvs update: Updating .\n";
965 for my $line (split '\n', $showref) {
966 if ( $line =~ m% refs/heads/(.*)$% ) {
967 print "E cvs update: New directory `$1'\n";
970 print "ok\n";
971 return 1;
975 # Grab a handle to the SQLite db and do any necessary updates
976 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
978 $updater->update();
980 argsfromdir($updater);
982 #$log->debug("update state : " . Dumper($state));
984 my $last_dirname = "///";
986 # foreach file specified on the command line ...
987 foreach my $filename ( @{$state->{args}} )
989 $filename = filecleanup($filename);
991 $log->debug("Processing file $filename");
993 unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
995 my $cur_dirname = dirname($filename);
996 if ( $cur_dirname ne $last_dirname )
998 $last_dirname = $cur_dirname;
999 if ( $cur_dirname eq "" )
1001 $cur_dirname = ".";
1003 print "E cvs update: Updating $cur_dirname\n";
1007 # if we have a -C we should pretend we never saw modified stuff
1008 if ( exists ( $state->{opt}{C} ) )
1010 delete $state->{entries}{$filename}{modified_hash};
1011 delete $state->{entries}{$filename}{modified_filename};
1012 $state->{entries}{$filename}{unchanged} = 1;
1015 my $meta;
1016 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
1018 $meta = $updater->getmeta($filename, $1);
1019 } else {
1020 $meta = $updater->getmeta($filename);
1023 # If -p was given, "print" the contents of the requested revision.
1024 if ( exists ( $state->{opt}{p} ) ) {
1025 if ( defined ( $meta->{revision} ) ) {
1026 $log->info("Printing '$filename' revision " . $meta->{revision});
1028 transmitfile($meta->{filehash}, { print => 1 });
1031 next;
1034 if ( ! defined $meta )
1036 $meta = {
1037 name => $filename,
1038 revision => 0,
1039 filehash => 'added'
1043 my $oldmeta = $meta;
1045 my $wrev = revparse($filename);
1047 # If the working copy is an old revision, lets get that version too for comparison.
1048 if ( defined($wrev) and $wrev != $meta->{revision} )
1050 $oldmeta = $updater->getmeta($filename, $wrev);
1053 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
1055 # Files are up to date if the working copy and repo copy have the same revision,
1056 # and the working copy is unmodified _and_ the user hasn't specified -C
1057 next if ( defined ( $wrev )
1058 and defined($meta->{revision})
1059 and $wrev == $meta->{revision}
1060 and $state->{entries}{$filename}{unchanged}
1061 and not exists ( $state->{opt}{C} ) );
1063 # If the working copy and repo copy have the same revision,
1064 # but the working copy is modified, tell the client it's modified
1065 if ( defined ( $wrev )
1066 and defined($meta->{revision})
1067 and $wrev == $meta->{revision}
1068 and defined($state->{entries}{$filename}{modified_hash})
1069 and not exists ( $state->{opt}{C} ) )
1071 $log->info("Tell the client the file is modified");
1072 print "MT text M \n";
1073 print "MT fname $filename\n";
1074 print "MT newline\n";
1075 next;
1078 if ( $meta->{filehash} eq "deleted" )
1080 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1082 $log->info("Removing '$filename' from working copy (no longer in the repo)");
1084 print "E cvs update: `$filename' is no longer in the repository\n";
1085 # Don't want to actually _DO_ the update if -n specified
1086 unless ( $state->{globaloptions}{-n} ) {
1087 print "Removed $dirpart\n";
1088 print "$filepart\n";
1091 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
1092 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1093 or $meta->{filehash} eq 'added' )
1095 # normal update, just send the new revision (either U=Update,
1096 # or A=Add, or R=Remove)
1097 if ( defined($wrev) && $wrev < 0 )
1099 $log->info("Tell the client the file is scheduled for removal");
1100 print "MT text R \n";
1101 print "MT fname $filename\n";
1102 print "MT newline\n";
1103 next;
1105 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
1107 $log->info("Tell the client the file is scheduled for addition");
1108 print "MT text A \n";
1109 print "MT fname $filename\n";
1110 print "MT newline\n";
1111 next;
1114 else {
1115 $log->info("Updating '$filename' to ".$meta->{revision});
1116 print "MT +updated\n";
1117 print "MT text U \n";
1118 print "MT fname $filename\n";
1119 print "MT newline\n";
1120 print "MT -updated\n";
1123 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1125 # Don't want to actually _DO_ the update if -n specified
1126 unless ( $state->{globaloptions}{-n} )
1128 if ( defined ( $wrev ) )
1130 # instruct client we're sending a file to put in this path as a replacement
1131 print "Update-existing $dirpart\n";
1132 $log->debug("Updating existing file 'Update-existing $dirpart'");
1133 } else {
1134 # instruct client we're sending a file to put in this path as a new file
1135 print "Clear-static-directory $dirpart\n";
1136 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1137 print "Clear-sticky $dirpart\n";
1138 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1140 $log->debug("Creating new file 'Created $dirpart'");
1141 print "Created $dirpart\n";
1143 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1145 # this is an "entries" line
1146 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1147 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1148 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1150 # permissions
1151 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1152 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1154 # transmit file
1155 transmitfile($meta->{filehash});
1157 } else {
1158 $log->info("Updating '$filename'");
1159 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
1161 my $mergeDir = setupTmpDir();
1163 my $file_local = $filepart . ".mine";
1164 my $mergedFile = "$mergeDir/$file_local";
1165 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1166 my $file_old = $filepart . "." . $oldmeta->{revision};
1167 transmitfile($oldmeta->{filehash}, { targetfile => $file_old });
1168 my $file_new = $filepart . "." . $meta->{revision};
1169 transmitfile($meta->{filehash}, { targetfile => $file_new });
1171 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1172 $log->info("Merging $file_local, $file_old, $file_new");
1173 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
1175 $log->debug("Temporary directory for merge is $mergeDir");
1177 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1178 $return >>= 8;
1180 cleanupTmpDir();
1182 if ( $return == 0 )
1184 $log->info("Merged successfully");
1185 print "M M $filename\n";
1186 $log->debug("Merged $dirpart");
1188 # Don't want to actually _DO_ the update if -n specified
1189 unless ( $state->{globaloptions}{-n} )
1191 print "Merged $dirpart\n";
1192 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1193 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1194 my $kopts = kopts_from_path("$dirpart/$filepart",
1195 "file",$mergedFile);
1196 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1197 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1200 elsif ( $return == 1 )
1202 $log->info("Merged with conflicts");
1203 print "E cvs update: conflicts found in $filename\n";
1204 print "M C $filename\n";
1206 # Don't want to actually _DO_ the update if -n specified
1207 unless ( $state->{globaloptions}{-n} )
1209 print "Merged $dirpart\n";
1210 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1211 my $kopts = kopts_from_path("$dirpart/$filepart",
1212 "file",$mergedFile);
1213 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
1216 else
1218 $log->warn("Merge failed");
1219 next;
1222 # Don't want to actually _DO_ the update if -n specified
1223 unless ( $state->{globaloptions}{-n} )
1225 # permissions
1226 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1227 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1229 # transmit file, format is single integer on a line by itself (file
1230 # size) followed by the file contents
1231 # TODO : we should copy files in blocks
1232 my $data = `cat $mergedFile`;
1233 $log->debug("File size : " . length($data));
1234 print length($data) . "\n";
1235 print $data;
1241 print "ok\n";
1244 sub req_ci
1246 my ( $cmd, $data ) = @_;
1248 argsplit("ci");
1250 #$log->debug("State : " . Dumper($state));
1252 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1254 if ( $state->{method} eq 'pserver')
1256 print "error 1 pserver access cannot commit\n";
1257 cleanupWorkTree();
1258 exit;
1261 if ( -e $state->{CVSROOT} . "/index" )
1263 $log->warn("file 'index' already exists in the git repository");
1264 print "error 1 Index already exists in git repo\n";
1265 cleanupWorkTree();
1266 exit;
1269 # Grab a handle to the SQLite db and do any necessary updates
1270 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1271 $updater->update();
1273 # Remember where the head was at the beginning.
1274 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1275 chomp $parenthash;
1276 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1277 print "error 1 pserver cannot find the current HEAD of module";
1278 cleanupWorkTree();
1279 exit;
1282 setupWorkTree($parenthash);
1284 $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1286 $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
1288 my @committedfiles = ();
1289 my %oldmeta;
1291 # foreach file specified on the command line ...
1292 foreach my $filename ( @{$state->{args}} )
1294 my $committedfile = $filename;
1295 $filename = filecleanup($filename);
1297 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1299 my $meta = $updater->getmeta($filename);
1300 $oldmeta{$filename} = $meta;
1302 my $wrev = revparse($filename);
1304 my ( $filepart, $dirpart ) = filenamesplit($filename);
1306 # do a checkout of the file if it is part of this tree
1307 if ($wrev) {
1308 system('git', 'checkout-index', '-f', '-u', $filename);
1309 unless ($? == 0) {
1310 die "Error running git-checkout-index -f -u $filename : $!";
1314 my $addflag = 0;
1315 my $rmflag = 0;
1316 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1317 $addflag = 1 unless ( -e $filename );
1319 # Do up to date checking
1320 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1322 # fail everything if an up to date check fails
1323 print "error 1 Up to date check failed for $filename\n";
1324 cleanupWorkTree();
1325 exit;
1328 push @committedfiles, $committedfile;
1329 $log->info("Committing $filename");
1331 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1333 unless ( $rmflag )
1335 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1336 rename $state->{entries}{$filename}{modified_filename},$filename;
1338 # Calculate modes to remove
1339 my $invmode = "";
1340 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1342 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1343 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1346 if ( $rmflag )
1348 $log->info("Removing file '$filename'");
1349 unlink($filename);
1350 system("git", "update-index", "--remove", $filename);
1352 elsif ( $addflag )
1354 $log->info("Adding file '$filename'");
1355 system("git", "update-index", "--add", $filename);
1356 } else {
1357 $log->info("Updating file '$filename'");
1358 system("git", "update-index", $filename);
1362 unless ( scalar(@committedfiles) > 0 )
1364 print "E No files to commit\n";
1365 print "ok\n";
1366 cleanupWorkTree();
1367 return;
1370 my $treehash = `git write-tree`;
1371 chomp $treehash;
1373 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1375 # write our commit message out if we have one ...
1376 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1377 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1378 if ( defined ( $cfg->{gitcvs}{commitmsgannotation} ) ) {
1379 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/ ) {
1380 print $msg_fh "\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"
1382 } else {
1383 print $msg_fh "\n\nvia git-CVS emulator\n";
1385 close $msg_fh;
1387 my $commithash = `git commit-tree $treehash -p $parenthash < $msg_filename`;
1388 chomp($commithash);
1389 $log->info("Commit hash : $commithash");
1391 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1393 $log->warn("Commit failed (Invalid commit hash)");
1394 print "error 1 Commit failed (unknown reason)\n";
1395 cleanupWorkTree();
1396 exit;
1399 ### Emulate git-receive-pack by running hooks/update
1400 my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1401 $parenthash, $commithash );
1402 if( -x $hook[0] ) {
1403 unless( system( @hook ) == 0 )
1405 $log->warn("Commit failed (update hook declined to update ref)");
1406 print "error 1 Commit failed (update hook declined)\n";
1407 cleanupWorkTree();
1408 exit;
1412 ### Update the ref
1413 if (system(qw(git update-ref -m), "cvsserver ci",
1414 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1415 $log->warn("update-ref for $state->{module} failed.");
1416 print "error 1 Cannot commit -- update first\n";
1417 cleanupWorkTree();
1418 exit;
1421 ### Emulate git-receive-pack by running hooks/post-receive
1422 my $hook = $ENV{GIT_DIR}.'hooks/post-receive';
1423 if( -x $hook ) {
1424 open(my $pipe, "| $hook") || die "can't fork $!";
1426 local $SIG{PIPE} = sub { die 'pipe broke' };
1428 print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
1430 close $pipe || die "bad pipe: $! $?";
1433 $updater->update();
1435 ### Then hooks/post-update
1436 $hook = $ENV{GIT_DIR}.'hooks/post-update';
1437 if (-x $hook) {
1438 system($hook, "refs/heads/$state->{module}");
1441 # foreach file specified on the command line ...
1442 foreach my $filename ( @committedfiles )
1444 $filename = filecleanup($filename);
1446 my $meta = $updater->getmeta($filename);
1447 unless (defined $meta->{revision}) {
1448 $meta->{revision} = 1;
1451 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
1453 $log->debug("Checked-in $dirpart : $filename");
1455 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
1456 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
1458 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
1459 print "Remove-entry $dirpart\n";
1460 print "$filename\n";
1461 } else {
1462 if ($meta->{revision} == 1) {
1463 print "M initial revision: 1.1\n";
1464 } else {
1465 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
1467 print "Checked-in $dirpart\n";
1468 print "$filename\n";
1469 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1470 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1474 cleanupWorkTree();
1475 print "ok\n";
1478 sub req_status
1480 my ( $cmd, $data ) = @_;
1482 argsplit("status");
1484 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1485 #$log->debug("status state : " . Dumper($state));
1487 # Grab a handle to the SQLite db and do any necessary updates
1488 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1489 $updater->update();
1491 # if no files were specified, we need to work out what files we should be providing status on ...
1492 argsfromdir($updater);
1494 # foreach file specified on the command line ...
1495 foreach my $filename ( @{$state->{args}} )
1497 $filename = filecleanup($filename);
1499 next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0;
1501 my $meta = $updater->getmeta($filename);
1502 my $oldmeta = $meta;
1504 my $wrev = revparse($filename);
1506 # If the working copy is an old revision, lets get that version too for comparison.
1507 if ( defined($wrev) and $wrev != $meta->{revision} )
1509 $oldmeta = $updater->getmeta($filename, $wrev);
1512 # TODO : All possible statuses aren't yet implemented
1513 my $status;
1514 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1515 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1517 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1518 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1521 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1522 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1524 ( $state->{entries}{$filename}{unchanged}
1525 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1528 # Need checkout if it exists in the repo but doesn't have a working copy
1529 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1531 # Locally modified if working copy and repo copy have the same revision but there are local changes
1532 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1534 # Needs Merge if working copy revision is less than repo copy and there are local changes
1535 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1537 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1538 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1539 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1540 $status ||= "File had conflicts on merge" if ( 0 );
1542 $status ||= "Unknown";
1544 my ($filepart) = filenamesplit($filename);
1546 print "M ===================================================================\n";
1547 print "M File: $filepart\tStatus: $status\n";
1548 if ( defined($state->{entries}{$filename}{revision}) )
1550 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1551 } else {
1552 print "M Working revision:\tNo entry for $filename\n";
1554 if ( defined($meta->{revision}) )
1556 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1557 print "M Sticky Tag:\t\t(none)\n";
1558 print "M Sticky Date:\t\t(none)\n";
1559 print "M Sticky Options:\t\t(none)\n";
1560 } else {
1561 print "M Repository revision:\tNo revision control file\n";
1563 print "M\n";
1566 print "ok\n";
1569 sub req_diff
1571 my ( $cmd, $data ) = @_;
1573 argsplit("diff");
1575 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1576 #$log->debug("status state : " . Dumper($state));
1578 my ($revision1, $revision2);
1579 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1581 $revision1 = $state->{opt}{r}[0];
1582 $revision2 = $state->{opt}{r}[1];
1583 } else {
1584 $revision1 = $state->{opt}{r};
1587 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1588 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1590 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1592 # Grab a handle to the SQLite db and do any necessary updates
1593 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1594 $updater->update();
1596 # if no files were specified, we need to work out what files we should be providing status on ...
1597 argsfromdir($updater);
1599 # foreach file specified on the command line ...
1600 foreach my $filename ( @{$state->{args}} )
1602 $filename = filecleanup($filename);
1604 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1606 my $wrev = revparse($filename);
1608 # We need _something_ to diff against
1609 next unless ( defined ( $wrev ) );
1611 # if we have a -r switch, use it
1612 if ( defined ( $revision1 ) )
1614 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1615 $meta1 = $updater->getmeta($filename, $revision1);
1616 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1618 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1619 next;
1621 transmitfile($meta1->{filehash}, { targetfile => $file1 });
1623 # otherwise we just use the working copy revision
1624 else
1626 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1627 $meta1 = $updater->getmeta($filename, $wrev);
1628 transmitfile($meta1->{filehash}, { targetfile => $file1 });
1631 # if we have a second -r switch, use it too
1632 if ( defined ( $revision2 ) )
1634 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1635 $meta2 = $updater->getmeta($filename, $revision2);
1637 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1639 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1640 next;
1643 transmitfile($meta2->{filehash}, { targetfile => $file2 });
1645 # otherwise we just use the working copy
1646 else
1648 $file2 = $state->{entries}{$filename}{modified_filename};
1651 # if we have been given -r, and we don't have a $file2 yet, lets get one
1652 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1654 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1655 $meta2 = $updater->getmeta($filename, $wrev);
1656 transmitfile($meta2->{filehash}, { targetfile => $file2 });
1659 # We need to have retrieved something useful
1660 next unless ( defined ( $meta1 ) );
1662 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1663 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1665 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1666 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1669 # Apparently we only show diffs for locally modified files
1670 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1672 print "M Index: $filename\n";
1673 print "M ===================================================================\n";
1674 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1675 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1676 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1677 print "M diff ";
1678 foreach my $opt ( keys %{$state->{opt}} )
1680 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1682 foreach my $value ( @{$state->{opt}{$opt}} )
1684 print "-$opt $value ";
1686 } else {
1687 print "-$opt ";
1688 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1691 print "$filename\n";
1693 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1695 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1697 if ( exists $state->{opt}{u} )
1699 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1700 } else {
1701 system("diff $file1 $file2 > $filediff");
1704 while ( <$fh> )
1706 print "M $_";
1708 close $fh;
1711 print "ok\n";
1714 sub req_log
1716 my ( $cmd, $data ) = @_;
1718 argsplit("log");
1720 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1721 #$log->debug("log state : " . Dumper($state));
1723 my ( $minrev, $maxrev );
1724 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1726 my $control = $2;
1727 $minrev = $1;
1728 $maxrev = $3;
1729 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1730 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1731 $minrev++ if ( defined($minrev) and $control eq "::" );
1734 # Grab a handle to the SQLite db and do any necessary updates
1735 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1736 $updater->update();
1738 # if no files were specified, we need to work out what files we should be providing status on ...
1739 argsfromdir($updater);
1741 # foreach file specified on the command line ...
1742 foreach my $filename ( @{$state->{args}} )
1744 $filename = filecleanup($filename);
1746 my $headmeta = $updater->getmeta($filename);
1748 my $revisions = $updater->getlog($filename);
1749 my $totalrevisions = scalar(@$revisions);
1751 if ( defined ( $minrev ) )
1753 $log->debug("Removing revisions less than $minrev");
1754 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1756 pop @$revisions;
1759 if ( defined ( $maxrev ) )
1761 $log->debug("Removing revisions greater than $maxrev");
1762 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1764 shift @$revisions;
1768 next unless ( scalar(@$revisions) );
1770 print "M \n";
1771 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1772 print "M Working file: $filename\n";
1773 print "M head: 1.$headmeta->{revision}\n";
1774 print "M branch:\n";
1775 print "M locks: strict\n";
1776 print "M access list:\n";
1777 print "M symbolic names:\n";
1778 print "M keyword substitution: kv\n";
1779 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1780 print "M description:\n";
1782 foreach my $revision ( @$revisions )
1784 print "M ----------------------------\n";
1785 print "M revision 1.$revision->{revision}\n";
1786 # reformat the date for log output
1787 $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
1788 $revision->{author} = cvs_author($revision->{author});
1789 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1790 my $commitmessage = $updater->commitmessage($revision->{commithash});
1791 $commitmessage =~ s/^/M /mg;
1792 print $commitmessage . "\n";
1794 print "M =============================================================================\n";
1797 print "ok\n";
1800 sub req_annotate
1802 my ( $cmd, $data ) = @_;
1804 argsplit("annotate");
1806 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1807 #$log->debug("status state : " . Dumper($state));
1809 # Grab a handle to the SQLite db and do any necessary updates
1810 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1811 $updater->update();
1813 # if no files were specified, we need to work out what files we should be providing annotate on ...
1814 argsfromdir($updater);
1816 # we'll need a temporary checkout dir
1817 setupWorkTree();
1819 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");
1821 # foreach file specified on the command line ...
1822 foreach my $filename ( @{$state->{args}} )
1824 $filename = filecleanup($filename);
1826 my $meta = $updater->getmeta($filename);
1828 next unless ( $meta->{revision} );
1830 # get all the commits that this file was in
1831 # in dense format -- aka skip dead revisions
1832 my $revisions = $updater->gethistorydense($filename);
1833 my $lastseenin = $revisions->[0][2];
1835 # populate the temporary index based on the latest commit were we saw
1836 # the file -- but do it cheaply without checking out any files
1837 # TODO: if we got a revision from the client, use that instead
1838 # to look up the commithash in sqlite (still good to default to
1839 # the current head as we do now)
1840 system("git", "read-tree", $lastseenin);
1841 unless ($? == 0)
1843 print "E error running git-read-tree $lastseenin $ENV{GIT_INDEX_FILE} $!\n";
1844 return;
1846 $log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit $lastseenin - exit status $?");
1848 # do a checkout of the file
1849 system('git', 'checkout-index', '-f', '-u', $filename);
1850 unless ($? == 0) {
1851 print "E error running git-checkout-index -f -u $filename : $!\n";
1852 return;
1855 $log->info("Annotate $filename");
1857 # Prepare a file with the commits from the linearized
1858 # history that annotate should know about. This prevents
1859 # git-jsannotate telling us about commits we are hiding
1860 # from the client.
1862 my $a_hints = "$work->{workDir}/.annotate_hints";
1863 if (!open(ANNOTATEHINTS, '>', $a_hints)) {
1864 print "E failed to open '$a_hints' for writing: $!\n";
1865 return;
1867 for (my $i=0; $i < @$revisions; $i++)
1869 print ANNOTATEHINTS $revisions->[$i][2];
1870 if ($i+1 < @$revisions) { # have we got a parent?
1871 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1873 print ANNOTATEHINTS "\n";
1876 print ANNOTATEHINTS "\n";
1877 close ANNOTATEHINTS
1878 or (print "E failed to write $a_hints: $!\n"), return;
1880 my @cmd = (qw(git annotate -l -S), $a_hints, $filename);
1881 if (!open(ANNOTATE, "-|", @cmd)) {
1882 print "E error invoking ". join(' ',@cmd) .": $!\n";
1883 return;
1885 my $metadata = {};
1886 print "E Annotations for $filename\n";
1887 print "E ***************\n";
1888 while ( <ANNOTATE> )
1890 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1892 my $commithash = $1;
1893 my $data = $2;
1894 unless ( defined ( $metadata->{$commithash} ) )
1896 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1897 $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
1898 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1900 printf("M 1.%-5d (%-8s %10s): %s\n",
1901 $metadata->{$commithash}{revision},
1902 $metadata->{$commithash}{author},
1903 $metadata->{$commithash}{modified},
1904 $data
1906 } else {
1907 $log->warn("Error in annotate output! LINE: $_");
1908 print "E Annotate error \n";
1909 next;
1912 close ANNOTATE;
1915 # done; get out of the tempdir
1916 cleanupWorkTree();
1918 print "ok\n";
1922 # This method takes the state->{arguments} array and produces two new arrays.
1923 # The first is $state->{args} which is everything before the '--' argument, and
1924 # the second is $state->{files} which is everything after it.
1925 sub argsplit
1927 $state->{args} = [];
1928 $state->{files} = [];
1929 $state->{opt} = {};
1931 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1933 my $type = shift;
1935 if ( defined($type) )
1937 my $opt = {};
1938 $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" );
1939 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1940 $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" );
1941 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1942 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1943 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1944 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1945 $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" );
1948 while ( scalar ( @{$state->{arguments}} ) > 0 )
1950 my $arg = shift @{$state->{arguments}};
1952 next if ( $arg eq "--" );
1953 next unless ( $arg =~ /\S/ );
1955 # if the argument looks like a switch
1956 if ( $arg =~ /^-(\w)(.*)/ )
1958 # if it's a switch that takes an argument
1959 if ( $opt->{$1} )
1961 # If this switch has already been provided
1962 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1964 $state->{opt}{$1} = [ $state->{opt}{$1} ];
1965 if ( length($2) > 0 )
1967 push @{$state->{opt}{$1}},$2;
1968 } else {
1969 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1971 } else {
1972 # if there's extra data in the arg, use that as the argument for the switch
1973 if ( length($2) > 0 )
1975 $state->{opt}{$1} = $2;
1976 } else {
1977 $state->{opt}{$1} = shift @{$state->{arguments}};
1980 } else {
1981 $state->{opt}{$1} = undef;
1984 else
1986 push @{$state->{args}}, $arg;
1990 else
1992 my $mode = 0;
1994 foreach my $value ( @{$state->{arguments}} )
1996 if ( $value eq "--" )
1998 $mode++;
1999 next;
2001 push @{$state->{args}}, $value if ( $mode == 0 );
2002 push @{$state->{files}}, $value if ( $mode == 1 );
2007 # This method uses $state->{directory} to populate $state->{args} with a list of filenames
2008 sub argsfromdir
2010 my $updater = shift;
2012 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
2014 return if ( scalar ( @{$state->{args}} ) > 1 );
2016 my @gethead = @{$updater->gethead};
2018 # push added files
2019 foreach my $file (keys %{$state->{entries}}) {
2020 if ( exists $state->{entries}{$file}{revision} &&
2021 $state->{entries}{$file}{revision} == 0 )
2023 push @gethead, { name => $file, filehash => 'added' };
2027 if ( scalar(@{$state->{args}}) == 1 )
2029 my $arg = $state->{args}[0];
2030 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
2032 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
2034 foreach my $file ( @gethead )
2036 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2037 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
2038 push @{$state->{args}}, $file->{name};
2041 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
2042 } else {
2043 $log->info("Only one arg specified, populating file list automatically");
2045 $state->{args} = [];
2047 foreach my $file ( @gethead )
2049 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2050 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
2051 push @{$state->{args}}, $file->{name};
2056 # This method cleans up the $state variable after a command that uses arguments has run
2057 sub statecleanup
2059 $state->{files} = [];
2060 $state->{args} = [];
2061 $state->{arguments} = [];
2062 $state->{entries} = {};
2065 sub revparse
2067 my $filename = shift;
2069 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
2071 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
2072 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
2074 return undef;
2077 # This method takes a file hash and does a CVS "file transfer". Its
2078 # exact behaviour depends on a second, optional hash table argument:
2079 # - If $options->{targetfile}, dump the contents to that file;
2080 # - If $options->{print}, use M/MT to transmit the contents one line
2081 # at a time;
2082 # - Otherwise, transmit the size of the file, followed by the file
2083 # contents.
2084 sub transmitfile
2086 my $filehash = shift;
2087 my $options = shift;
2089 if ( defined ( $filehash ) and $filehash eq "deleted" )
2091 $log->warn("filehash is 'deleted'");
2092 return;
2095 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2097 my $type = `git cat-file -t $filehash`;
2098 chomp $type;
2100 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2102 my $size = `git cat-file -s $filehash`;
2103 chomp $size;
2105 $log->debug("transmitfile($filehash) size=$size, type=$type");
2107 if ( open my $fh, '-|', "git", "cat-file", "blob", $filehash )
2109 if ( defined ( $options->{targetfile} ) )
2111 my $targetfile = $options->{targetfile};
2112 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2113 print NEWFILE $_ while ( <$fh> );
2114 close NEWFILE or die("Failed to write '$targetfile': $!");
2115 } elsif ( defined ( $options->{print} ) && $options->{print} ) {
2116 while ( <$fh> ) {
2117 if( /\n\z/ ) {
2118 print 'M ', $_;
2119 } else {
2120 print 'MT text ', $_, "\n";
2123 } else {
2124 print "$size\n";
2125 print while ( <$fh> );
2127 close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
2128 } else {
2129 die("Couldn't execute git-cat-file");
2133 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2134 # refers to the directory portion and the file portion of the filename
2135 # respectively
2136 sub filenamesplit
2138 my $filename = shift;
2139 my $fixforlocaldir = shift;
2141 my ( $filepart, $dirpart ) = ( $filename, "." );
2142 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2143 $dirpart .= "/";
2145 if ( $fixforlocaldir )
2147 $dirpart =~ s/^$state->{prependdir}//;
2150 return ( $filepart, $dirpart );
2153 sub filecleanup
2155 my $filename = shift;
2157 return undef unless(defined($filename));
2158 if ( $filename =~ /^\// )
2160 print "E absolute filenames '$filename' not supported by server\n";
2161 return undef;
2164 $filename =~ s/^\.\///g;
2165 $filename = $state->{prependdir} . $filename;
2166 return $filename;
2169 sub validateGitDir
2171 if( !defined($state->{CVSROOT}) )
2173 print "error 1 CVSROOT not specified\n";
2174 cleanupWorkTree();
2175 exit;
2177 if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') )
2179 print "error 1 Internally inconsistent CVSROOT\n";
2180 cleanupWorkTree();
2181 exit;
2185 # Setup working directory in a work tree with the requested version
2186 # loaded in the index.
2187 sub setupWorkTree
2189 my ($ver) = @_;
2191 validateGitDir();
2193 if( ( defined($work->{state}) && $work->{state} != 1 ) ||
2194 defined($work->{tmpDir}) )
2196 $log->warn("Bad work tree state management");
2197 print "error 1 Internal setup multiple work trees without cleanup\n";
2198 cleanupWorkTree();
2199 exit;
2202 $work->{workDir} = tempdir ( DIR => $TEMP_DIR );
2204 if( !defined($work->{index}) )
2206 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2209 chdir $work->{workDir} or
2210 die "Unable to chdir to $work->{workDir}\n";
2212 $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");
2214 $ENV{GIT_WORK_TREE} = ".";
2215 $ENV{GIT_INDEX_FILE} = $work->{index};
2216 $work->{state} = 2;
2218 if($ver)
2220 system("git","read-tree",$ver);
2221 unless ($? == 0)
2223 $log->warn("Error running git-read-tree");
2224 die "Error running git-read-tree $ver in $work->{workDir} $!\n";
2227 # else # req_annotate reads tree for each file
2230 # Ensure current directory is in some kind of working directory,
2231 # with a recent version loaded in the index.
2232 sub ensureWorkTree
2234 if( defined($work->{tmpDir}) )
2236 $log->warn("Bad work tree state management [ensureWorkTree()]");
2237 print "error 1 Internal setup multiple dirs without cleanup\n";
2238 cleanupWorkTree();
2239 exit;
2241 if( $work->{state} )
2243 return;
2246 validateGitDir();
2248 if( !defined($work->{emptyDir}) )
2250 $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0);
2252 chdir $work->{emptyDir} or
2253 die "Unable to chdir to $work->{emptyDir}\n";
2255 my $ver = `git show-ref -s refs/heads/$state->{module}`;
2256 chomp $ver;
2257 if ($ver !~ /^[0-9a-f]{40}$/)
2259 $log->warn("Error from git show-ref -s refs/head$state->{module}");
2260 print "error 1 cannot find the current HEAD of module";
2261 cleanupWorkTree();
2262 exit;
2265 if( !defined($work->{index}) )
2267 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2270 $ENV{GIT_WORK_TREE} = ".";
2271 $ENV{GIT_INDEX_FILE} = $work->{index};
2272 $work->{state} = 1;
2274 system("git","read-tree",$ver);
2275 unless ($? == 0)
2277 die "Error running git-read-tree $ver $!\n";
2281 # Cleanup working directory that is not needed any longer.
2282 sub cleanupWorkTree
2284 if( ! $work->{state} )
2286 return;
2289 chdir "/" or die "Unable to chdir '/'\n";
2291 if( defined($work->{workDir}) )
2293 rmtree( $work->{workDir} );
2294 undef $work->{workDir};
2296 undef $work->{state};
2299 # Setup a temporary directory (not a working tree), typically for
2300 # merging dirty state as in req_update.
2301 sub setupTmpDir
2303 $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR );
2304 chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n";
2306 return $work->{tmpDir};
2309 # Clean up a previously setupTmpDir. Restore previous work tree if
2310 # appropriate.
2311 sub cleanupTmpDir
2313 if ( !defined($work->{tmpDir}) )
2315 $log->warn("cleanup tmpdir that has not been setup");
2316 die "Cleanup tmpDir that has not been setup\n";
2318 if( defined($work->{state}) )
2320 if( $work->{state} == 1 )
2322 chdir $work->{emptyDir} or
2323 die "Unable to chdir to $work->{emptyDir}\n";
2325 elsif( $work->{state} == 2 )
2327 chdir $work->{workDir} or
2328 die "Unable to chdir to $work->{emptyDir}\n";
2330 else
2332 $log->warn("Inconsistent work dir state");
2333 die "Inconsistent work dir state\n";
2336 else
2338 chdir "/" or die "Unable to chdir '/'\n";
2342 # Given a path, this function returns a string containing the kopts
2343 # that should go into that path's Entries line. For example, a binary
2344 # file should get -kb.
2345 sub kopts_from_path
2347 my ($path, $srcType, $name) = @_;
2349 if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and
2350 $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i )
2352 my ($val) = check_attr( "crlf", $path );
2353 if ( $val eq "set" )
2355 return "";
2357 elsif ( $val eq "unset" )
2359 return "-kb"
2361 else
2363 $log->info("Unrecognized check_attr crlf $path : $val");
2367 if ( defined ( $cfg->{gitcvs}{allbinary} ) )
2369 if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) )
2371 return "-kb";
2373 elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
2375 if( $srcType eq "sha1Or-k" &&
2376 !defined($name) )
2378 my ($ret)=$state->{entries}{$path}{options};
2379 if( !defined($ret) )
2381 $ret=$state->{opt}{k};
2382 if(defined($ret))
2384 $ret="-k$ret";
2386 else
2388 $ret="";
2391 if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) )
2393 print "E Bad -k option\n";
2394 $log->warn("Bad -k option: $ret");
2395 die "Error: Bad -k option: $ret\n";
2398 return $ret;
2400 else
2402 if( is_binary($srcType,$name) )
2404 $log->debug("... as binary");
2405 return "-kb";
2407 else
2409 $log->debug("... as text");
2414 # Return "" to give no special treatment to any path
2415 return "";
2418 sub check_attr
2420 my ($attr,$path) = @_;
2421 ensureWorkTree();
2422 if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path )
2424 my $val = <$fh>;
2425 close $fh;
2426 $val =~ s/.*: ([^:\r\n]*)\s*$/$1/;
2427 return $val;
2429 else
2431 return undef;
2435 # This should have the same heuristics as convert.c:is_binary() and related.
2436 # Note that the bare CR test is done by callers in convert.c.
2437 sub is_binary
2439 my ($srcType,$name) = @_;
2440 $log->debug("is_binary($srcType,$name)");
2442 # Minimize amount of interpreted code run in the inner per-character
2443 # loop for large files, by totalling each character value and
2444 # then analyzing the totals.
2445 my @counts;
2446 my $i;
2447 for($i=0;$i<256;$i++)
2449 $counts[$i]=0;
2452 my $fh = open_blob_or_die($srcType,$name);
2453 my $line;
2454 while( defined($line=<$fh>) )
2456 # Any '\0' and bare CR are considered binary.
2457 if( $line =~ /\0|(\r[^\n])/ )
2459 close($fh);
2460 return 1;
2463 # Count up each character in the line:
2464 my $len=length($line);
2465 for($i=0;$i<$len;$i++)
2467 $counts[ord(substr($line,$i,1))]++;
2470 close $fh;
2472 # Don't count CR and LF as either printable/nonprintable
2473 $counts[ord("\n")]=0;
2474 $counts[ord("\r")]=0;
2476 # Categorize individual character count into printable and nonprintable:
2477 my $printable=0;
2478 my $nonprintable=0;
2479 for($i=0;$i<256;$i++)
2481 if( $i < 32 &&
2482 $i != ord("\b") &&
2483 $i != ord("\t") &&
2484 $i != 033 && # ESC
2485 $i != 014 ) # FF
2487 $nonprintable+=$counts[$i];
2489 elsif( $i==127 ) # DEL
2491 $nonprintable+=$counts[$i];
2493 else
2495 $printable+=$counts[$i];
2499 return ($printable >> 7) < $nonprintable;
2502 # Returns open file handle. Possible invocations:
2503 # - open_blob_or_die("file",$filename);
2504 # - open_blob_or_die("sha1",$filehash);
2505 sub open_blob_or_die
2507 my ($srcType,$name) = @_;
2508 my ($fh);
2509 if( $srcType eq "file" )
2511 if( !open $fh,"<",$name )
2513 $log->warn("Unable to open file $name: $!");
2514 die "Unable to open file $name: $!\n";
2517 elsif( $srcType eq "sha1" || $srcType eq "sha1Or-k" )
2519 unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
2521 $log->warn("Need filehash");
2522 die "Need filehash\n";
2525 my $type = `git cat-file -t $name`;
2526 chomp $type;
2528 unless ( defined ( $type ) and $type eq "blob" )
2530 $log->warn("Invalid type '$type' for '$name'");
2531 die ( "Invalid type '$type' (expected 'blob')" )
2534 my $size = `git cat-file -s $name`;
2535 chomp $size;
2537 $log->debug("open_blob_or_die($name) size=$size, type=$type");
2539 unless( open $fh, '-|', "git", "cat-file", "blob", $name )
2541 $log->warn("Unable to open sha1 $name");
2542 die "Unable to open sha1 $name\n";
2545 else
2547 $log->warn("Unknown type of blob source: $srcType");
2548 die "Unknown type of blob source: $srcType\n";
2550 return $fh;
2553 # Generate a CVS author name from Git author information, by taking the local
2554 # part of the email address and replacing characters not in the Portable
2555 # Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS
2556 # Login names are Unix login names, which should be restricted to this
2557 # character set.
2558 sub cvs_author
2560 my $author_line = shift;
2561 (my $author) = $author_line =~ /<([^@>]*)/;
2563 $author =~ s/[^-a-zA-Z0-9_.]/_/g;
2564 $author =~ s/^-/_/;
2566 $author;
2569 package GITCVS::log;
2571 ####
2572 #### Copyright The Open University UK - 2006.
2573 ####
2574 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2575 #### Martin Langhoff <martin@catalyst.net.nz>
2576 ####
2577 ####
2579 use strict;
2580 use warnings;
2582 =head1 NAME
2584 GITCVS::log
2586 =head1 DESCRIPTION
2588 This module provides very crude logging with a similar interface to
2589 Log::Log4perl
2591 =head1 METHODS
2593 =cut
2595 =head2 new
2597 Creates a new log object, optionally you can specify a filename here to
2598 indicate the file to log to. If no log file is specified, you can specify one
2599 later with method setfile, or indicate you no longer want logging with method
2600 nofile.
2602 Until one of these methods is called, all log calls will buffer messages ready
2603 to write out.
2605 =cut
2606 sub new
2608 my $class = shift;
2609 my $filename = shift;
2611 my $self = {};
2613 bless $self, $class;
2615 if ( defined ( $filename ) )
2617 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2620 return $self;
2623 =head2 setfile
2625 This methods takes a filename, and attempts to open that file as the log file.
2626 If successful, all buffered data is written out to the file, and any further
2627 logging is written directly to the file.
2629 =cut
2630 sub setfile
2632 my $self = shift;
2633 my $filename = shift;
2635 if ( defined ( $filename ) )
2637 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2640 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2642 while ( my $line = shift @{$self->{buffer}} )
2644 print {$self->{fh}} $line;
2648 =head2 nofile
2650 This method indicates no logging is going to be used. It flushes any entries in
2651 the internal buffer, and sets a flag to ensure no further data is put there.
2653 =cut
2654 sub nofile
2656 my $self = shift;
2658 $self->{nolog} = 1;
2660 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2662 $self->{buffer} = [];
2665 =head2 _logopen
2667 Internal method. Returns true if the log file is open, false otherwise.
2669 =cut
2670 sub _logopen
2672 my $self = shift;
2674 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2675 return 0;
2678 =head2 debug info warn fatal
2680 These four methods are wrappers to _log. They provide the actual interface for
2681 logging data.
2683 =cut
2684 sub debug { my $self = shift; $self->_log("debug", @_); }
2685 sub info { my $self = shift; $self->_log("info" , @_); }
2686 sub warn { my $self = shift; $self->_log("warn" , @_); }
2687 sub fatal { my $self = shift; $self->_log("fatal", @_); }
2689 =head2 _log
2691 This is an internal method called by the logging functions. It generates a
2692 timestamp and pushes the logged line either to file, or internal buffer.
2694 =cut
2695 sub _log
2697 my $self = shift;
2698 my $level = shift;
2700 return if ( $self->{nolog} );
2702 my @time = localtime;
2703 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2704 $time[5] + 1900,
2705 $time[4] + 1,
2706 $time[3],
2707 $time[2],
2708 $time[1],
2709 $time[0],
2710 uc $level,
2713 if ( $self->_logopen )
2715 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2716 } else {
2717 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2721 =head2 DESTROY
2723 This method simply closes the file handle if one is open
2725 =cut
2726 sub DESTROY
2728 my $self = shift;
2730 if ( $self->_logopen )
2732 close $self->{fh};
2736 package GITCVS::updater;
2738 ####
2739 #### Copyright The Open University UK - 2006.
2740 ####
2741 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2742 #### Martin Langhoff <martin@catalyst.net.nz>
2743 ####
2744 ####
2746 use strict;
2747 use warnings;
2748 use DBI;
2750 =head1 METHODS
2752 =cut
2754 =head2 new
2756 =cut
2757 sub new
2759 my $class = shift;
2760 my $config = shift;
2761 my $module = shift;
2762 my $log = shift;
2764 die "Need to specify a git repository" unless ( defined($config) and -d $config );
2765 die "Need to specify a module" unless ( defined($module) );
2767 $class = ref($class) || $class;
2769 my $self = {};
2771 bless $self, $class;
2773 $self->{valid_tables} = {'revision' => 1,
2774 'revision_ix1' => 1,
2775 'revision_ix2' => 1,
2776 'head' => 1,
2777 'head_ix1' => 1,
2778 'properties' => 1,
2779 'commitmsgs' => 1};
2781 $self->{module} = $module;
2782 $self->{git_path} = $config . "/";
2784 $self->{log} = $log;
2786 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2788 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
2789 $cfg->{gitcvs}{dbdriver} || "SQLite";
2790 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2791 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2792 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2793 $cfg->{gitcvs}{dbuser} || "";
2794 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2795 $cfg->{gitcvs}{dbpass} || "";
2796 $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||
2797 $cfg->{gitcvs}{dbtablenameprefix} || "";
2798 my %mapping = ( m => $module,
2799 a => $state->{method},
2800 u => getlogin || getpwuid($<) || $<,
2801 G => $self->{git_path},
2802 g => mangle_dirname($self->{git_path}),
2804 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2805 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
2806 $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg;
2807 $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});
2809 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2810 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2811 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
2812 $self->{dbuser},
2813 $self->{dbpass});
2814 die "Error connecting to database\n" unless defined $self->{dbh};
2816 $self->{tables} = {};
2817 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
2819 $self->{tables}{$table} = 1;
2822 # Construct the revision table if required
2823 unless ( $self->{tables}{$self->tablename("revision")} )
2825 my $tablename = $self->tablename("revision");
2826 my $ix1name = $self->tablename("revision_ix1");
2827 my $ix2name = $self->tablename("revision_ix2");
2828 $self->{dbh}->do("
2829 CREATE TABLE $tablename (
2830 name TEXT NOT NULL,
2831 revision INTEGER NOT NULL,
2832 filehash TEXT NOT NULL,
2833 commithash TEXT NOT NULL,
2834 author TEXT NOT NULL,
2835 modified TEXT NOT NULL,
2836 mode TEXT NOT NULL
2839 $self->{dbh}->do("
2840 CREATE INDEX $ix1name
2841 ON $tablename (name,revision)
2843 $self->{dbh}->do("
2844 CREATE INDEX $ix2name
2845 ON $tablename (name,commithash)
2849 # Construct the head table if required
2850 unless ( $self->{tables}{$self->tablename("head")} )
2852 my $tablename = $self->tablename("head");
2853 my $ix1name = $self->tablename("head_ix1");
2854 $self->{dbh}->do("
2855 CREATE TABLE $tablename (
2856 name TEXT NOT NULL,
2857 revision INTEGER NOT NULL,
2858 filehash TEXT NOT NULL,
2859 commithash TEXT NOT NULL,
2860 author TEXT NOT NULL,
2861 modified TEXT NOT NULL,
2862 mode TEXT NOT NULL
2865 $self->{dbh}->do("
2866 CREATE INDEX $ix1name
2867 ON $tablename (name)
2871 # Construct the properties table if required
2872 unless ( $self->{tables}{$self->tablename("properties")} )
2874 my $tablename = $self->tablename("properties");
2875 $self->{dbh}->do("
2876 CREATE TABLE $tablename (
2877 key TEXT NOT NULL PRIMARY KEY,
2878 value TEXT
2883 # Construct the commitmsgs table if required
2884 unless ( $self->{tables}{$self->tablename("commitmsgs")} )
2886 my $tablename = $self->tablename("commitmsgs");
2887 $self->{dbh}->do("
2888 CREATE TABLE $tablename (
2889 key TEXT NOT NULL PRIMARY KEY,
2890 value TEXT
2895 return $self;
2898 =head2 tablename
2900 =cut
2901 sub tablename
2903 my $self = shift;
2904 my $name = shift;
2906 if (exists $self->{valid_tables}{$name}) {
2907 return $self->{dbtablenameprefix} . $name;
2908 } else {
2909 return undef;
2913 =head2 update
2915 =cut
2916 sub update
2918 my $self = shift;
2920 # first lets get the commit list
2921 $ENV{GIT_DIR} = $self->{git_path};
2923 my $commitsha1 = `git rev-parse $self->{module}`;
2924 chomp $commitsha1;
2926 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
2927 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2929 die("Invalid module '$self->{module}'");
2933 my $git_log;
2934 my $lastcommit = $self->_get_prop("last_commit");
2936 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
2937 return 1;
2940 # Start exclusive lock here...
2941 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2943 # TODO: log processing is memory bound
2944 # if we can parse into a 2nd file that is in reverse order
2945 # we can probably do something really efficient
2946 my @git_log_params = ('--pretty', '--parents', '--topo-order');
2948 if (defined $lastcommit) {
2949 push @git_log_params, "$lastcommit..$self->{module}";
2950 } else {
2951 push @git_log_params, $self->{module};
2953 # git-rev-list is the backend / plumbing version of git-log
2954 open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
2956 my @commits;
2958 my %commit = ();
2960 while ( <GITLOG> )
2962 chomp;
2963 if (m/^commit\s+(.*)$/) {
2964 # on ^commit lines put the just seen commit in the stack
2965 # and prime things for the next one
2966 if (keys %commit) {
2967 my %copy = %commit;
2968 unshift @commits, \%copy;
2969 %commit = ();
2971 my @parents = split(m/\s+/, $1);
2972 $commit{hash} = shift @parents;
2973 $commit{parents} = \@parents;
2974 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2975 # on rfc822-like lines seen before we see any message,
2976 # lowercase the entry and put it in the hash as key-value
2977 $commit{lc($1)} = $2;
2978 } else {
2979 # message lines - skip initial empty line
2980 # and trim whitespace
2981 if (!exists($commit{message}) && m/^\s*$/) {
2982 # define it to mark the end of headers
2983 $commit{message} = '';
2984 next;
2986 s/^\s+//; s/\s+$//; # trim ws
2987 $commit{message} .= $_ . "\n";
2990 close GITLOG;
2992 unshift @commits, \%commit if ( keys %commit );
2994 # Now all the commits are in the @commits bucket
2995 # ordered by time DESC. for each commit that needs processing,
2996 # determine whether it's following the last head we've seen or if
2997 # it's on its own branch, grab a file list, and add whatever's changed
2998 # NOTE: $lastcommit refers to the last commit from previous run
2999 # $lastpicked is the last commit we picked in this run
3000 my $lastpicked;
3001 my $head = {};
3002 if (defined $lastcommit) {
3003 $lastpicked = $lastcommit;
3006 my $committotal = scalar(@commits);
3007 my $commitcount = 0;
3009 # Load the head table into $head (for cached lookups during the update process)
3010 foreach my $file ( @{$self->gethead()} )
3012 $head->{$file->{name}} = $file;
3015 foreach my $commit ( @commits )
3017 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
3018 if (defined $lastpicked)
3020 if (!in_array($lastpicked, @{$commit->{parents}}))
3022 # skip, we'll see this delta
3023 # as part of a merge later
3024 # warn "skipping off-track $commit->{hash}\n";
3025 next;
3026 } elsif (@{$commit->{parents}} > 1) {
3027 # it is a merge commit, for each parent that is
3028 # not $lastpicked, see if we can get a log
3029 # from the merge-base to that parent to put it
3030 # in the message as a merge summary.
3031 my @parents = @{$commit->{parents}};
3032 foreach my $parent (@parents) {
3033 # git-merge-base can potentially (but rarely) throw
3034 # several candidate merge bases. let's assume
3035 # that the first one is the best one.
3036 if ($parent eq $lastpicked) {
3037 next;
3039 my $base = eval {
3040 safe_pipe_capture('git', 'merge-base',
3041 $lastpicked, $parent);
3043 # The two branches may not be related at all,
3044 # in which case merge base simply fails to find
3045 # any, but that's Ok.
3046 next if ($@);
3048 chomp $base;
3049 if ($base) {
3050 my @merged;
3051 # print "want to log between $base $parent \n";
3052 open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
3053 or die "Cannot call git-log: $!";
3054 my $mergedhash;
3055 while (<GITLOG>) {
3056 chomp;
3057 if (!defined $mergedhash) {
3058 if (m/^commit\s+(.+)$/) {
3059 $mergedhash = $1;
3060 } else {
3061 next;
3063 } else {
3064 # grab the first line that looks non-rfc822
3065 # aka has content after leading space
3066 if (m/^\s+(\S.*)$/) {
3067 my $title = $1;
3068 $title = substr($title,0,100); # truncate
3069 unshift @merged, "$mergedhash $title";
3070 undef $mergedhash;
3074 close GITLOG;
3075 if (@merged) {
3076 $commit->{mergemsg} = $commit->{message};
3077 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
3078 foreach my $summary (@merged) {
3079 $commit->{mergemsg} .= "\t$summary\n";
3081 $commit->{mergemsg} .= "\n\n";
3082 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
3089 # convert the date to CVS-happy format
3090 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
3092 if ( defined ( $lastpicked ) )
3094 my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
3095 local ($/) = "\0";
3096 while ( <FILELIST> )
3098 chomp;
3099 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
3101 die("Couldn't process git-diff-tree line : $_");
3103 my ($mode, $hash, $change) = ($1, $2, $3);
3104 my $name = <FILELIST>;
3105 chomp($name);
3107 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3109 my $git_perms = "";
3110 $git_perms .= "r" if ( $mode & 4 );
3111 $git_perms .= "w" if ( $mode & 2 );
3112 $git_perms .= "x" if ( $mode & 1 );
3113 $git_perms = "rw" if ( $git_perms eq "" );
3115 if ( $change eq "D" )
3117 #$log->debug("DELETE $name");
3118 $head->{$name} = {
3119 name => $name,
3120 revision => $head->{$name}{revision} + 1,
3121 filehash => "deleted",
3122 commithash => $commit->{hash},
3123 modified => $commit->{date},
3124 author => $commit->{author},
3125 mode => $git_perms,
3127 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3129 elsif ( $change eq "M" || $change eq "T" )
3131 #$log->debug("MODIFIED $name");
3132 $head->{$name} = {
3133 name => $name,
3134 revision => $head->{$name}{revision} + 1,
3135 filehash => $hash,
3136 commithash => $commit->{hash},
3137 modified => $commit->{date},
3138 author => $commit->{author},
3139 mode => $git_perms,
3141 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3143 elsif ( $change eq "A" )
3145 #$log->debug("ADDED $name");
3146 $head->{$name} = {
3147 name => $name,
3148 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
3149 filehash => $hash,
3150 commithash => $commit->{hash},
3151 modified => $commit->{date},
3152 author => $commit->{author},
3153 mode => $git_perms,
3155 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3157 else
3159 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3160 die;
3163 close FILELIST;
3164 } else {
3165 # this is used to detect files removed from the repo
3166 my $seen_files = {};
3168 my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
3169 local $/ = "\0";
3170 while ( <FILELIST> )
3172 chomp;
3173 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3175 die("Couldn't process git-ls-tree line : $_");
3178 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
3180 $seen_files->{$git_filename} = 1;
3182 my ( $oldhash, $oldrevision, $oldmode ) = (
3183 $head->{$git_filename}{filehash},
3184 $head->{$git_filename}{revision},
3185 $head->{$git_filename}{mode}
3188 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
3190 $git_perms = "";
3191 $git_perms .= "r" if ( $1 & 4 );
3192 $git_perms .= "w" if ( $1 & 2 );
3193 $git_perms .= "x" if ( $1 & 1 );
3194 } else {
3195 $git_perms = "rw";
3198 # unless the file exists with the same hash, we need to update it ...
3199 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
3201 my $newrevision = ( $oldrevision or 0 ) + 1;
3203 $head->{$git_filename} = {
3204 name => $git_filename,
3205 revision => $newrevision,
3206 filehash => $git_hash,
3207 commithash => $commit->{hash},
3208 modified => $commit->{date},
3209 author => $commit->{author},
3210 mode => $git_perms,
3214 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3217 close FILELIST;
3219 # Detect deleted files
3220 foreach my $file ( keys %$head )
3222 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
3224 $head->{$file}{revision}++;
3225 $head->{$file}{filehash} = "deleted";
3226 $head->{$file}{commithash} = $commit->{hash};
3227 $head->{$file}{modified} = $commit->{date};
3228 $head->{$file}{author} = $commit->{author};
3230 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
3233 # END : "Detect deleted files"
3237 if (exists $commit->{mergemsg})
3239 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3242 $lastpicked = $commit->{hash};
3244 $self->_set_prop("last_commit", $commit->{hash});
3247 $self->delete_head();
3248 foreach my $file ( keys %$head )
3250 $self->insert_head(
3251 $file,
3252 $head->{$file}{revision},
3253 $head->{$file}{filehash},
3254 $head->{$file}{commithash},
3255 $head->{$file}{modified},
3256 $head->{$file}{author},
3257 $head->{$file}{mode},
3260 # invalidate the gethead cache
3261 $self->{gethead_cache} = undef;
3264 # Ending exclusive lock here
3265 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
3268 sub insert_rev
3270 my $self = shift;
3271 my $name = shift;
3272 my $revision = shift;
3273 my $filehash = shift;
3274 my $commithash = shift;
3275 my $modified = shift;
3276 my $author = shift;
3277 my $mode = shift;
3278 my $tablename = $self->tablename("revision");
3280 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3281 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3284 sub insert_mergelog
3286 my $self = shift;
3287 my $key = shift;
3288 my $value = shift;
3289 my $tablename = $self->tablename("commitmsgs");
3291 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3292 $insert_mergelog->execute($key, $value);
3295 sub delete_head
3297 my $self = shift;
3298 my $tablename = $self->tablename("head");
3300 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1);
3301 $delete_head->execute();
3304 sub insert_head
3306 my $self = shift;
3307 my $name = shift;
3308 my $revision = shift;
3309 my $filehash = shift;
3310 my $commithash = shift;
3311 my $modified = shift;
3312 my $author = shift;
3313 my $mode = shift;
3314 my $tablename = $self->tablename("head");
3316 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
3317 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3320 sub _headrev
3322 my $self = shift;
3323 my $filename = shift;
3324 my $tablename = $self->tablename("head");
3326 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1);
3327 $db_query->execute($filename);
3328 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
3330 return ( $hash, $revision, $mode );
3333 sub _get_prop
3335 my $self = shift;
3336 my $key = shift;
3337 my $tablename = $self->tablename("properties");
3339 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3340 $db_query->execute($key);
3341 my ( $value ) = $db_query->fetchrow_array;
3343 return $value;
3346 sub _set_prop
3348 my $self = shift;
3349 my $key = shift;
3350 my $value = shift;
3351 my $tablename = $self->tablename("properties");
3353 my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1);
3354 $db_query->execute($value, $key);
3356 unless ( $db_query->rows )
3358 $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3359 $db_query->execute($key, $value);
3362 return $value;
3365 =head2 gethead
3367 =cut
3369 sub gethead
3371 my $self = shift;
3372 my $tablename = $self->tablename("head");
3374 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
3376 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1);
3377 $db_query->execute();
3379 my $tree = [];
3380 while ( my $file = $db_query->fetchrow_hashref )
3382 push @$tree, $file;
3385 $self->{gethead_cache} = $tree;
3387 return $tree;
3390 =head2 getlog
3392 =cut
3394 sub getlog
3396 my $self = shift;
3397 my $filename = shift;
3398 my $tablename = $self->tablename("revision");
3400 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3401 $db_query->execute($filename);
3403 my $tree = [];
3404 while ( my $file = $db_query->fetchrow_hashref )
3406 push @$tree, $file;
3409 return $tree;
3412 =head2 getmeta
3414 This function takes a filename (with path) argument and returns a hashref of
3415 metadata for that file.
3417 =cut
3419 sub getmeta
3421 my $self = shift;
3422 my $filename = shift;
3423 my $revision = shift;
3424 my $tablename_rev = $self->tablename("revision");
3425 my $tablename_head = $self->tablename("head");
3427 my $db_query;
3428 if ( defined($revision) and $revision =~ /^\d+$/ )
3430 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
3431 $db_query->execute($filename, $revision);
3433 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
3435 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
3436 $db_query->execute($filename, $revision);
3437 } else {
3438 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
3439 $db_query->execute($filename);
3442 return $db_query->fetchrow_hashref;
3445 =head2 commitmessage
3447 this function takes a commithash and returns the commit message for that commit
3449 =cut
3450 sub commitmessage
3452 my $self = shift;
3453 my $commithash = shift;
3454 my $tablename = $self->tablename("commitmsgs");
3456 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
3458 my $db_query;
3459 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3460 $db_query->execute($commithash);
3462 my ( $message ) = $db_query->fetchrow_array;
3464 if ( defined ( $message ) )
3466 $message .= " " if ( $message =~ /\n$/ );
3467 return $message;
3470 my @lines = safe_pipe_capture("git", "cat-file", "commit", $commithash);
3471 shift @lines while ( $lines[0] =~ /\S/ );
3472 $message = join("",@lines);
3473 $message .= " " if ( $message =~ /\n$/ );
3474 return $message;
3477 =head2 gethistory
3479 This function takes a filename (with path) argument and returns an arrayofarrays
3480 containing revision,filehash,commithash ordered by revision descending
3482 =cut
3483 sub gethistory
3485 my $self = shift;
3486 my $filename = shift;
3487 my $tablename = $self->tablename("revision");
3489 my $db_query;
3490 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3491 $db_query->execute($filename);
3493 return $db_query->fetchall_arrayref;
3496 =head2 gethistorydense
3498 This function takes a filename (with path) argument and returns an arrayofarrays
3499 containing revision,filehash,commithash ordered by revision descending.
3501 This version of gethistory skips deleted entries -- so it is useful for annotate.
3502 The 'dense' part is a reference to a '--dense' option available for git-rev-list
3503 and other git tools that depend on it.
3505 =cut
3506 sub gethistorydense
3508 my $self = shift;
3509 my $filename = shift;
3510 my $tablename = $self->tablename("revision");
3512 my $db_query;
3513 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
3514 $db_query->execute($filename);
3516 return $db_query->fetchall_arrayref;
3519 =head2 in_array()
3521 from Array::PAT - mimics the in_array() function
3522 found in PHP. Yuck but works for small arrays.
3524 =cut
3525 sub in_array
3527 my ($check, @array) = @_;
3528 my $retval = 0;
3529 foreach my $test (@array){
3530 if($check eq $test){
3531 $retval = 1;
3534 return $retval;
3537 =head2 safe_pipe_capture
3539 an alternative to `command` that allows input to be passed as an array
3540 to work around shell problems with weird characters in arguments
3542 =cut
3543 sub safe_pipe_capture {
3545 my @output;
3547 if (my $pid = open my $child, '-|') {
3548 @output = (<$child>);
3549 close $child or die join(' ',@_).": $! $?";
3550 } else {
3551 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3553 return wantarray ? @output : join('',@output);
3556 =head2 mangle_dirname
3558 create a string from a directory name that is suitable to use as
3559 part of a filename, mainly by converting all chars except \w.- to _
3561 =cut
3562 sub mangle_dirname {
3563 my $dirname = shift;
3564 return unless defined $dirname;
3566 $dirname =~ s/[^\w.-]/_/g;
3568 return $dirname;
3571 =head2 mangle_tablename
3573 create a string from a that is suitable to use as part of an SQL table
3574 name, mainly by converting all chars except \w to _
3576 =cut
3577 sub mangle_tablename {
3578 my $tablename = shift;
3579 return unless defined $tablename;
3581 $tablename =~ s/[^\w_]/_/g;
3583 return $tablename;