git-send-email: Add --thread option
[git/aroben.git] / git-cvsserver.perl
blob5cbf27eebc0f090c0d7e45e82064344b30d326fc
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::Basename;
25 use Getopt::Long qw(:config require_order no_ignore_case);
27 my $VERSION = '@@GIT_VERSION@@';
29 my $log = GITCVS::log->new();
30 my $cfg;
32 my $DATE_LIST = {
33 Jan => "01",
34 Feb => "02",
35 Mar => "03",
36 Apr => "04",
37 May => "05",
38 Jun => "06",
39 Jul => "07",
40 Aug => "08",
41 Sep => "09",
42 Oct => "10",
43 Nov => "11",
44 Dec => "12",
47 # Enable autoflush for STDOUT (otherwise the whole thing falls apart)
48 $| = 1;
50 #### Definition and mappings of functions ####
52 my $methods = {
53 'Root' => \&req_Root,
54 'Valid-responses' => \&req_Validresponses,
55 'valid-requests' => \&req_validrequests,
56 'Directory' => \&req_Directory,
57 'Entry' => \&req_Entry,
58 'Modified' => \&req_Modified,
59 'Unchanged' => \&req_Unchanged,
60 'Questionable' => \&req_Questionable,
61 'Argument' => \&req_Argument,
62 'Argumentx' => \&req_Argument,
63 'expand-modules' => \&req_expandmodules,
64 'add' => \&req_add,
65 'remove' => \&req_remove,
66 'co' => \&req_co,
67 'update' => \&req_update,
68 'ci' => \&req_ci,
69 'diff' => \&req_diff,
70 'log' => \&req_log,
71 'rlog' => \&req_log,
72 'tag' => \&req_CATCHALL,
73 'status' => \&req_status,
74 'admin' => \&req_CATCHALL,
75 'history' => \&req_CATCHALL,
76 'watchers' => \&req_CATCHALL,
77 'editors' => \&req_CATCHALL,
78 'annotate' => \&req_annotate,
79 'Global_option' => \&req_Globaloption,
80 #'annotate' => \&req_CATCHALL,
83 ##############################################
86 # $state holds all the bits of information the clients sends us that could
87 # potentially be useful when it comes to actually _doing_ something.
88 my $state = { prependdir => '' };
89 $log->info("--------------- STARTING -----------------");
91 my $usage =
92 "Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n".
93 " --base-path <path> : Prepend to requested CVSROOT\n".
94 " --strict-paths : Don't allow recursing into subdirectories\n".
95 " --export-all : Don't check for gitcvs.enabled in config\n".
96 " --version, -V : Print version information and exit\n".
97 " --help, -h, -H : Print usage information and exit\n".
98 "\n".
99 "<directory> ... is a list of allowed directories. If no directories\n".
100 "are given, all are allowed. This is an additional restriction, gitcvs\n".
101 "access still needs to be enabled by the gitcvs.enabled config option.\n";
103 my @opts = ( 'help|h|H', 'version|V',
104 'base-path=s', 'strict-paths', 'export-all' );
105 GetOptions( $state, @opts )
106 or die $usage;
108 if ($state->{version}) {
109 print "git-cvsserver version $VERSION\n";
110 exit;
112 if ($state->{help}) {
113 print $usage;
114 exit;
117 my $TEMP_DIR = tempdir( CLEANUP => 1 );
118 $log->debug("Temporary directory is '$TEMP_DIR'");
120 $state->{method} = 'ext';
121 if (@ARGV) {
122 if ($ARGV[0] eq 'pserver') {
123 $state->{method} = 'pserver';
124 shift @ARGV;
125 } elsif ($ARGV[0] eq 'server') {
126 shift @ARGV;
130 # everything else is a directory
131 $state->{allowed_roots} = [ @ARGV ];
133 # don't export the whole system unless the users requests it
134 if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
135 die "--export-all can only be used together with an explicit whitelist\n";
138 # if we are called with a pserver argument,
139 # deal with the authentication cat before entering the
140 # main loop
141 if ($state->{method} eq 'pserver') {
142 my $line = <STDIN>; chomp $line;
143 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
144 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
146 my $request = $1;
147 $line = <STDIN>; chomp $line;
148 req_Root('root', $line) # reuse Root
149 or die "E Invalid root $line \n";
150 $line = <STDIN>; chomp $line;
151 unless ($line eq 'anonymous') {
152 print "E Only anonymous user allowed via pserver\n";
153 print "I HATE YOU\n";
154 exit 1;
156 $line = <STDIN>; chomp $line; # validate the password?
157 $line = <STDIN>; chomp $line;
158 unless ($line eq "END $request REQUEST") {
159 die "E Do not understand $line -- expecting END $request REQUEST\n";
161 print "I LOVE YOU\n";
162 exit if $request eq 'VERIFICATION'; # cvs login
163 # and now back to our regular programme...
166 # Keep going until the client closes the connection
167 while (<STDIN>)
169 chomp;
171 # Check to see if we've seen this method, and call appropriate function.
172 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
174 # use the $methods hash to call the appropriate sub for this command
175 #$log->info("Method : $1");
176 &{$methods->{$1}}($1,$2);
177 } else {
178 # log fatal because we don't understand this function. If this happens
179 # we're fairly screwed because we don't know if the client is expecting
180 # a response. If it is, the client will hang, we'll hang, and the whole
181 # thing will be custard.
182 $log->fatal("Don't understand command $_\n");
183 die("Unknown command $_");
187 $log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
188 $log->info("--------------- FINISH -----------------");
190 # Magic catchall method.
191 # This is the method that will handle all commands we haven't yet
192 # implemented. It simply sends a warning to the log file indicating a
193 # command that hasn't been implemented has been invoked.
194 sub req_CATCHALL
196 my ( $cmd, $data ) = @_;
197 $log->warn("Unhandled command : req_$cmd : $data");
201 # Root pathname \n
202 # Response expected: no. Tell the server which CVSROOT to use. Note that
203 # pathname is a local directory and not a fully qualified CVSROOT variable.
204 # pathname must already exist; if creating a new root, use the init
205 # request, not Root. pathname does not include the hostname of the server,
206 # how to access the server, etc.; by the time the CVS protocol is in use,
207 # connection, authentication, etc., are already taken care of. The Root
208 # request must be sent only once, and it must be sent before any requests
209 # other than Valid-responses, valid-requests, UseUnchanged, Set or init.
210 sub req_Root
212 my ( $cmd, $data ) = @_;
213 $log->debug("req_Root : $data");
215 unless ($data =~ m#^/#) {
216 print "error 1 Root must be an absolute pathname\n";
217 return 0;
220 my $cvsroot = $state->{'base-path'} || '';
221 $cvsroot =~ s#/+$##;
222 $cvsroot .= $data;
224 if ($state->{CVSROOT}
225 && ($state->{CVSROOT} ne $cvsroot)) {
226 print "error 1 Conflicting roots specified\n";
227 return 0;
230 $state->{CVSROOT} = $cvsroot;
232 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
234 if (@{$state->{allowed_roots}}) {
235 my $allowed = 0;
236 foreach my $dir (@{$state->{allowed_roots}}) {
237 next unless $dir =~ m#^/#;
238 $dir =~ s#/+$##;
239 if ($state->{'strict-paths'}) {
240 if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
241 $allowed = 1;
242 last;
244 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
245 $allowed = 1;
246 last;
250 unless ($allowed) {
251 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
252 print "E \n";
253 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
254 return 0;
258 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
259 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
260 print "E \n";
261 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
262 return 0;
265 my @gitvars = `git-config -l`;
266 if ($?) {
267 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
268 print "E \n";
269 print "error 1 - problem executing git-config\n";
270 return 0;
272 foreach my $line ( @gitvars )
274 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
275 unless ($2) {
276 $cfg->{$1}{$3} = $4;
277 } else {
278 $cfg->{$1}{$2}{$3} = $4;
282 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
283 || $cfg->{gitcvs}{enabled});
284 unless ($state->{'export-all'} ||
285 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
286 print "E GITCVS emulation needs to be enabled on this repo\n";
287 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
288 print "E \n";
289 print "error 1 GITCVS emulation disabled\n";
290 return 0;
293 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
294 if ( $logfile )
296 $log->setfile($logfile);
297 } else {
298 $log->nofile();
301 return 1;
304 # Global_option option \n
305 # Response expected: no. Transmit one of the global options `-q', `-Q',
306 # `-l', `-t', `-r', or `-n'. option must be one of those strings, no
307 # variations (such as combining of options) are allowed. For graceful
308 # handling of valid-requests, it is probably better to make new global
309 # options separate requests, rather than trying to add them to this
310 # request.
311 sub req_Globaloption
313 my ( $cmd, $data ) = @_;
314 $log->debug("req_Globaloption : $data");
315 $state->{globaloptions}{$data} = 1;
318 # Valid-responses request-list \n
319 # Response expected: no. Tell the server what responses the client will
320 # accept. request-list is a space separated list of tokens.
321 sub req_Validresponses
323 my ( $cmd, $data ) = @_;
324 $log->debug("req_Validresponses : $data");
326 # TODO : re-enable this, currently it's not particularly useful
327 #$state->{validresponses} = [ split /\s+/, $data ];
330 # valid-requests \n
331 # Response expected: yes. Ask the server to send back a Valid-requests
332 # response.
333 sub req_validrequests
335 my ( $cmd, $data ) = @_;
337 $log->debug("req_validrequests");
339 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
340 $log->debug("SEND : ok");
342 print "Valid-requests " . join(" ",keys %$methods) . "\n";
343 print "ok\n";
346 # Directory local-directory \n
347 # Additional data: repository \n. Response expected: no. Tell the server
348 # what directory to use. The repository should be a directory name from a
349 # previous server response. Note that this both gives a default for Entry
350 # and Modified and also for ci and the other commands; normal usage is to
351 # send Directory for each directory in which there will be an Entry or
352 # Modified, and then a final Directory for the original directory, then the
353 # command. The local-directory is relative to the top level at which the
354 # command is occurring (i.e. the last Directory which is sent before the
355 # command); to indicate that top level, `.' should be sent for
356 # local-directory.
357 sub req_Directory
359 my ( $cmd, $data ) = @_;
361 my $repository = <STDIN>;
362 chomp $repository;
365 $state->{localdir} = $data;
366 $state->{repository} = $repository;
367 $state->{path} = $repository;
368 $state->{path} =~ s/^$state->{CVSROOT}\///;
369 $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
370 $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
372 $state->{directory} = $state->{localdir};
373 $state->{directory} = "" if ( $state->{directory} eq "." );
374 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
376 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
378 $log->info("Setting prepend to '$state->{path}'");
379 $state->{prependdir} = $state->{path};
380 foreach my $entry ( keys %{$state->{entries}} )
382 $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
383 delete $state->{entries}{$entry};
387 if ( defined ( $state->{prependdir} ) )
389 $log->debug("Prepending '$state->{prependdir}' to state|directory");
390 $state->{directory} = $state->{prependdir} . $state->{directory}
392 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
395 # Entry entry-line \n
396 # Response expected: no. Tell the server what version of a file is on the
397 # local machine. The name in entry-line is a name relative to the directory
398 # most recently specified with Directory. If the user is operating on only
399 # some files in a directory, Entry requests for only those files need be
400 # included. If an Entry request is sent without Modified, Is-modified, or
401 # Unchanged, it means the file is lost (does not exist in the working
402 # directory). If both Entry and one of Modified, Is-modified, or Unchanged
403 # are sent for the same file, Entry must be sent first. For a given file,
404 # one can send Modified, Is-modified, or Unchanged, but not more than one
405 # of these three.
406 sub req_Entry
408 my ( $cmd, $data ) = @_;
410 #$log->debug("req_Entry : $data");
412 my @data = split(/\//, $data);
414 $state->{entries}{$state->{directory}.$data[1]} = {
415 revision => $data[2],
416 conflict => $data[3],
417 options => $data[4],
418 tag_or_date => $data[5],
421 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
424 # Questionable filename \n
425 # Response expected: no. Additional data: no. Tell the server to check
426 # whether filename should be ignored, and if not, next time the server
427 # sends responses, send (in a M response) `?' followed by the directory and
428 # filename. filename must not contain `/'; it needs to be a file in the
429 # directory named by the most recent Directory request.
430 sub req_Questionable
432 my ( $cmd, $data ) = @_;
434 $log->debug("req_Questionable : $data");
435 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
438 # add \n
439 # Response expected: yes. Add a file or directory. This uses any previous
440 # Argument, Directory, Entry, or Modified requests, if they have been sent.
441 # The last Directory sent specifies the working directory at the time of
442 # the operation. To add a directory, send the directory to be added using
443 # Directory and Argument requests.
444 sub req_add
446 my ( $cmd, $data ) = @_;
448 argsplit("add");
450 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
451 $updater->update();
453 argsfromdir($updater);
455 my $addcount = 0;
457 foreach my $filename ( @{$state->{args}} )
459 $filename = filecleanup($filename);
461 my $meta = $updater->getmeta($filename);
462 my $wrev = revparse($filename);
464 if ($wrev && $meta && ($wrev < 0))
466 # previously removed file, add back
467 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
469 print "MT +updated\n";
470 print "MT text U \n";
471 print "MT fname $filename\n";
472 print "MT newline\n";
473 print "MT -updated\n";
475 unless ( $state->{globaloptions}{-n} )
477 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
479 print "Created $dirpart\n";
480 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
482 # this is an "entries" line
483 my $kopts = kopts_from_path($filepart);
484 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
485 print "/$filepart/1.$meta->{revision}//$kopts/\n";
486 # permissions
487 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
488 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
489 # transmit file
490 transmitfile($meta->{filehash});
493 next;
496 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
498 print "E cvs add: nothing known about `$filename'\n";
499 next;
501 # TODO : check we're not squashing an already existing file
502 if ( defined ( $state->{entries}{$filename}{revision} ) )
504 print "E cvs add: `$filename' has already been entered\n";
505 next;
508 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
510 print "E cvs add: scheduling file `$filename' for addition\n";
512 print "Checked-in $dirpart\n";
513 print "$filename\n";
514 my $kopts = kopts_from_path($filepart);
515 print "/$filepart/0//$kopts/\n";
517 $addcount++;
520 if ( $addcount == 1 )
522 print "E cvs add: use `cvs commit' to add this file permanently\n";
524 elsif ( $addcount > 1 )
526 print "E cvs add: use `cvs commit' to add these files permanently\n";
529 print "ok\n";
532 # remove \n
533 # Response expected: yes. Remove a file. This uses any previous Argument,
534 # Directory, Entry, or Modified requests, if they have been sent. The last
535 # Directory sent specifies the working directory at the time of the
536 # operation. Note that this request does not actually do anything to the
537 # repository; the only effect of a successful remove request is to supply
538 # the client with a new entries line containing `-' to indicate a removed
539 # file. In fact, the client probably could perform this operation without
540 # contacting the server, although using remove may cause the server to
541 # perform a few more checks. The client sends a subsequent ci request to
542 # actually record the removal in the repository.
543 sub req_remove
545 my ( $cmd, $data ) = @_;
547 argsplit("remove");
549 # Grab a handle to the SQLite db and do any necessary updates
550 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
551 $updater->update();
553 #$log->debug("add state : " . Dumper($state));
555 my $rmcount = 0;
557 foreach my $filename ( @{$state->{args}} )
559 $filename = filecleanup($filename);
561 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
563 print "E cvs remove: file `$filename' still in working directory\n";
564 next;
567 my $meta = $updater->getmeta($filename);
568 my $wrev = revparse($filename);
570 unless ( defined ( $wrev ) )
572 print "E cvs remove: nothing known about `$filename'\n";
573 next;
576 if ( defined($wrev) and $wrev < 0 )
578 print "E cvs remove: file `$filename' already scheduled for removal\n";
579 next;
582 unless ( $wrev == $meta->{revision} )
584 # TODO : not sure if the format of this message is quite correct.
585 print "E cvs remove: Up to date check failed for `$filename'\n";
586 next;
590 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
592 print "E cvs remove: scheduling `$filename' for removal\n";
594 print "Checked-in $dirpart\n";
595 print "$filename\n";
596 my $kopts = kopts_from_path($filepart);
597 print "/$filepart/-1.$wrev//$kopts/\n";
599 $rmcount++;
602 if ( $rmcount == 1 )
604 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
606 elsif ( $rmcount > 1 )
608 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
611 print "ok\n";
614 # Modified filename \n
615 # Response expected: no. Additional data: mode, \n, file transmission. Send
616 # the server a copy of one locally modified file. filename is a file within
617 # the most recent directory sent with Directory; it must not contain `/'.
618 # If the user is operating on only some files in a directory, only those
619 # files need to be included. This can also be sent without Entry, if there
620 # is no entry for the file.
621 sub req_Modified
623 my ( $cmd, $data ) = @_;
625 my $mode = <STDIN>;
626 chomp $mode;
627 my $size = <STDIN>;
628 chomp $size;
630 # Grab config information
631 my $blocksize = 8192;
632 my $bytesleft = $size;
633 my $tmp;
635 # Get a filehandle/name to write it to
636 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
638 # Loop over file data writing out to temporary file.
639 while ( $bytesleft )
641 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
642 read STDIN, $tmp, $blocksize;
643 print $fh $tmp;
644 $bytesleft -= $blocksize;
647 close $fh;
649 # Ensure we have something sensible for the file mode
650 if ( $mode =~ /u=(\w+)/ )
652 $mode = $1;
653 } else {
654 $mode = "rw";
657 # Save the file data in $state
658 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
659 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
660 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
661 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
663 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
666 # Unchanged filename \n
667 # Response expected: no. Tell the server that filename has not been
668 # modified in the checked out directory. The filename is a file within the
669 # most recent directory sent with Directory; it must not contain `/'.
670 sub req_Unchanged
672 my ( $cmd, $data ) = @_;
674 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
676 #$log->debug("req_Unchanged : $data");
679 # Argument text \n
680 # Response expected: no. Save argument for use in a subsequent command.
681 # Arguments accumulate until an argument-using command is given, at which
682 # point they are forgotten.
683 # Argumentx text \n
684 # Response expected: no. Append \n followed by text to the current argument
685 # being saved.
686 sub req_Argument
688 my ( $cmd, $data ) = @_;
690 # Argumentx means: append to last Argument (with a newline in front)
692 $log->debug("$cmd : $data");
694 if ( $cmd eq 'Argumentx') {
695 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
696 } else {
697 push @{$state->{arguments}}, $data;
701 # expand-modules \n
702 # Response expected: yes. Expand the modules which are specified in the
703 # arguments. Returns the data in Module-expansion responses. Note that the
704 # server can assume that this is checkout or export, not rtag or rdiff; the
705 # latter do not access the working directory and thus have no need to
706 # expand modules on the client side. Expand may not be the best word for
707 # what this request does. It does not necessarily tell you all the files
708 # contained in a module, for example. Basically it is a way of telling you
709 # which working directories the server needs to know about in order to
710 # handle a checkout of the specified modules. For example, suppose that the
711 # server has a module defined by
712 # aliasmodule -a 1dir
713 # That is, one can check out aliasmodule and it will take 1dir in the
714 # repository and check it out to 1dir in the working directory. Now suppose
715 # the client already has this module checked out and is planning on using
716 # the co request to update it. Without using expand-modules, the client
717 # would have two bad choices: it could either send information about all
718 # working directories under the current directory, which could be
719 # unnecessarily slow, or it could be ignorant of the fact that aliasmodule
720 # stands for 1dir, and neglect to send information for 1dir, which would
721 # lead to incorrect operation. With expand-modules, the client would first
722 # ask for the module to be expanded:
723 sub req_expandmodules
725 my ( $cmd, $data ) = @_;
727 argsplit();
729 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
731 unless ( ref $state->{arguments} eq "ARRAY" )
733 print "ok\n";
734 return;
737 foreach my $module ( @{$state->{arguments}} )
739 $log->debug("SEND : Module-expansion $module");
740 print "Module-expansion $module\n";
743 print "ok\n";
744 statecleanup();
747 # co \n
748 # Response expected: yes. Get files from the repository. This uses any
749 # previous Argument, Directory, Entry, or Modified requests, if they have
750 # been sent. Arguments to this command are module names; the client cannot
751 # know what directories they correspond to except by (1) just sending the
752 # co request, and then seeing what directory names the server sends back in
753 # its responses, and (2) the expand-modules request.
754 sub req_co
756 my ( $cmd, $data ) = @_;
758 argsplit("co");
760 my $module = $state->{args}[0];
761 my $checkout_path = $module;
763 # use the user specified directory if we're given it
764 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
766 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
768 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
770 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
772 # Grab a handle to the SQLite db and do any necessary updates
773 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
774 $updater->update();
776 $checkout_path =~ s|/$||; # get rid of trailing slashes
778 # Eclipse seems to need the Clear-sticky command
779 # to prepare the 'Entries' file for the new directory.
780 print "Clear-sticky $checkout_path/\n";
781 print $state->{CVSROOT} . "/$module/\n";
782 print "Clear-static-directory $checkout_path/\n";
783 print $state->{CVSROOT} . "/$module/\n";
784 print "Clear-sticky $checkout_path/\n"; # yes, twice
785 print $state->{CVSROOT} . "/$module/\n";
786 print "Template $checkout_path/\n";
787 print $state->{CVSROOT} . "/$module/\n";
788 print "0\n";
790 # instruct the client that we're checking out to $checkout_path
791 print "E cvs checkout: Updating $checkout_path\n";
793 my %seendirs = ();
794 my $lastdir ='';
796 # recursive
797 sub prepdir {
798 my ($dir, $repodir, $remotedir, $seendirs) = @_;
799 my $parent = dirname($dir);
800 $dir =~ s|/+$||;
801 $repodir =~ s|/+$||;
802 $remotedir =~ s|/+$||;
803 $parent =~ s|/+$||;
804 $log->debug("announcedir $dir, $repodir, $remotedir" );
806 if ($parent eq '.' || $parent eq './') {
807 $parent = '';
809 # recurse to announce unseen parents first
810 if (length($parent) && !exists($seendirs->{$parent})) {
811 prepdir($parent, $repodir, $remotedir, $seendirs);
813 # Announce that we are going to modify at the parent level
814 if ($parent) {
815 print "E cvs checkout: Updating $remotedir/$parent\n";
816 } else {
817 print "E cvs checkout: Updating $remotedir\n";
819 print "Clear-sticky $remotedir/$parent/\n";
820 print "$repodir/$parent/\n";
822 print "Clear-static-directory $remotedir/$dir/\n";
823 print "$repodir/$dir/\n";
824 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
825 print "$repodir/$parent/\n";
826 print "Template $remotedir/$dir/\n";
827 print "$repodir/$dir/\n";
828 print "0\n";
830 $seendirs->{$dir} = 1;
833 foreach my $git ( @{$updater->gethead} )
835 # Don't want to check out deleted files
836 next if ( $git->{filehash} eq "deleted" );
838 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
840 if (length($git->{dir}) && $git->{dir} ne './'
841 && $git->{dir} ne $lastdir ) {
842 unless (exists($seendirs{$git->{dir}})) {
843 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
844 $checkout_path, \%seendirs);
845 $lastdir = $git->{dir};
846 $seendirs{$git->{dir}} = 1;
848 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
851 # modification time of this file
852 print "Mod-time $git->{modified}\n";
854 # print some information to the client
855 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
857 print "M U $checkout_path/$git->{dir}$git->{name}\n";
858 } else {
859 print "M U $checkout_path/$git->{name}\n";
862 # instruct client we're sending a file to put in this path
863 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
865 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
867 # this is an "entries" line
868 my $kopts = kopts_from_path($git->{name});
869 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
870 # permissions
871 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
873 # transmit file
874 transmitfile($git->{filehash});
877 print "ok\n";
879 statecleanup();
882 # update \n
883 # Response expected: yes. Actually do a cvs update command. This uses any
884 # previous Argument, Directory, Entry, or Modified requests, if they have
885 # been sent. The last Directory sent specifies the working directory at the
886 # time of the operation. The -I option is not used--files which the client
887 # can decide whether to ignore are not mentioned and the client sends the
888 # Questionable request for others.
889 sub req_update
891 my ( $cmd, $data ) = @_;
893 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
895 argsplit("update");
898 # It may just be a client exploring the available heads/modules
899 # in that case, list them as top level directories and leave it
900 # at that. Eclipse uses this technique to offer you a list of
901 # projects (heads in this case) to checkout.
903 if ($state->{module} eq '') {
904 print "E cvs update: Updating .\n";
905 opendir HEADS, $state->{CVSROOT} . '/refs/heads';
906 while (my $head = readdir(HEADS)) {
907 if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
908 print "E cvs update: New directory `$head'\n";
911 closedir HEADS;
912 print "ok\n";
913 return 1;
917 # Grab a handle to the SQLite db and do any necessary updates
918 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
920 $updater->update();
922 argsfromdir($updater);
924 #$log->debug("update state : " . Dumper($state));
926 # foreach file specified on the command line ...
927 foreach my $filename ( @{$state->{args}} )
929 $filename = filecleanup($filename);
931 $log->debug("Processing file $filename");
933 # if we have a -C we should pretend we never saw modified stuff
934 if ( exists ( $state->{opt}{C} ) )
936 delete $state->{entries}{$filename}{modified_hash};
937 delete $state->{entries}{$filename}{modified_filename};
938 $state->{entries}{$filename}{unchanged} = 1;
941 my $meta;
942 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
944 $meta = $updater->getmeta($filename, $1);
945 } else {
946 $meta = $updater->getmeta($filename);
949 if ( ! defined $meta )
951 $meta = {
952 name => $filename,
953 revision => 0,
954 filehash => 'added'
958 my $oldmeta = $meta;
960 my $wrev = revparse($filename);
962 # If the working copy is an old revision, lets get that version too for comparison.
963 if ( defined($wrev) and $wrev != $meta->{revision} )
965 $oldmeta = $updater->getmeta($filename, $wrev);
968 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
970 # Files are up to date if the working copy and repo copy have the same revision,
971 # and the working copy is unmodified _and_ the user hasn't specified -C
972 next if ( defined ( $wrev )
973 and defined($meta->{revision})
974 and $wrev == $meta->{revision}
975 and $state->{entries}{$filename}{unchanged}
976 and not exists ( $state->{opt}{C} ) );
978 # If the working copy and repo copy have the same revision,
979 # but the working copy is modified, tell the client it's modified
980 if ( defined ( $wrev )
981 and defined($meta->{revision})
982 and $wrev == $meta->{revision}
983 and defined($state->{entries}{$filename}{modified_hash})
984 and not exists ( $state->{opt}{C} ) )
986 $log->info("Tell the client the file is modified");
987 print "MT text M \n";
988 print "MT fname $filename\n";
989 print "MT newline\n";
990 next;
993 if ( $meta->{filehash} eq "deleted" )
995 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
997 $log->info("Removing '$filename' from working copy (no longer in the repo)");
999 print "E cvs update: `$filename' is no longer in the repository\n";
1000 # Don't want to actually _DO_ the update if -n specified
1001 unless ( $state->{globaloptions}{-n} ) {
1002 print "Removed $dirpart\n";
1003 print "$filepart\n";
1006 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
1007 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1008 or $meta->{filehash} eq 'added' )
1010 # normal update, just send the new revision (either U=Update,
1011 # or A=Add, or R=Remove)
1012 if ( defined($wrev) && $wrev < 0 )
1014 $log->info("Tell the client the file is scheduled for removal");
1015 print "MT text R \n";
1016 print "MT fname $filename\n";
1017 print "MT newline\n";
1018 next;
1020 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
1022 $log->info("Tell the client the file is scheduled for addition");
1023 print "MT text A \n";
1024 print "MT fname $filename\n";
1025 print "MT newline\n";
1026 next;
1029 else {
1030 $log->info("Updating '$filename' to ".$meta->{revision});
1031 print "MT +updated\n";
1032 print "MT text U \n";
1033 print "MT fname $filename\n";
1034 print "MT newline\n";
1035 print "MT -updated\n";
1038 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1040 # Don't want to actually _DO_ the update if -n specified
1041 unless ( $state->{globaloptions}{-n} )
1043 if ( defined ( $wrev ) )
1045 # instruct client we're sending a file to put in this path as a replacement
1046 print "Update-existing $dirpart\n";
1047 $log->debug("Updating existing file 'Update-existing $dirpart'");
1048 } else {
1049 # instruct client we're sending a file to put in this path as a new file
1050 print "Clear-static-directory $dirpart\n";
1051 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1052 print "Clear-sticky $dirpart\n";
1053 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1055 $log->debug("Creating new file 'Created $dirpart'");
1056 print "Created $dirpart\n";
1058 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1060 # this is an "entries" line
1061 my $kopts = kopts_from_path($filepart);
1062 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1063 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1065 # permissions
1066 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1067 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1069 # transmit file
1070 transmitfile($meta->{filehash});
1072 } else {
1073 $log->info("Updating '$filename'");
1074 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
1076 my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
1078 chdir $dir;
1079 my $file_local = $filepart . ".mine";
1080 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1081 my $file_old = $filepart . "." . $oldmeta->{revision};
1082 transmitfile($oldmeta->{filehash}, $file_old);
1083 my $file_new = $filepart . "." . $meta->{revision};
1084 transmitfile($meta->{filehash}, $file_new);
1086 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1087 $log->info("Merging $file_local, $file_old, $file_new");
1088 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
1090 $log->debug("Temporary directory for merge is $dir");
1092 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
1093 $return >>= 8;
1095 if ( $return == 0 )
1097 $log->info("Merged successfully");
1098 print "M M $filename\n";
1099 $log->debug("Merged $dirpart");
1101 # Don't want to actually _DO_ the update if -n specified
1102 unless ( $state->{globaloptions}{-n} )
1104 print "Merged $dirpart\n";
1105 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1106 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1107 my $kopts = kopts_from_path($filepart);
1108 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1109 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1112 elsif ( $return == 1 )
1114 $log->info("Merged with conflicts");
1115 print "E cvs update: conflicts found in $filename\n";
1116 print "M C $filename\n";
1118 # Don't want to actually _DO_ the update if -n specified
1119 unless ( $state->{globaloptions}{-n} )
1121 print "Merged $dirpart\n";
1122 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1123 my $kopts = kopts_from_path($filepart);
1124 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
1127 else
1129 $log->warn("Merge failed");
1130 next;
1133 # Don't want to actually _DO_ the update if -n specified
1134 unless ( $state->{globaloptions}{-n} )
1136 # permissions
1137 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1138 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1140 # transmit file, format is single integer on a line by itself (file
1141 # size) followed by the file contents
1142 # TODO : we should copy files in blocks
1143 my $data = `cat $file_local`;
1144 $log->debug("File size : " . length($data));
1145 print length($data) . "\n";
1146 print $data;
1149 chdir "/";
1154 print "ok\n";
1157 sub req_ci
1159 my ( $cmd, $data ) = @_;
1161 argsplit("ci");
1163 #$log->debug("State : " . Dumper($state));
1165 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1167 if ( $state->{method} eq 'pserver')
1169 print "error 1 pserver access cannot commit\n";
1170 exit;
1173 if ( -e $state->{CVSROOT} . "/index" )
1175 $log->warn("file 'index' already exists in the git repository");
1176 print "error 1 Index already exists in git repo\n";
1177 exit;
1180 # Grab a handle to the SQLite db and do any necessary updates
1181 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1182 $updater->update();
1184 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1185 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1186 $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");
1188 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1189 $ENV{GIT_INDEX_FILE} = $file_index;
1191 # Remember where the head was at the beginning.
1192 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1193 chomp $parenthash;
1194 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1195 print "error 1 pserver cannot find the current HEAD of module";
1196 exit;
1199 chdir $tmpdir;
1201 # populate the temporary index based
1202 system("git-read-tree", $parenthash);
1203 unless ($? == 0)
1205 die "Error running git-read-tree $state->{module} $file_index $!";
1207 $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
1209 my @committedfiles = ();
1210 my %oldmeta;
1212 # foreach file specified on the command line ...
1213 foreach my $filename ( @{$state->{args}} )
1215 my $committedfile = $filename;
1216 $filename = filecleanup($filename);
1218 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1220 my $meta = $updater->getmeta($filename);
1221 $oldmeta{$filename} = $meta;
1223 my $wrev = revparse($filename);
1225 my ( $filepart, $dirpart ) = filenamesplit($filename);
1227 # do a checkout of the file if it part of this tree
1228 if ($wrev) {
1229 system('git-checkout-index', '-f', '-u', $filename);
1230 unless ($? == 0) {
1231 die "Error running git-checkout-index -f -u $filename : $!";
1235 my $addflag = 0;
1236 my $rmflag = 0;
1237 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1238 $addflag = 1 unless ( -e $filename );
1240 # Do up to date checking
1241 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1243 # fail everything if an up to date check fails
1244 print "error 1 Up to date check failed for $filename\n";
1245 chdir "/";
1246 exit;
1249 push @committedfiles, $committedfile;
1250 $log->info("Committing $filename");
1252 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1254 unless ( $rmflag )
1256 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1257 rename $state->{entries}{$filename}{modified_filename},$filename;
1259 # Calculate modes to remove
1260 my $invmode = "";
1261 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1263 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1264 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1267 if ( $rmflag )
1269 $log->info("Removing file '$filename'");
1270 unlink($filename);
1271 system("git-update-index", "--remove", $filename);
1273 elsif ( $addflag )
1275 $log->info("Adding file '$filename'");
1276 system("git-update-index", "--add", $filename);
1277 } else {
1278 $log->info("Updating file '$filename'");
1279 system("git-update-index", $filename);
1283 unless ( scalar(@committedfiles) > 0 )
1285 print "E No files to commit\n";
1286 print "ok\n";
1287 chdir "/";
1288 return;
1291 my $treehash = `git-write-tree`;
1292 chomp $treehash;
1294 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1296 # write our commit message out if we have one ...
1297 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1298 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1299 print $msg_fh "\n\nvia git-CVS emulator\n";
1300 close $msg_fh;
1302 my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1303 chomp($commithash);
1304 $log->info("Commit hash : $commithash");
1306 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1308 $log->warn("Commit failed (Invalid commit hash)");
1309 print "error 1 Commit failed (unknown reason)\n";
1310 chdir "/";
1311 exit;
1314 # Check that this is allowed, just as we would with a receive-pack
1315 my @cmd = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1316 $parenthash, $commithash );
1317 if( -x $cmd[0] ) {
1318 unless( system( @cmd ) == 0 )
1320 $log->warn("Commit failed (update hook declined to update ref)");
1321 print "error 1 Commit failed (update hook declined)\n";
1322 chdir "/";
1323 exit;
1327 if (system(qw(git update-ref -m), "cvsserver ci",
1328 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1329 $log->warn("update-ref for $state->{module} failed.");
1330 print "error 1 Cannot commit -- update first\n";
1331 exit;
1334 $updater->update();
1336 # foreach file specified on the command line ...
1337 foreach my $filename ( @committedfiles )
1339 $filename = filecleanup($filename);
1341 my $meta = $updater->getmeta($filename);
1342 unless (defined $meta->{revision}) {
1343 $meta->{revision} = 1;
1346 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
1348 $log->debug("Checked-in $dirpart : $filename");
1350 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
1351 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
1353 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
1354 print "Remove-entry $dirpart\n";
1355 print "$filename\n";
1356 } else {
1357 if ($meta->{revision} == 1) {
1358 print "M initial revision: 1.1\n";
1359 } else {
1360 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
1362 print "Checked-in $dirpart\n";
1363 print "$filename\n";
1364 my $kopts = kopts_from_path($filepart);
1365 print "/$filepart/1.$meta->{revision}//$kopts/\n";
1369 chdir "/";
1370 print "ok\n";
1373 sub req_status
1375 my ( $cmd, $data ) = @_;
1377 argsplit("status");
1379 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1380 #$log->debug("status state : " . Dumper($state));
1382 # Grab a handle to the SQLite db and do any necessary updates
1383 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1384 $updater->update();
1386 # if no files were specified, we need to work out what files we should be providing status on ...
1387 argsfromdir($updater);
1389 # foreach file specified on the command line ...
1390 foreach my $filename ( @{$state->{args}} )
1392 $filename = filecleanup($filename);
1394 my $meta = $updater->getmeta($filename);
1395 my $oldmeta = $meta;
1397 my $wrev = revparse($filename);
1399 # If the working copy is an old revision, lets get that version too for comparison.
1400 if ( defined($wrev) and $wrev != $meta->{revision} )
1402 $oldmeta = $updater->getmeta($filename, $wrev);
1405 # TODO : All possible statuses aren't yet implemented
1406 my $status;
1407 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1408 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1410 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1411 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1414 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1415 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1417 ( $state->{entries}{$filename}{unchanged}
1418 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1421 # Need checkout if it exists in the repo but doesn't have a working copy
1422 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1424 # Locally modified if working copy and repo copy have the same revision but there are local changes
1425 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1427 # Needs Merge if working copy revision is less than repo copy and there are local changes
1428 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1430 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1431 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1432 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1433 $status ||= "File had conflicts on merge" if ( 0 );
1435 $status ||= "Unknown";
1437 print "M ===================================================================\n";
1438 print "M File: $filename\tStatus: $status\n";
1439 if ( defined($state->{entries}{$filename}{revision}) )
1441 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1442 } else {
1443 print "M Working revision:\tNo entry for $filename\n";
1445 if ( defined($meta->{revision}) )
1447 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
1448 print "M Sticky Tag:\t\t(none)\n";
1449 print "M Sticky Date:\t\t(none)\n";
1450 print "M Sticky Options:\t\t(none)\n";
1451 } else {
1452 print "M Repository revision:\tNo revision control file\n";
1454 print "M\n";
1457 print "ok\n";
1460 sub req_diff
1462 my ( $cmd, $data ) = @_;
1464 argsplit("diff");
1466 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1467 #$log->debug("status state : " . Dumper($state));
1469 my ($revision1, $revision2);
1470 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1472 $revision1 = $state->{opt}{r}[0];
1473 $revision2 = $state->{opt}{r}[1];
1474 } else {
1475 $revision1 = $state->{opt}{r};
1478 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1479 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1481 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1483 # Grab a handle to the SQLite db and do any necessary updates
1484 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1485 $updater->update();
1487 # if no files were specified, we need to work out what files we should be providing status on ...
1488 argsfromdir($updater);
1490 # foreach file specified on the command line ...
1491 foreach my $filename ( @{$state->{args}} )
1493 $filename = filecleanup($filename);
1495 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1497 my $wrev = revparse($filename);
1499 # We need _something_ to diff against
1500 next unless ( defined ( $wrev ) );
1502 # if we have a -r switch, use it
1503 if ( defined ( $revision1 ) )
1505 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1506 $meta1 = $updater->getmeta($filename, $revision1);
1507 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1509 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1510 next;
1512 transmitfile($meta1->{filehash}, $file1);
1514 # otherwise we just use the working copy revision
1515 else
1517 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1518 $meta1 = $updater->getmeta($filename, $wrev);
1519 transmitfile($meta1->{filehash}, $file1);
1522 # if we have a second -r switch, use it too
1523 if ( defined ( $revision2 ) )
1525 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1526 $meta2 = $updater->getmeta($filename, $revision2);
1528 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1530 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1531 next;
1534 transmitfile($meta2->{filehash}, $file2);
1536 # otherwise we just use the working copy
1537 else
1539 $file2 = $state->{entries}{$filename}{modified_filename};
1542 # if we have been given -r, and we don't have a $file2 yet, lets get one
1543 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1545 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1546 $meta2 = $updater->getmeta($filename, $wrev);
1547 transmitfile($meta2->{filehash}, $file2);
1550 # We need to have retrieved something useful
1551 next unless ( defined ( $meta1 ) );
1553 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1554 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1556 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1557 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1560 # Apparently we only show diffs for locally modified files
1561 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1563 print "M Index: $filename\n";
1564 print "M ===================================================================\n";
1565 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1566 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1567 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1568 print "M diff ";
1569 foreach my $opt ( keys %{$state->{opt}} )
1571 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1573 foreach my $value ( @{$state->{opt}{$opt}} )
1575 print "-$opt $value ";
1577 } else {
1578 print "-$opt ";
1579 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1582 print "$filename\n";
1584 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1586 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1588 if ( exists $state->{opt}{u} )
1590 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1591 } else {
1592 system("diff $file1 $file2 > $filediff");
1595 while ( <$fh> )
1597 print "M $_";
1599 close $fh;
1602 print "ok\n";
1605 sub req_log
1607 my ( $cmd, $data ) = @_;
1609 argsplit("log");
1611 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1612 #$log->debug("log state : " . Dumper($state));
1614 my ( $minrev, $maxrev );
1615 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1617 my $control = $2;
1618 $minrev = $1;
1619 $maxrev = $3;
1620 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1621 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1622 $minrev++ if ( defined($minrev) and $control eq "::" );
1625 # Grab a handle to the SQLite db and do any necessary updates
1626 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1627 $updater->update();
1629 # if no files were specified, we need to work out what files we should be providing status on ...
1630 argsfromdir($updater);
1632 # foreach file specified on the command line ...
1633 foreach my $filename ( @{$state->{args}} )
1635 $filename = filecleanup($filename);
1637 my $headmeta = $updater->getmeta($filename);
1639 my $revisions = $updater->getlog($filename);
1640 my $totalrevisions = scalar(@$revisions);
1642 if ( defined ( $minrev ) )
1644 $log->debug("Removing revisions less than $minrev");
1645 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1647 pop @$revisions;
1650 if ( defined ( $maxrev ) )
1652 $log->debug("Removing revisions greater than $maxrev");
1653 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1655 shift @$revisions;
1659 next unless ( scalar(@$revisions) );
1661 print "M \n";
1662 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1663 print "M Working file: $filename\n";
1664 print "M head: 1.$headmeta->{revision}\n";
1665 print "M branch:\n";
1666 print "M locks: strict\n";
1667 print "M access list:\n";
1668 print "M symbolic names:\n";
1669 print "M keyword substitution: kv\n";
1670 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1671 print "M description:\n";
1673 foreach my $revision ( @$revisions )
1675 print "M ----------------------------\n";
1676 print "M revision 1.$revision->{revision}\n";
1677 # reformat the date for log output
1678 $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
1679 $revision->{author} =~ s/\s+.*//;
1680 $revision->{author} =~ s/^(.{8}).*/$1/;
1681 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1682 my $commitmessage = $updater->commitmessage($revision->{commithash});
1683 $commitmessage =~ s/^/M /mg;
1684 print $commitmessage . "\n";
1686 print "M =============================================================================\n";
1689 print "ok\n";
1692 sub req_annotate
1694 my ( $cmd, $data ) = @_;
1696 argsplit("annotate");
1698 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1699 #$log->debug("status state : " . Dumper($state));
1701 # Grab a handle to the SQLite db and do any necessary updates
1702 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1703 $updater->update();
1705 # if no files were specified, we need to work out what files we should be providing annotate on ...
1706 argsfromdir($updater);
1708 # we'll need a temporary checkout dir
1709 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1710 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1711 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1713 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1714 $ENV{GIT_INDEX_FILE} = $file_index;
1716 chdir $tmpdir;
1718 # foreach file specified on the command line ...
1719 foreach my $filename ( @{$state->{args}} )
1721 $filename = filecleanup($filename);
1723 my $meta = $updater->getmeta($filename);
1725 next unless ( $meta->{revision} );
1727 # get all the commits that this file was in
1728 # in dense format -- aka skip dead revisions
1729 my $revisions = $updater->gethistorydense($filename);
1730 my $lastseenin = $revisions->[0][2];
1732 # populate the temporary index based on the latest commit were we saw
1733 # the file -- but do it cheaply without checking out any files
1734 # TODO: if we got a revision from the client, use that instead
1735 # to look up the commithash in sqlite (still good to default to
1736 # the current head as we do now)
1737 system("git-read-tree", $lastseenin);
1738 unless ($? == 0)
1740 die "Error running git-read-tree $lastseenin $file_index $!";
1742 $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1744 # do a checkout of the file
1745 system('git-checkout-index', '-f', '-u', $filename);
1746 unless ($? == 0) {
1747 die "Error running git-checkout-index -f -u $filename : $!";
1750 $log->info("Annotate $filename");
1752 # Prepare a file with the commits from the linearized
1753 # history that annotate should know about. This prevents
1754 # git-jsannotate telling us about commits we are hiding
1755 # from the client.
1757 open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
1758 for (my $i=0; $i < @$revisions; $i++)
1760 print ANNOTATEHINTS $revisions->[$i][2];
1761 if ($i+1 < @$revisions) { # have we got a parent?
1762 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1764 print ANNOTATEHINTS "\n";
1767 print ANNOTATEHINTS "\n";
1768 close ANNOTATEHINTS;
1770 my $annotatecmd = 'git-annotate';
1771 open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
1772 or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
1773 my $metadata = {};
1774 print "E Annotations for $filename\n";
1775 print "E ***************\n";
1776 while ( <ANNOTATE> )
1778 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1780 my $commithash = $1;
1781 my $data = $2;
1782 unless ( defined ( $metadata->{$commithash} ) )
1784 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1785 $metadata->{$commithash}{author} =~ s/\s+.*//;
1786 $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
1787 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1789 printf("M 1.%-5d (%-8s %10s): %s\n",
1790 $metadata->{$commithash}{revision},
1791 $metadata->{$commithash}{author},
1792 $metadata->{$commithash}{modified},
1793 $data
1795 } else {
1796 $log->warn("Error in annotate output! LINE: $_");
1797 print "E Annotate error \n";
1798 next;
1801 close ANNOTATE;
1804 # done; get out of the tempdir
1805 chdir "/";
1807 print "ok\n";
1811 # This method takes the state->{arguments} array and produces two new arrays.
1812 # The first is $state->{args} which is everything before the '--' argument, and
1813 # the second is $state->{files} which is everything after it.
1814 sub argsplit
1816 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1818 my $type = shift;
1820 $state->{args} = [];
1821 $state->{files} = [];
1822 $state->{opt} = {};
1824 if ( defined($type) )
1826 my $opt = {};
1827 $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
1828 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1829 $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
1830 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1831 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1832 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1833 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1834 $opt = { l => 0, b => 0, h => 0, R => 0, t => 0, N => 0, S => 0, r => 1, d => 1, s => 1, w => 1 } if ( $type eq "log" );
1837 while ( scalar ( @{$state->{arguments}} ) > 0 )
1839 my $arg = shift @{$state->{arguments}};
1841 next if ( $arg eq "--" );
1842 next unless ( $arg =~ /\S/ );
1844 # if the argument looks like a switch
1845 if ( $arg =~ /^-(\w)(.*)/ )
1847 # if it's a switch that takes an argument
1848 if ( $opt->{$1} )
1850 # If this switch has already been provided
1851 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1853 $state->{opt}{$1} = [ $state->{opt}{$1} ];
1854 if ( length($2) > 0 )
1856 push @{$state->{opt}{$1}},$2;
1857 } else {
1858 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1860 } else {
1861 # if there's extra data in the arg, use that as the argument for the switch
1862 if ( length($2) > 0 )
1864 $state->{opt}{$1} = $2;
1865 } else {
1866 $state->{opt}{$1} = shift @{$state->{arguments}};
1869 } else {
1870 $state->{opt}{$1} = undef;
1873 else
1875 push @{$state->{args}}, $arg;
1879 else
1881 my $mode = 0;
1883 foreach my $value ( @{$state->{arguments}} )
1885 if ( $value eq "--" )
1887 $mode++;
1888 next;
1890 push @{$state->{args}}, $value if ( $mode == 0 );
1891 push @{$state->{files}}, $value if ( $mode == 1 );
1896 # This method uses $state->{directory} to populate $state->{args} with a list of filenames
1897 sub argsfromdir
1899 my $updater = shift;
1901 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
1903 return if ( scalar ( @{$state->{args}} ) > 1 );
1905 my @gethead = @{$updater->gethead};
1907 # push added files
1908 foreach my $file (keys %{$state->{entries}}) {
1909 if ( exists $state->{entries}{$file}{revision} &&
1910 $state->{entries}{$file}{revision} == 0 )
1912 push @gethead, { name => $file, filehash => 'added' };
1916 if ( scalar(@{$state->{args}}) == 1 )
1918 my $arg = $state->{args}[0];
1919 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
1921 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
1923 foreach my $file ( @gethead )
1925 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1926 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
1927 push @{$state->{args}}, $file->{name};
1930 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
1931 } else {
1932 $log->info("Only one arg specified, populating file list automatically");
1934 $state->{args} = [];
1936 foreach my $file ( @gethead )
1938 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1939 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
1940 push @{$state->{args}}, $file->{name};
1945 # This method cleans up the $state variable after a command that uses arguments has run
1946 sub statecleanup
1948 $state->{files} = [];
1949 $state->{args} = [];
1950 $state->{arguments} = [];
1951 $state->{entries} = {};
1954 sub revparse
1956 my $filename = shift;
1958 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
1960 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
1961 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
1963 return undef;
1966 # This method takes a file hash and does a CVS "file transfer" which transmits the
1967 # size of the file, and then the file contents.
1968 # If a second argument $targetfile is given, the file is instead written out to
1969 # a file by the name of $targetfile
1970 sub transmitfile
1972 my $filehash = shift;
1973 my $targetfile = shift;
1975 if ( defined ( $filehash ) and $filehash eq "deleted" )
1977 $log->warn("filehash is 'deleted'");
1978 return;
1981 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
1983 my $type = `git-cat-file -t $filehash`;
1984 chomp $type;
1986 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
1988 my $size = `git-cat-file -s $filehash`;
1989 chomp $size;
1991 $log->debug("transmitfile($filehash) size=$size, type=$type");
1993 if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
1995 if ( defined ( $targetfile ) )
1997 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
1998 print NEWFILE $_ while ( <$fh> );
1999 close NEWFILE;
2000 } else {
2001 print "$size\n";
2002 print while ( <$fh> );
2004 close $fh or die ("Couldn't close filehandle for transmitfile()");
2005 } else {
2006 die("Couldn't execute git-cat-file");
2010 # This method takes a file name, and returns ( $dirpart, $filepart ) which
2011 # refers to the directory portion and the file portion of the filename
2012 # respectively
2013 sub filenamesplit
2015 my $filename = shift;
2016 my $fixforlocaldir = shift;
2018 my ( $filepart, $dirpart ) = ( $filename, "." );
2019 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2020 $dirpart .= "/";
2022 if ( $fixforlocaldir )
2024 $dirpart =~ s/^$state->{prependdir}//;
2027 return ( $filepart, $dirpart );
2030 sub filecleanup
2032 my $filename = shift;
2034 return undef unless(defined($filename));
2035 if ( $filename =~ /^\// )
2037 print "E absolute filenames '$filename' not supported by server\n";
2038 return undef;
2041 $filename =~ s/^\.\///g;
2042 $filename = $state->{prependdir} . $filename;
2043 return $filename;
2046 # Given a path, this function returns a string containing the kopts
2047 # that should go into that path's Entries line. For example, a binary
2048 # file should get -kb.
2049 sub kopts_from_path
2051 my ($path) = @_;
2053 # Once it exists, the git attributes system should be used to look up
2054 # what attributes apply to this path.
2056 # Until then, take the setting from the config file
2057 unless ( defined ( $cfg->{gitcvs}{allbinary} ) and $cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i )
2059 # Return "" to give no special treatment to any path
2060 return "";
2061 } else {
2062 # Alternatively, to have all files treated as if they are binary (which
2063 # is more like git itself), always return the "-kb" option
2064 return "-kb";
2068 package GITCVS::log;
2070 ####
2071 #### Copyright The Open University UK - 2006.
2072 ####
2073 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2074 #### Martin Langhoff <martin@catalyst.net.nz>
2075 ####
2076 ####
2078 use strict;
2079 use warnings;
2081 =head1 NAME
2083 GITCVS::log
2085 =head1 DESCRIPTION
2087 This module provides very crude logging with a similar interface to
2088 Log::Log4perl
2090 =head1 METHODS
2092 =cut
2094 =head2 new
2096 Creates a new log object, optionally you can specify a filename here to
2097 indicate the file to log to. If no log file is specified, you can specify one
2098 later with method setfile, or indicate you no longer want logging with method
2099 nofile.
2101 Until one of these methods is called, all log calls will buffer messages ready
2102 to write out.
2104 =cut
2105 sub new
2107 my $class = shift;
2108 my $filename = shift;
2110 my $self = {};
2112 bless $self, $class;
2114 if ( defined ( $filename ) )
2116 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2119 return $self;
2122 =head2 setfile
2124 This methods takes a filename, and attempts to open that file as the log file.
2125 If successful, all buffered data is written out to the file, and any further
2126 logging is written directly to the file.
2128 =cut
2129 sub setfile
2131 my $self = shift;
2132 my $filename = shift;
2134 if ( defined ( $filename ) )
2136 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2139 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2141 while ( my $line = shift @{$self->{buffer}} )
2143 print {$self->{fh}} $line;
2147 =head2 nofile
2149 This method indicates no logging is going to be used. It flushes any entries in
2150 the internal buffer, and sets a flag to ensure no further data is put there.
2152 =cut
2153 sub nofile
2155 my $self = shift;
2157 $self->{nolog} = 1;
2159 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2161 $self->{buffer} = [];
2164 =head2 _logopen
2166 Internal method. Returns true if the log file is open, false otherwise.
2168 =cut
2169 sub _logopen
2171 my $self = shift;
2173 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2174 return 0;
2177 =head2 debug info warn fatal
2179 These four methods are wrappers to _log. They provide the actual interface for
2180 logging data.
2182 =cut
2183 sub debug { my $self = shift; $self->_log("debug", @_); }
2184 sub info { my $self = shift; $self->_log("info" , @_); }
2185 sub warn { my $self = shift; $self->_log("warn" , @_); }
2186 sub fatal { my $self = shift; $self->_log("fatal", @_); }
2188 =head2 _log
2190 This is an internal method called by the logging functions. It generates a
2191 timestamp and pushes the logged line either to file, or internal buffer.
2193 =cut
2194 sub _log
2196 my $self = shift;
2197 my $level = shift;
2199 return if ( $self->{nolog} );
2201 my @time = localtime;
2202 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2203 $time[5] + 1900,
2204 $time[4] + 1,
2205 $time[3],
2206 $time[2],
2207 $time[1],
2208 $time[0],
2209 uc $level,
2212 if ( $self->_logopen )
2214 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2215 } else {
2216 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2220 =head2 DESTROY
2222 This method simply closes the file handle if one is open
2224 =cut
2225 sub DESTROY
2227 my $self = shift;
2229 if ( $self->_logopen )
2231 close $self->{fh};
2235 package GITCVS::updater;
2237 ####
2238 #### Copyright The Open University UK - 2006.
2239 ####
2240 #### Authors: Martyn Smith <martyn@catalyst.net.nz>
2241 #### Martin Langhoff <martin@catalyst.net.nz>
2242 ####
2243 ####
2245 use strict;
2246 use warnings;
2247 use DBI;
2249 =head1 METHODS
2251 =cut
2253 =head2 new
2255 =cut
2256 sub new
2258 my $class = shift;
2259 my $config = shift;
2260 my $module = shift;
2261 my $log = shift;
2263 die "Need to specify a git repository" unless ( defined($config) and -d $config );
2264 die "Need to specify a module" unless ( defined($module) );
2266 $class = ref($class) || $class;
2268 my $self = {};
2270 bless $self, $class;
2272 $self->{module} = $module;
2273 $self->{git_path} = $config . "/";
2275 $self->{log} = $log;
2277 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2279 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
2280 $cfg->{gitcvs}{dbdriver} || "SQLite";
2281 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2282 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2283 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2284 $cfg->{gitcvs}{dbuser} || "";
2285 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2286 $cfg->{gitcvs}{dbpass} || "";
2287 my %mapping = ( m => $module,
2288 a => $state->{method},
2289 u => getlogin || getpwuid($<) || $<,
2290 G => $self->{git_path},
2291 g => mangle_dirname($self->{git_path}),
2293 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2294 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
2296 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2297 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2298 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
2299 $self->{dbuser},
2300 $self->{dbpass});
2301 die "Error connecting to database\n" unless defined $self->{dbh};
2303 $self->{tables} = {};
2304 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
2306 $self->{tables}{$table} = 1;
2309 # Construct the revision table if required
2310 unless ( $self->{tables}{revision} )
2312 $self->{dbh}->do("
2313 CREATE TABLE revision (
2314 name TEXT NOT NULL,
2315 revision INTEGER NOT NULL,
2316 filehash TEXT NOT NULL,
2317 commithash TEXT NOT NULL,
2318 author TEXT NOT NULL,
2319 modified TEXT NOT NULL,
2320 mode TEXT NOT NULL
2323 $self->{dbh}->do("
2324 CREATE INDEX revision_ix1
2325 ON revision (name,revision)
2327 $self->{dbh}->do("
2328 CREATE INDEX revision_ix2
2329 ON revision (name,commithash)
2333 # Construct the head table if required
2334 unless ( $self->{tables}{head} )
2336 $self->{dbh}->do("
2337 CREATE TABLE head (
2338 name TEXT NOT NULL,
2339 revision INTEGER NOT NULL,
2340 filehash TEXT NOT NULL,
2341 commithash TEXT NOT NULL,
2342 author TEXT NOT NULL,
2343 modified TEXT NOT NULL,
2344 mode TEXT NOT NULL
2347 $self->{dbh}->do("
2348 CREATE INDEX head_ix1
2349 ON head (name)
2353 # Construct the properties table if required
2354 unless ( $self->{tables}{properties} )
2356 $self->{dbh}->do("
2357 CREATE TABLE properties (
2358 key TEXT NOT NULL PRIMARY KEY,
2359 value TEXT
2364 # Construct the commitmsgs table if required
2365 unless ( $self->{tables}{commitmsgs} )
2367 $self->{dbh}->do("
2368 CREATE TABLE commitmsgs (
2369 key TEXT NOT NULL PRIMARY KEY,
2370 value TEXT
2375 return $self;
2378 =head2 update
2380 =cut
2381 sub update
2383 my $self = shift;
2385 # first lets get the commit list
2386 $ENV{GIT_DIR} = $self->{git_path};
2388 my $commitsha1 = `git rev-parse $self->{module}`;
2389 chomp $commitsha1;
2391 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
2392 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2394 die("Invalid module '$self->{module}'");
2398 my $git_log;
2399 my $lastcommit = $self->_get_prop("last_commit");
2401 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
2402 return 1;
2405 # Start exclusive lock here...
2406 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2408 # TODO: log processing is memory bound
2409 # if we can parse into a 2nd file that is in reverse order
2410 # we can probably do something really efficient
2411 my @git_log_params = ('--pretty', '--parents', '--topo-order');
2413 if (defined $lastcommit) {
2414 push @git_log_params, "$lastcommit..$self->{module}";
2415 } else {
2416 push @git_log_params, $self->{module};
2418 # git-rev-list is the backend / plumbing version of git-log
2419 open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
2421 my @commits;
2423 my %commit = ();
2425 while ( <GITLOG> )
2427 chomp;
2428 if (m/^commit\s+(.*)$/) {
2429 # on ^commit lines put the just seen commit in the stack
2430 # and prime things for the next one
2431 if (keys %commit) {
2432 my %copy = %commit;
2433 unshift @commits, \%copy;
2434 %commit = ();
2436 my @parents = split(m/\s+/, $1);
2437 $commit{hash} = shift @parents;
2438 $commit{parents} = \@parents;
2439 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2440 # on rfc822-like lines seen before we see any message,
2441 # lowercase the entry and put it in the hash as key-value
2442 $commit{lc($1)} = $2;
2443 } else {
2444 # message lines - skip initial empty line
2445 # and trim whitespace
2446 if (!exists($commit{message}) && m/^\s*$/) {
2447 # define it to mark the end of headers
2448 $commit{message} = '';
2449 next;
2451 s/^\s+//; s/\s+$//; # trim ws
2452 $commit{message} .= $_ . "\n";
2455 close GITLOG;
2457 unshift @commits, \%commit if ( keys %commit );
2459 # Now all the commits are in the @commits bucket
2460 # ordered by time DESC. for each commit that needs processing,
2461 # determine whether it's following the last head we've seen or if
2462 # it's on its own branch, grab a file list, and add whatever's changed
2463 # NOTE: $lastcommit refers to the last commit from previous run
2464 # $lastpicked is the last commit we picked in this run
2465 my $lastpicked;
2466 my $head = {};
2467 if (defined $lastcommit) {
2468 $lastpicked = $lastcommit;
2471 my $committotal = scalar(@commits);
2472 my $commitcount = 0;
2474 # Load the head table into $head (for cached lookups during the update process)
2475 foreach my $file ( @{$self->gethead()} )
2477 $head->{$file->{name}} = $file;
2480 foreach my $commit ( @commits )
2482 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2483 if (defined $lastpicked)
2485 if (!in_array($lastpicked, @{$commit->{parents}}))
2487 # skip, we'll see this delta
2488 # as part of a merge later
2489 # warn "skipping off-track $commit->{hash}\n";
2490 next;
2491 } elsif (@{$commit->{parents}} > 1) {
2492 # it is a merge commit, for each parent that is
2493 # not $lastpicked, see if we can get a log
2494 # from the merge-base to that parent to put it
2495 # in the message as a merge summary.
2496 my @parents = @{$commit->{parents}};
2497 foreach my $parent (@parents) {
2498 # git-merge-base can potentially (but rarely) throw
2499 # several candidate merge bases. let's assume
2500 # that the first one is the best one.
2501 if ($parent eq $lastpicked) {
2502 next;
2504 open my $p, 'git-merge-base '. $lastpicked . ' '
2505 . $parent . '|';
2506 my @output = (<$p>);
2507 close $p;
2508 my $base = join('', @output);
2509 chomp $base;
2510 if ($base) {
2511 my @merged;
2512 # print "want to log between $base $parent \n";
2513 open(GITLOG, '-|', 'git-log', "$base..$parent")
2514 or die "Cannot call git-log: $!";
2515 my $mergedhash;
2516 while (<GITLOG>) {
2517 chomp;
2518 if (!defined $mergedhash) {
2519 if (m/^commit\s+(.+)$/) {
2520 $mergedhash = $1;
2521 } else {
2522 next;
2524 } else {
2525 # grab the first line that looks non-rfc822
2526 # aka has content after leading space
2527 if (m/^\s+(\S.*)$/) {
2528 my $title = $1;
2529 $title = substr($title,0,100); # truncate
2530 unshift @merged, "$mergedhash $title";
2531 undef $mergedhash;
2535 close GITLOG;
2536 if (@merged) {
2537 $commit->{mergemsg} = $commit->{message};
2538 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
2539 foreach my $summary (@merged) {
2540 $commit->{mergemsg} .= "\t$summary\n";
2542 $commit->{mergemsg} .= "\n\n";
2543 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2550 # convert the date to CVS-happy format
2551 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2553 if ( defined ( $lastpicked ) )
2555 my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
2556 local ($/) = "\0";
2557 while ( <FILELIST> )
2559 chomp;
2560 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
2562 die("Couldn't process git-diff-tree line : $_");
2564 my ($mode, $hash, $change) = ($1, $2, $3);
2565 my $name = <FILELIST>;
2566 chomp($name);
2568 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
2570 my $git_perms = "";
2571 $git_perms .= "r" if ( $mode & 4 );
2572 $git_perms .= "w" if ( $mode & 2 );
2573 $git_perms .= "x" if ( $mode & 1 );
2574 $git_perms = "rw" if ( $git_perms eq "" );
2576 if ( $change eq "D" )
2578 #$log->debug("DELETE $name");
2579 $head->{$name} = {
2580 name => $name,
2581 revision => $head->{$name}{revision} + 1,
2582 filehash => "deleted",
2583 commithash => $commit->{hash},
2584 modified => $commit->{date},
2585 author => $commit->{author},
2586 mode => $git_perms,
2588 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2590 elsif ( $change eq "M" )
2592 #$log->debug("MODIFIED $name");
2593 $head->{$name} = {
2594 name => $name,
2595 revision => $head->{$name}{revision} + 1,
2596 filehash => $hash,
2597 commithash => $commit->{hash},
2598 modified => $commit->{date},
2599 author => $commit->{author},
2600 mode => $git_perms,
2602 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2604 elsif ( $change eq "A" )
2606 #$log->debug("ADDED $name");
2607 $head->{$name} = {
2608 name => $name,
2609 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
2610 filehash => $hash,
2611 commithash => $commit->{hash},
2612 modified => $commit->{date},
2613 author => $commit->{author},
2614 mode => $git_perms,
2616 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2618 else
2620 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
2621 die;
2624 close FILELIST;
2625 } else {
2626 # this is used to detect files removed from the repo
2627 my $seen_files = {};
2629 my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
2630 local $/ = "\0";
2631 while ( <FILELIST> )
2633 chomp;
2634 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
2636 die("Couldn't process git-ls-tree line : $_");
2639 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2641 $seen_files->{$git_filename} = 1;
2643 my ( $oldhash, $oldrevision, $oldmode ) = (
2644 $head->{$git_filename}{filehash},
2645 $head->{$git_filename}{revision},
2646 $head->{$git_filename}{mode}
2649 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2651 $git_perms = "";
2652 $git_perms .= "r" if ( $1 & 4 );
2653 $git_perms .= "w" if ( $1 & 2 );
2654 $git_perms .= "x" if ( $1 & 1 );
2655 } else {
2656 $git_perms = "rw";
2659 # unless the file exists with the same hash, we need to update it ...
2660 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2662 my $newrevision = ( $oldrevision or 0 ) + 1;
2664 $head->{$git_filename} = {
2665 name => $git_filename,
2666 revision => $newrevision,
2667 filehash => $git_hash,
2668 commithash => $commit->{hash},
2669 modified => $commit->{date},
2670 author => $commit->{author},
2671 mode => $git_perms,
2675 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2678 close FILELIST;
2680 # Detect deleted files
2681 foreach my $file ( keys %$head )
2683 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
2685 $head->{$file}{revision}++;
2686 $head->{$file}{filehash} = "deleted";
2687 $head->{$file}{commithash} = $commit->{hash};
2688 $head->{$file}{modified} = $commit->{date};
2689 $head->{$file}{author} = $commit->{author};
2691 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
2694 # END : "Detect deleted files"
2698 if (exists $commit->{mergemsg})
2700 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
2703 $lastpicked = $commit->{hash};
2705 $self->_set_prop("last_commit", $commit->{hash});
2708 $self->delete_head();
2709 foreach my $file ( keys %$head )
2711 $self->insert_head(
2712 $file,
2713 $head->{$file}{revision},
2714 $head->{$file}{filehash},
2715 $head->{$file}{commithash},
2716 $head->{$file}{modified},
2717 $head->{$file}{author},
2718 $head->{$file}{mode},
2721 # invalidate the gethead cache
2722 $self->{gethead_cache} = undef;
2725 # Ending exclusive lock here
2726 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
2729 sub insert_rev
2731 my $self = shift;
2732 my $name = shift;
2733 my $revision = shift;
2734 my $filehash = shift;
2735 my $commithash = shift;
2736 my $modified = shift;
2737 my $author = shift;
2738 my $mode = shift;
2740 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2741 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2744 sub insert_mergelog
2746 my $self = shift;
2747 my $key = shift;
2748 my $value = shift;
2750 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2751 $insert_mergelog->execute($key, $value);
2754 sub delete_head
2756 my $self = shift;
2758 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
2759 $delete_head->execute();
2762 sub insert_head
2764 my $self = shift;
2765 my $name = shift;
2766 my $revision = shift;
2767 my $filehash = shift;
2768 my $commithash = shift;
2769 my $modified = shift;
2770 my $author = shift;
2771 my $mode = shift;
2773 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2774 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2777 sub _headrev
2779 my $self = shift;
2780 my $filename = shift;
2782 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2783 $db_query->execute($filename);
2784 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2786 return ( $hash, $revision, $mode );
2789 sub _get_prop
2791 my $self = shift;
2792 my $key = shift;
2794 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2795 $db_query->execute($key);
2796 my ( $value ) = $db_query->fetchrow_array;
2798 return $value;
2801 sub _set_prop
2803 my $self = shift;
2804 my $key = shift;
2805 my $value = shift;
2807 my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2808 $db_query->execute($value, $key);
2810 unless ( $db_query->rows )
2812 $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2813 $db_query->execute($key, $value);
2816 return $value;
2819 =head2 gethead
2821 =cut
2823 sub gethead
2825 my $self = shift;
2827 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
2829 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
2830 $db_query->execute();
2832 my $tree = [];
2833 while ( my $file = $db_query->fetchrow_hashref )
2835 push @$tree, $file;
2838 $self->{gethead_cache} = $tree;
2840 return $tree;
2843 =head2 getlog
2845 =cut
2847 sub getlog
2849 my $self = shift;
2850 my $filename = shift;
2852 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2853 $db_query->execute($filename);
2855 my $tree = [];
2856 while ( my $file = $db_query->fetchrow_hashref )
2858 push @$tree, $file;
2861 return $tree;
2864 =head2 getmeta
2866 This function takes a filename (with path) argument and returns a hashref of
2867 metadata for that file.
2869 =cut
2871 sub getmeta
2873 my $self = shift;
2874 my $filename = shift;
2875 my $revision = shift;
2877 my $db_query;
2878 if ( defined($revision) and $revision =~ /^\d+$/ )
2880 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2881 $db_query->execute($filename, $revision);
2883 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2885 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2886 $db_query->execute($filename, $revision);
2887 } else {
2888 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2889 $db_query->execute($filename);
2892 return $db_query->fetchrow_hashref;
2895 =head2 commitmessage
2897 this function takes a commithash and returns the commit message for that commit
2899 =cut
2900 sub commitmessage
2902 my $self = shift;
2903 my $commithash = shift;
2905 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2907 my $db_query;
2908 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2909 $db_query->execute($commithash);
2911 my ( $message ) = $db_query->fetchrow_array;
2913 if ( defined ( $message ) )
2915 $message .= " " if ( $message =~ /\n$/ );
2916 return $message;
2919 my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
2920 shift @lines while ( $lines[0] =~ /\S/ );
2921 $message = join("",@lines);
2922 $message .= " " if ( $message =~ /\n$/ );
2923 return $message;
2926 =head2 gethistory
2928 This function takes a filename (with path) argument and returns an arrayofarrays
2929 containing revision,filehash,commithash ordered by revision descending
2931 =cut
2932 sub gethistory
2934 my $self = shift;
2935 my $filename = shift;
2937 my $db_query;
2938 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2939 $db_query->execute($filename);
2941 return $db_query->fetchall_arrayref;
2944 =head2 gethistorydense
2946 This function takes a filename (with path) argument and returns an arrayofarrays
2947 containing revision,filehash,commithash ordered by revision descending.
2949 This version of gethistory skips deleted entries -- so it is useful for annotate.
2950 The 'dense' part is a reference to a '--dense' option available for git-rev-list
2951 and other git tools that depend on it.
2953 =cut
2954 sub gethistorydense
2956 my $self = shift;
2957 my $filename = shift;
2959 my $db_query;
2960 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2961 $db_query->execute($filename);
2963 return $db_query->fetchall_arrayref;
2966 =head2 in_array()
2968 from Array::PAT - mimics the in_array() function
2969 found in PHP. Yuck but works for small arrays.
2971 =cut
2972 sub in_array
2974 my ($check, @array) = @_;
2975 my $retval = 0;
2976 foreach my $test (@array){
2977 if($check eq $test){
2978 $retval = 1;
2981 return $retval;
2984 =head2 safe_pipe_capture
2986 an alternative to `command` that allows input to be passed as an array
2987 to work around shell problems with weird characters in arguments
2989 =cut
2990 sub safe_pipe_capture {
2992 my @output;
2994 if (my $pid = open my $child, '-|') {
2995 @output = (<$child>);
2996 close $child or die join(' ',@_).": $! $?";
2997 } else {
2998 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3000 return wantarray ? @output : join('',@output);
3003 =head2 mangle_dirname
3005 create a string from a directory name that is suitable to use as
3006 part of a filename, mainly by converting all chars except \w.- to _
3008 =cut
3009 sub mangle_dirname {
3010 my $dirname = shift;
3011 return unless defined $dirname;
3013 $dirname =~ s/[^\w.-]/_/g;
3015 return $dirname;