]> git.ipfire.org Git - thirdparty/git.git/blame - git-cvsserver.perl
Merge branch 'tl/anno'
[thirdparty/git.git] / git-cvsserver.perl
CommitLineData
3fda8c4c
ML
1#!/usr/bin/perl
2
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####
17
18use strict;
19use warnings;
20
21use Fcntl;
22use File::Temp qw/tempdir tempfile/;
23use File::Basename;
24
25my $log = GITCVS::log->new();
26my $cfg;
27
28my $DATE_LIST = {
29 Jan => "01",
30 Feb => "02",
31 Mar => "03",
32 Apr => "04",
33 May => "05",
34 Jun => "06",
35 Jul => "07",
36 Aug => "08",
37 Sep => "09",
38 Oct => "10",
39 Nov => "11",
40 Dec => "12",
41};
42
43# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
44$| = 1;
45
46#### Definition and mappings of functions ####
47
48my $methods = {
49 'Root' => \&req_Root,
50 'Valid-responses' => \&req_Validresponses,
51 'valid-requests' => \&req_validrequests,
52 'Directory' => \&req_Directory,
53 'Entry' => \&req_Entry,
54 'Modified' => \&req_Modified,
55 'Unchanged' => \&req_Unchanged,
7172aabb 56 'Questionable' => \&req_Questionable,
3fda8c4c
ML
57 'Argument' => \&req_Argument,
58 'Argumentx' => \&req_Argument,
59 'expand-modules' => \&req_expandmodules,
60 'add' => \&req_add,
61 'remove' => \&req_remove,
62 'co' => \&req_co,
63 'update' => \&req_update,
64 'ci' => \&req_ci,
65 'diff' => \&req_diff,
66 'log' => \&req_log,
7172aabb 67 'rlog' => \&req_log,
3fda8c4c
ML
68 'tag' => \&req_CATCHALL,
69 'status' => \&req_status,
70 'admin' => \&req_CATCHALL,
71 'history' => \&req_CATCHALL,
72 'watchers' => \&req_CATCHALL,
73 'editors' => \&req_CATCHALL,
74 'annotate' => \&req_annotate,
75 'Global_option' => \&req_Globaloption,
76 #'annotate' => \&req_CATCHALL,
77};
78
79##############################################
80
81
82# $state holds all the bits of information the clients sends us that could
83# potentially be useful when it comes to actually _doing_ something.
84my $state = {};
85$log->info("--------------- STARTING -----------------");
86
87my $TEMP_DIR = tempdir( CLEANUP => 1 );
88$log->debug("Temporary directory is '$TEMP_DIR'");
89
90# Keep going until the client closes the connection
91while (<STDIN>)
92{
93 chomp;
94
95 # Check to see if we've seen this method, and call appropiate function.
96 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
97 {
98 # use the $methods hash to call the appropriate sub for this command
99 #$log->info("Method : $1");
100 &{$methods->{$1}}($1,$2);
101 } else {
102 # log fatal because we don't understand this function. If this happens
103 # we're fairly screwed because we don't know if the client is expecting
104 # a response. If it is, the client will hang, we'll hang, and the whole
105 # thing will be custard.
106 $log->fatal("Don't understand command $_\n");
107 die("Unknown command $_");
108 }
109}
110
111$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
112$log->info("--------------- FINISH -----------------");
113
114# Magic catchall method.
115# This is the method that will handle all commands we haven't yet
116# implemented. It simply sends a warning to the log file indicating a
117# command that hasn't been implemented has been invoked.
118sub req_CATCHALL
119{
120 my ( $cmd, $data ) = @_;
121 $log->warn("Unhandled command : req_$cmd : $data");
122}
123
124
125# Root pathname \n
126# Response expected: no. Tell the server which CVSROOT to use. Note that
127# pathname is a local directory and not a fully qualified CVSROOT variable.
128# pathname must already exist; if creating a new root, use the init
129# request, not Root. pathname does not include the hostname of the server,
130# how to access the server, etc.; by the time the CVS protocol is in use,
131# connection, authentication, etc., are already taken care of. The Root
132# request must be sent only once, and it must be sent before any requests
133# other than Valid-responses, valid-requests, UseUnchanged, Set or init.
134sub req_Root
135{
136 my ( $cmd, $data ) = @_;
137 $log->debug("req_Root : $data");
138
139 $state->{CVSROOT} = $data;
140
141 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
cdb6760e
ML
142 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
143 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
144 print "E \n";
145 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
146 return 0;
147 }
3fda8c4c 148
cdb6760e
ML
149 my @gitvars = `git-var -l`;
150 if ($?) {
151 print "E problems executing git-var on the server -- this is not a git repository or the PATH is not set correcly.\n";
152 print "E \n";
153 print "error 1 - problem executing git-var\n";
154 return 0;
155 }
156 foreach my $line ( @gitvars )
3fda8c4c
ML
157 {
158 next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ );
159 $cfg->{$1}{$2} = $3;
160 }
161
162 unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i )
163 {
164 print "E GITCVS emulation needs to be enabled on this repo\n";
165 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
166 print "E \n";
167 print "error 1 GITCVS emulation disabled\n";
168 }
169
170 if ( defined ( $cfg->{gitcvs}{logfile} ) )
171 {
172 $log->setfile($cfg->{gitcvs}{logfile});
173 } else {
174 $log->nofile();
175 }
176}
177
178# Global_option option \n
179# Response expected: no. Transmit one of the global options `-q', `-Q',
180# `-l', `-t', `-r', or `-n'. option must be one of those strings, no
181# variations (such as combining of options) are allowed. For graceful
182# handling of valid-requests, it is probably better to make new global
183# options separate requests, rather than trying to add them to this
184# request.
185sub req_Globaloption
186{
187 my ( $cmd, $data ) = @_;
188 $log->debug("req_Globaloption : $data");
189
190 # TODO : is this data useful ???
191}
192
193# Valid-responses request-list \n
194# Response expected: no. Tell the server what responses the client will
195# accept. request-list is a space separated list of tokens.
196sub req_Validresponses
197{
198 my ( $cmd, $data ) = @_;
199 $log->debug("req_Validrepsonses : $data");
200
201 # TODO : re-enable this, currently it's not particularly useful
202 #$state->{validresponses} = [ split /\s+/, $data ];
203}
204
205# valid-requests \n
206# Response expected: yes. Ask the server to send back a Valid-requests
207# response.
208sub req_validrequests
209{
210 my ( $cmd, $data ) = @_;
211
212 $log->debug("req_validrequests");
213
214 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
215 $log->debug("SEND : ok");
216
217 print "Valid-requests " . join(" ",keys %$methods) . "\n";
218 print "ok\n";
219}
220
221# Directory local-directory \n
222# Additional data: repository \n. Response expected: no. Tell the server
223# what directory to use. The repository should be a directory name from a
224# previous server response. Note that this both gives a default for Entry
225# and Modified and also for ci and the other commands; normal usage is to
226# send Directory for each directory in which there will be an Entry or
227# Modified, and then a final Directory for the original directory, then the
228# command. The local-directory is relative to the top level at which the
229# command is occurring (i.e. the last Directory which is sent before the
230# command); to indicate that top level, `.' should be sent for
231# local-directory.
232sub req_Directory
233{
234 my ( $cmd, $data ) = @_;
235
236 my $repository = <STDIN>;
237 chomp $repository;
238
239
240 $state->{localdir} = $data;
241 $state->{repository} = $repository;
242 $state->{directory} = $repository;
243 $state->{directory} =~ s/^$state->{CVSROOT}\///;
244 $state->{module} = $1 if ($state->{directory} =~ s/^(.*?)(\/|$)//);
245 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
246
247 $log->debug("req_Directory : localdir=$data repository=$repository directory=$state->{directory} module=$state->{module}");
248}
249
250# Entry entry-line \n
251# Response expected: no. Tell the server what version of a file is on the
252# local machine. The name in entry-line is a name relative to the directory
253# most recently specified with Directory. If the user is operating on only
254# some files in a directory, Entry requests for only those files need be
255# included. If an Entry request is sent without Modified, Is-modified, or
256# Unchanged, it means the file is lost (does not exist in the working
257# directory). If both Entry and one of Modified, Is-modified, or Unchanged
258# are sent for the same file, Entry must be sent first. For a given file,
259# one can send Modified, Is-modified, or Unchanged, but not more than one
260# of these three.
261sub req_Entry
262{
263 my ( $cmd, $data ) = @_;
264
265 $log->debug("req_Entry : $data");
266
267 my @data = split(/\//, $data);
268
269 $state->{entries}{$state->{directory}.$data[1]} = {
270 revision => $data[2],
271 conflict => $data[3],
272 options => $data[4],
273 tag_or_date => $data[5],
274 };
275}
276
277# add \n
278# Response expected: yes. Add a file or directory. This uses any previous
279# Argument, Directory, Entry, or Modified requests, if they have been sent.
280# The last Directory sent specifies the working directory at the time of
281# the operation. To add a directory, send the directory to be added using
282# Directory and Argument requests.
283sub req_add
284{
285 my ( $cmd, $data ) = @_;
286
287 argsplit("add");
288
289 my $addcount = 0;
290
291 foreach my $filename ( @{$state->{args}} )
292 {
293 $filename = filecleanup($filename);
294
295 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
296 {
297 print "E cvs add: nothing known about `$filename'\n";
298 next;
299 }
300 # TODO : check we're not squashing an already existing file
301 if ( defined ( $state->{entries}{$filename}{revision} ) )
302 {
303 print "E cvs add: `$filename' has already been entered\n";
304 next;
305 }
306
307
308 my ( $filepart, $dirpart ) = filenamesplit($filename);
309
310 print "E cvs add: scheduling file `$filename' for addition\n";
311
312 print "Checked-in $dirpart\n";
313 print "$filename\n";
314 print "/$filepart/0///\n";
315
316 $addcount++;
317 }
318
319 if ( $addcount == 1 )
320 {
321 print "E cvs add: use `cvs commit' to add this file permanently\n";
322 }
323 elsif ( $addcount > 1 )
324 {
325 print "E cvs add: use `cvs commit' to add these files permanently\n";
326 }
327
328 print "ok\n";
329}
330
331# remove \n
332# Response expected: yes. Remove a file. This uses any previous Argument,
333# Directory, Entry, or Modified requests, if they have been sent. The last
334# Directory sent specifies the working directory at the time of the
335# operation. Note that this request does not actually do anything to the
336# repository; the only effect of a successful remove request is to supply
337# the client with a new entries line containing `-' to indicate a removed
338# file. In fact, the client probably could perform this operation without
339# contacting the server, although using remove may cause the server to
340# perform a few more checks. The client sends a subsequent ci request to
341# actually record the removal in the repository.
342sub req_remove
343{
344 my ( $cmd, $data ) = @_;
345
346 argsplit("remove");
347
348 # Grab a handle to the SQLite db and do any necessary updates
349 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
350 $updater->update();
351
352 #$log->debug("add state : " . Dumper($state));
353
354 my $rmcount = 0;
355
356 foreach my $filename ( @{$state->{args}} )
357 {
358 $filename = filecleanup($filename);
359
360 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
361 {
362 print "E cvs remove: file `$filename' still in working directory\n";
363 next;
364 }
365
366 my $meta = $updater->getmeta($filename);
367 my $wrev = revparse($filename);
368
369 unless ( defined ( $wrev ) )
370 {
371 print "E cvs remove: nothing known about `$filename'\n";
372 next;
373 }
374
375 if ( defined($wrev) and $wrev < 0 )
376 {
377 print "E cvs remove: file `$filename' already scheduled for removal\n";
378 next;
379 }
380
381 unless ( $wrev == $meta->{revision} )
382 {
383 # TODO : not sure if the format of this message is quite correct.
384 print "E cvs remove: Up to date check failed for `$filename'\n";
385 next;
386 }
387
388
389 my ( $filepart, $dirpart ) = filenamesplit($filename);
390
391 print "E cvs remove: scheduling `$filename' for removal\n";
392
393 print "Checked-in $dirpart\n";
394 print "$filename\n";
395 print "/$filepart/-1.$wrev///\n";
396
397 $rmcount++;
398 }
399
400 if ( $rmcount == 1 )
401 {
402 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
403 }
404 elsif ( $rmcount > 1 )
405 {
406 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
407 }
408
409 print "ok\n";
410}
411
412# Modified filename \n
413# Response expected: no. Additional data: mode, \n, file transmission. Send
414# the server a copy of one locally modified file. filename is a file within
415# the most recent directory sent with Directory; it must not contain `/'.
416# If the user is operating on only some files in a directory, only those
417# files need to be included. This can also be sent without Entry, if there
418# is no entry for the file.
419sub req_Modified
420{
421 my ( $cmd, $data ) = @_;
422
423 my $mode = <STDIN>;
424 chomp $mode;
425 my $size = <STDIN>;
426 chomp $size;
427
428 # Grab config information
429 my $blocksize = 8192;
430 my $bytesleft = $size;
431 my $tmp;
432
433 # Get a filehandle/name to write it to
434 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
435
436 # Loop over file data writing out to temporary file.
437 while ( $bytesleft )
438 {
439 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
440 read STDIN, $tmp, $blocksize;
441 print $fh $tmp;
442 $bytesleft -= $blocksize;
443 }
444
445 close $fh;
446
447 # Ensure we have something sensible for the file mode
448 if ( $mode =~ /u=(\w+)/ )
449 {
450 $mode = $1;
451 } else {
452 $mode = "rw";
453 }
454
455 # Save the file data in $state
456 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
457 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
458 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
459 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
460
461 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
462}
463
464# Unchanged filename \n
465# Response expected: no. Tell the server that filename has not been
466# modified in the checked out directory. The filename is a file within the
467# most recent directory sent with Directory; it must not contain `/'.
468sub req_Unchanged
469{
470 my ( $cmd, $data ) = @_;
471
472 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
473
474 #$log->debug("req_Unchanged : $data");
475}
476
7172aabb
ML
477# Questionable filename \n
478# Response expected: no. Additional data: no.
479# Tell the server to check whether filename should be ignored,
480# and if not, next time the server sends responses, send (in
481# a M response) `?' followed by the directory and filename.
482# filename must not contain `/'; it needs to be a file in the
483# directory named by the most recent Directory request.
484sub req_Questionable
485{
486 my ( $cmd, $data ) = @_;
487
488 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
489
490 #$log->debug("req_Questionable : $data");
491}
492
3fda8c4c
ML
493# Argument text \n
494# Response expected: no. Save argument for use in a subsequent command.
495# Arguments accumulate until an argument-using command is given, at which
496# point they are forgotten.
497# Argumentx text \n
498# Response expected: no. Append \n followed by text to the current argument
499# being saved.
500sub req_Argument
501{
502 my ( $cmd, $data ) = @_;
503
504 # TODO : Not quite sure how Argument and Argumentx differ, but I assume
505 # it's for multi-line arguments ... somehow ...
506
507 $log->debug("$cmd : $data");
508
509 push @{$state->{arguments}}, $data;
510}
511
512# expand-modules \n
513# Response expected: yes. Expand the modules which are specified in the
514# arguments. Returns the data in Module-expansion responses. Note that the
515# server can assume that this is checkout or export, not rtag or rdiff; the
516# latter do not access the working directory and thus have no need to
517# expand modules on the client side. Expand may not be the best word for
518# what this request does. It does not necessarily tell you all the files
519# contained in a module, for example. Basically it is a way of telling you
520# which working directories the server needs to know about in order to
521# handle a checkout of the specified modules. For example, suppose that the
522# server has a module defined by
523# aliasmodule -a 1dir
524# That is, one can check out aliasmodule and it will take 1dir in the
525# repository and check it out to 1dir in the working directory. Now suppose
526# the client already has this module checked out and is planning on using
527# the co request to update it. Without using expand-modules, the client
528# would have two bad choices: it could either send information about all
529# working directories under the current directory, which could be
530# unnecessarily slow, or it could be ignorant of the fact that aliasmodule
531# stands for 1dir, and neglect to send information for 1dir, which would
532# lead to incorrect operation. With expand-modules, the client would first
533# ask for the module to be expanded:
534sub req_expandmodules
535{
536 my ( $cmd, $data ) = @_;
537
538 argsplit();
539
540 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
541
542 unless ( ref $state->{arguments} eq "ARRAY" )
543 {
544 print "ok\n";
545 return;
546 }
547
548 foreach my $module ( @{$state->{arguments}} )
549 {
550 $log->debug("SEND : Module-expansion $module");
551 print "Module-expansion $module\n";
552 }
553
554 print "ok\n";
555 statecleanup();
556}
557
558# co \n
559# Response expected: yes. Get files from the repository. This uses any
560# previous Argument, Directory, Entry, or Modified requests, if they have
561# been sent. Arguments to this command are module names; the client cannot
562# know what directories they correspond to except by (1) just sending the
563# co request, and then seeing what directory names the server sends back in
564# its responses, and (2) the expand-modules request.
565sub req_co
566{
567 my ( $cmd, $data ) = @_;
568
569 argsplit("co");
570
571 my $module = $state->{args}[0];
572 my $checkout_path = $module;
573
574 # use the user specified directory if we're given it
575 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
576
577 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
578
579 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
580
581 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
582
583 # Grab a handle to the SQLite db and do any necessary updates
584 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
585 $updater->update();
586
c8c4f220
ML
587 $checkout_path =~ s|/$||; # get rid of trailing slashes
588
589 # Eclipse seems to need the Clear-sticky command
590 # to prepare the 'Entries' file for the new directory.
591 print "Clear-sticky $checkout_path/\n";
e74ee784 592 print $state->{CVSROOT} . "/$module/\n";
c8c4f220 593 print "Clear-static-directory $checkout_path/\n";
e74ee784 594 print $state->{CVSROOT} . "/$module/\n";
6be32d47
ML
595 print "Clear-sticky $checkout_path/\n"; # yes, twice
596 print $state->{CVSROOT} . "/$module/\n";
597 print "Template $checkout_path/\n";
598 print $state->{CVSROOT} . "/$module/\n";
599 print "0\n";
c8c4f220 600
3fda8c4c 601 # instruct the client that we're checking out to $checkout_path
c8c4f220
ML
602 print "E cvs checkout: Updating $checkout_path\n";
603
604 my %seendirs = ();
501c7372 605 my $lastdir ='';
3fda8c4c 606
6be32d47
ML
607 # recursive
608 sub prepdir {
609 my ($dir, $repodir, $remotedir, $seendirs) = @_;
610 my $parent = dirname($dir);
611 $dir =~ s|/+$||;
612 $repodir =~ s|/+$||;
613 $remotedir =~ s|/+$||;
614 $parent =~ s|/+$||;
615 $log->debug("announcedir $dir, $repodir, $remotedir" );
616
617 if ($parent eq '.' || $parent eq './') {
618 $parent = '';
619 }
620 # recurse to announce unseen parents first
621 if (length($parent) && !exists($seendirs->{$parent})) {
622 prepdir($parent, $repodir, $remotedir, $seendirs);
623 }
624 # Announce that we are going to modify at the parent level
625 if ($parent) {
626 print "E cvs checkout: Updating $remotedir/$parent\n";
627 } else {
628 print "E cvs checkout: Updating $remotedir\n";
629 }
630 print "Clear-sticky $remotedir/$parent/\n";
631 print "$repodir/$parent/\n";
632
633 print "Clear-static-directory $remotedir/$dir/\n";
634 print "$repodir/$dir/\n";
635 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
636 print "$repodir/$parent/\n";
637 print "Template $remotedir/$dir/\n";
638 print "$repodir/$dir/\n";
639 print "0\n";
640
641 $seendirs->{$dir} = 1;
642 }
643
3fda8c4c
ML
644 foreach my $git ( @{$updater->gethead} )
645 {
646 # Don't want to check out deleted files
647 next if ( $git->{filehash} eq "deleted" );
648
649 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
650
6be32d47
ML
651 if (length($git->{dir}) && $git->{dir} ne './'
652 && $git->{dir} ne $lastdir ) {
653 unless (exists($seendirs{$git->{dir}})) {
654 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
655 $checkout_path, \%seendirs);
656 $lastdir = $git->{dir};
657 $seendirs{$git->{dir}} = 1;
658 }
659 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
660 }
661
3fda8c4c
ML
662 # modification time of this file
663 print "Mod-time $git->{modified}\n";
664
665 # print some information to the client
3fda8c4c
ML
666 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
667 {
c8c4f220 668 print "M U $checkout_path/$git->{dir}$git->{name}\n";
3fda8c4c 669 } else {
c8c4f220 670 print "M U $checkout_path/$git->{name}\n";
3fda8c4c 671 }
c8c4f220 672
6be32d47
ML
673 # instruct client we're sending a file to put in this path
674 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
3fda8c4c 675
6be32d47 676 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
3fda8c4c
ML
677
678 # this is an "entries" line
679 print "/$git->{name}/1.$git->{revision}///\n";
680 # permissions
681 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
682
683 # transmit file
684 transmitfile($git->{filehash});
685 }
686
687 print "ok\n";
688
689 statecleanup();
690}
691
692# update \n
693# Response expected: yes. Actually do a cvs update command. This uses any
694# previous Argument, Directory, Entry, or Modified requests, if they have
695# been sent. The last Directory sent specifies the working directory at the
696# time of the operation. The -I option is not used--files which the client
697# can decide whether to ignore are not mentioned and the client sends the
698# Questionable request for others.
699sub req_update
700{
701 my ( $cmd, $data ) = @_;
702
703 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
704
705 argsplit("update");
706
858cbfba
ML
707 #
708 # It may just be a client exploring the available heads/modukles
709 # in that case, list them as top level directories and leave it
710 # at that. Eclipse uses this technique to offer you a list of
711 # projects (heads in this case) to checkout.
712 #
713 if ($state->{module} eq '') {
714 print "E cvs update: Updating .\n";
715 opendir HEADS, $state->{CVSROOT} . '/refs/heads';
716 while (my $head = readdir(HEADS)) {
717 if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
718 print "E cvs update: New directory `$head'\n";
719 }
720 }
721 closedir HEADS;
722 print "ok\n";
723 return 1;
724 }
725
726
3fda8c4c
ML
727 # Grab a handle to the SQLite db and do any necessary updates
728 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
729
730 $updater->update();
731
732 # if no files were specified, we need to work out what files we should be providing status on ...
733 argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
734
735 #$log->debug("update state : " . Dumper($state));
736
737 # foreach file specified on the commandline ...
738 foreach my $filename ( @{$state->{args}} )
739 {
740 $filename = filecleanup($filename);
741
742 # if we have a -C we should pretend we never saw modified stuff
743 if ( exists ( $state->{opt}{C} ) )
744 {
745 delete $state->{entries}{$filename}{modified_hash};
746 delete $state->{entries}{$filename}{modified_filename};
747 $state->{entries}{$filename}{unchanged} = 1;
748 }
749
750 my $meta;
751 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
752 {
753 $meta = $updater->getmeta($filename, $1);
754 } else {
755 $meta = $updater->getmeta($filename);
756 }
757
758 next unless ( $meta->{revision} );
759
760 my $oldmeta = $meta;
761
762 my $wrev = revparse($filename);
763
764 # If the working copy is an old revision, lets get that version too for comparison.
765 if ( defined($wrev) and $wrev != $meta->{revision} )
766 {
767 $oldmeta = $updater->getmeta($filename, $wrev);
768 }
769
770 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
771
ec58db15
ML
772 # Files are up to date if the working copy and repo copy have the same revision,
773 # and the working copy is unmodified _and_ the user hasn't specified -C
774 next if ( defined ( $wrev )
775 and defined($meta->{revision})
776 and $wrev == $meta->{revision}
777 and $state->{entries}{$filename}{unchanged}
778 and not exists ( $state->{opt}{C} ) );
779
780 # If the working copy and repo copy have the same revision,
781 # but the working copy is modified, tell the client it's modified
782 if ( defined ( $wrev )
783 and defined($meta->{revision})
784 and $wrev == $meta->{revision}
785 and not exists ( $state->{opt}{C} ) )
786 {
787 $log->info("Tell the client the file is modified");
788 print "MT text U\n";
789 print "MT fname $filename\n";
790 print "MT newline\n";
791 next;
792 }
3fda8c4c
ML
793
794 if ( $meta->{filehash} eq "deleted" )
795 {
796 my ( $filepart, $dirpart ) = filenamesplit($filename);
797
798 $log->info("Removing '$filename' from working copy (no longer in the repo)");
799
800 print "E cvs update: `$filename' is no longer in the repository\n";
801 print "Removed $dirpart\n";
802 print "$filepart\n";
803 }
ec58db15
ML
804 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
805 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} )
3fda8c4c
ML
806 {
807 $log->info("Updating '$filename'");
808 # normal update, just send the new revision (either U=Update, or A=Add, or R=Remove)
809 print "MT +updated\n";
810 print "MT text U\n";
811 print "MT fname $filename\n";
812 print "MT newline\n";
813 print "MT -updated\n";
814
815 my ( $filepart, $dirpart ) = filenamesplit($filename);
816 $dirpart =~ s/^$state->{directory}//;
817
818 if ( defined ( $wrev ) )
819 {
820 # instruct client we're sending a file to put in this path as a replacement
821 print "Update-existing $dirpart\n";
822 $log->debug("Updating existing file 'Update-existing $dirpart'");
823 } else {
824 # instruct client we're sending a file to put in this path as a new file
825 print "Created $dirpart\n";
826 $log->debug("Creating new file 'Created $dirpart'");
827 }
828 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
829
830 # this is an "entries" line
831 $log->debug("/$filepart/1.$meta->{revision}///");
832 print "/$filepart/1.$meta->{revision}///\n";
833
834 # permissions
835 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
836 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
837
838 # transmit file
839 transmitfile($meta->{filehash});
840 } else {
ec58db15 841 $log->info("Updating '$filename'");
3fda8c4c
ML
842 my ( $filepart, $dirpart ) = filenamesplit($meta->{name});
843
844 my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
845
846 chdir $dir;
847 my $file_local = $filepart . ".mine";
848 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
849 my $file_old = $filepart . "." . $oldmeta->{revision};
850 transmitfile($oldmeta->{filehash}, $file_old);
851 my $file_new = $filepart . "." . $meta->{revision};
852 transmitfile($meta->{filehash}, $file_new);
853
854 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
855 $log->info("Merging $file_local, $file_old, $file_new");
856
857 $log->debug("Temporary directory for merge is $dir");
858
859 my $return = system("merge", $file_local, $file_old, $file_new);
860 $return >>= 8;
861
862 if ( $return == 0 )
863 {
864 $log->info("Merged successfully");
865 print "M M $filename\n";
866 $log->debug("Update-existing $dirpart");
867 print "Update-existing $dirpart\n";
868 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
869 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
870 $log->debug("/$filepart/1.$meta->{revision}///");
871 print "/$filepart/1.$meta->{revision}///\n";
872 }
873 elsif ( $return == 1 )
874 {
875 $log->info("Merged with conflicts");
876 print "M C $filename\n";
877 print "Update-existing $dirpart\n";
878 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
879 print "/$filepart/1.$meta->{revision}/+//\n";
880 }
881 else
882 {
883 $log->warn("Merge failed");
884 next;
885 }
886
887 # permissions
888 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
889 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
890
891 # transmit file, format is single integer on a line by itself (file
892 # size) followed by the file contents
893 # TODO : we should copy files in blocks
894 my $data = `cat $file_local`;
895 $log->debug("File size : " . length($data));
896 print length($data) . "\n";
897 print $data;
898
899 chdir "/";
900 }
901
902 }
903
904 print "ok\n";
905}
906
907sub req_ci
908{
909 my ( $cmd, $data ) = @_;
910
911 argsplit("ci");
912
913 #$log->debug("State : " . Dumper($state));
914
915 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
916
917 if ( -e $state->{CVSROOT} . "/index" )
918 {
919 print "error 1 Index already exists in git repo\n";
920 exit;
921 }
922
923 my $lockfile = "$state->{CVSROOT}/refs/heads/$state->{module}.lock";
924 unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) )
925 {
926 print "error 1 Lock file '$lockfile' already exists, please try again\n";
927 exit;
928 }
929
930 # Grab a handle to the SQLite db and do any necessary updates
931 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
932 $updater->update();
933
934 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
935 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
936 $log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'");
937
938 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
939 $ENV{GIT_INDEX_FILE} = $file_index;
940
941 chdir $tmpdir;
942
943 # populate the temporary index based
944 system("git-read-tree", $state->{module});
945 unless ($? == 0)
946 {
947 die "Error running git-read-tree $state->{module} $file_index $!";
948 }
949 $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
950
951
952 my @committedfiles = ();
953
954 # foreach file specified on the commandline ...
955 foreach my $filename ( @{$state->{args}} )
956 {
957 $filename = filecleanup($filename);
958
959 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
960
961 my $meta = $updater->getmeta($filename);
962
963 my $wrev = revparse($filename);
964
965 my ( $filepart, $dirpart ) = filenamesplit($filename);
966
967 # do a checkout of the file if it part of this tree
968 if ($wrev) {
969 system('git-checkout-index', '-f', '-u', $filename);
970 unless ($? == 0) {
971 die "Error running git-checkout-index -f -u $filename : $!";
972 }
973 }
974
975 my $addflag = 0;
976 my $rmflag = 0;
977 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
978 $addflag = 1 unless ( -e $filename );
979
980 # Do up to date checking
981 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
982 {
983 # fail everything if an up to date check fails
984 print "error 1 Up to date check failed for $filename\n";
985 close LOCKFILE;
986 unlink($lockfile);
987 chdir "/";
988 exit;
989 }
990
991 push @committedfiles, $filename;
992 $log->info("Committing $filename");
993
994 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
995
996 unless ( $rmflag )
997 {
998 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
999 rename $state->{entries}{$filename}{modified_filename},$filename;
1000
1001 # Calculate modes to remove
1002 my $invmode = "";
1003 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1004
1005 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1006 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1007 }
1008
1009 if ( $rmflag )
1010 {
1011 $log->info("Removing file '$filename'");
1012 unlink($filename);
1013 system("git-update-index", "--remove", $filename);
1014 }
1015 elsif ( $addflag )
1016 {
1017 $log->info("Adding file '$filename'");
1018 system("git-update-index", "--add", $filename);
1019 } else {
1020 $log->info("Updating file '$filename'");
1021 system("git-update-index", $filename);
1022 }
1023 }
1024
1025 unless ( scalar(@committedfiles) > 0 )
1026 {
1027 print "E No files to commit\n";
1028 print "ok\n";
1029 close LOCKFILE;
1030 unlink($lockfile);
1031 chdir "/";
1032 return;
1033 }
1034
1035 my $treehash = `git-write-tree`;
1036 my $parenthash = `cat $ENV{GIT_DIR}refs/heads/$state->{module}`;
1037 chomp $treehash;
1038 chomp $parenthash;
1039
1040 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1041
1042 # write our commit message out if we have one ...
1043 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1044 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1045 print $msg_fh "\n\nvia git-CVS emulator\n";
1046 close $msg_fh;
1047
1048 my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1049 $log->info("Commit hash : $commithash");
1050
1051 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1052 {
1053 $log->warn("Commit failed (Invalid commit hash)");
1054 print "error 1 Commit failed (unknown reason)\n";
1055 close LOCKFILE;
1056 unlink($lockfile);
1057 chdir "/";
1058 exit;
1059 }
1060
1061 open FILE, ">", "$ENV{GIT_DIR}refs/heads/$state->{module}";
1062 print FILE $commithash;
1063 close FILE;
1064
1065 $updater->update();
1066
1067 # foreach file specified on the commandline ...
1068 foreach my $filename ( @committedfiles )
1069 {
1070 $filename = filecleanup($filename);
1071
1072 my $meta = $updater->getmeta($filename);
1073
1074 my ( $filepart, $dirpart ) = filenamesplit($filename);
1075
1076 $log->debug("Checked-in $dirpart : $filename");
1077
1078 if ( $meta->{filehash} eq "deleted" )
1079 {
1080 print "Remove-entry $dirpart\n";
1081 print "$filename\n";
1082 } else {
1083 print "Checked-in $dirpart\n";
1084 print "$filename\n";
1085 print "/$filepart/1.$meta->{revision}///\n";
1086 }
1087 }
1088
1089 close LOCKFILE;
1090 unlink($lockfile);
1091 chdir "/";
1092
1093 print "ok\n";
1094}
1095
1096sub req_status
1097{
1098 my ( $cmd, $data ) = @_;
1099
1100 argsplit("status");
1101
1102 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1103 #$log->debug("status state : " . Dumper($state));
1104
1105 # Grab a handle to the SQLite db and do any necessary updates
1106 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1107 $updater->update();
1108
1109 # if no files were specified, we need to work out what files we should be providing status on ...
1110 argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1111
1112 # foreach file specified on the commandline ...
1113 foreach my $filename ( @{$state->{args}} )
1114 {
1115 $filename = filecleanup($filename);
1116
1117 my $meta = $updater->getmeta($filename);
1118 my $oldmeta = $meta;
1119
1120 my $wrev = revparse($filename);
1121
1122 # If the working copy is an old revision, lets get that version too for comparison.
1123 if ( defined($wrev) and $wrev != $meta->{revision} )
1124 {
1125 $oldmeta = $updater->getmeta($filename, $wrev);
1126 }
1127
1128 # TODO : All possible statuses aren't yet implemented
1129 my $status;
1130 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1131 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1132 and
1133 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1134 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1135 );
1136
1137 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1138 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1139 and
1140 ( $state->{entries}{$filename}{unchanged}
1141 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1142 );
1143
1144 # Need checkout if it exists in the repo but doesn't have a working copy
1145 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1146
1147 # Locally modified if working copy and repo copy have the same revision but there are local changes
1148 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1149
1150 # Needs Merge if working copy revision is less than repo copy and there are local changes
1151 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1152
1153 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1154 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1155 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1156 $status ||= "File had conflicts on merge" if ( 0 );
1157
1158 $status ||= "Unknown";
1159
1160 print "M ===================================================================\n";
1161 print "M File: $filename\tStatus: $status\n";
1162 if ( defined($state->{entries}{$filename}{revision}) )
1163 {
1164 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1165 } else {
1166 print "M Working revision:\tNo entry for $filename\n";
1167 }
1168 if ( defined($meta->{revision}) )
1169 {
1170 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{repository}/$filename,v\n";
1171 print "M Sticky Tag:\t\t(none)\n";
1172 print "M Sticky Date:\t\t(none)\n";
1173 print "M Sticky Options:\t\t(none)\n";
1174 } else {
1175 print "M Repository revision:\tNo revision control file\n";
1176 }
1177 print "M\n";
1178 }
1179
1180 print "ok\n";
1181}
1182
1183sub req_diff
1184{
1185 my ( $cmd, $data ) = @_;
1186
1187 argsplit("diff");
1188
1189 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1190 #$log->debug("status state : " . Dumper($state));
1191
1192 my ($revision1, $revision2);
1193 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1194 {
1195 $revision1 = $state->{opt}{r}[0];
1196 $revision2 = $state->{opt}{r}[1];
1197 } else {
1198 $revision1 = $state->{opt}{r};
1199 }
1200
1201 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1202 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1203
1204 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1205
1206 # Grab a handle to the SQLite db and do any necessary updates
1207 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1208 $updater->update();
1209
1210 # if no files were specified, we need to work out what files we should be providing status on ...
1211 argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1212
1213 # foreach file specified on the commandline ...
1214 foreach my $filename ( @{$state->{args}} )
1215 {
1216 $filename = filecleanup($filename);
1217
1218 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1219
1220 my $wrev = revparse($filename);
1221
1222 # We need _something_ to diff against
1223 next unless ( defined ( $wrev ) );
1224
1225 # if we have a -r switch, use it
1226 if ( defined ( $revision1 ) )
1227 {
1228 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1229 $meta1 = $updater->getmeta($filename, $revision1);
1230 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1231 {
1232 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1233 next;
1234 }
1235 transmitfile($meta1->{filehash}, $file1);
1236 }
1237 # otherwise we just use the working copy revision
1238 else
1239 {
1240 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1241 $meta1 = $updater->getmeta($filename, $wrev);
1242 transmitfile($meta1->{filehash}, $file1);
1243 }
1244
1245 # if we have a second -r switch, use it too
1246 if ( defined ( $revision2 ) )
1247 {
1248 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1249 $meta2 = $updater->getmeta($filename, $revision2);
1250
1251 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1252 {
1253 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1254 next;
1255 }
1256
1257 transmitfile($meta2->{filehash}, $file2);
1258 }
1259 # otherwise we just use the working copy
1260 else
1261 {
1262 $file2 = $state->{entries}{$filename}{modified_filename};
1263 }
1264
1265 # if we have been given -r, and we don't have a $file2 yet, lets get one
1266 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1267 {
1268 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1269 $meta2 = $updater->getmeta($filename, $wrev);
1270 transmitfile($meta2->{filehash}, $file2);
1271 }
1272
1273 # We need to have retrieved something useful
1274 next unless ( defined ( $meta1 ) );
1275
1276 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1277 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1278 and
1279 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1280 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1281 );
1282
1283 # Apparently we only show diffs for locally modified files
1284 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1285
1286 print "M Index: $filename\n";
1287 print "M ===================================================================\n";
1288 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1289 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1290 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1291 print "M diff ";
1292 foreach my $opt ( keys %{$state->{opt}} )
1293 {
1294 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1295 {
1296 foreach my $value ( @{$state->{opt}{$opt}} )
1297 {
1298 print "-$opt $value ";
1299 }
1300 } else {
1301 print "-$opt ";
1302 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1303 }
1304 }
1305 print "$filename\n";
1306
1307 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1308
1309 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1310
1311 if ( exists $state->{opt}{u} )
1312 {
1313 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1314 } else {
1315 system("diff $file1 $file2 > $filediff");
1316 }
1317
1318 while ( <$fh> )
1319 {
1320 print "M $_";
1321 }
1322 close $fh;
1323 }
1324
1325 print "ok\n";
1326}
1327
1328sub req_log
1329{
1330 my ( $cmd, $data ) = @_;
1331
1332 argsplit("log");
1333
1334 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1335 #$log->debug("log state : " . Dumper($state));
1336
1337 my ( $minrev, $maxrev );
1338 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1339 {
1340 my $control = $2;
1341 $minrev = $1;
1342 $maxrev = $3;
1343 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1344 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1345 $minrev++ if ( defined($minrev) and $control eq "::" );
1346 }
1347
1348 # Grab a handle to the SQLite db and do any necessary updates
1349 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1350 $updater->update();
1351
1352 # if no files were specified, we need to work out what files we should be providing status on ...
1353 argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1354
1355 # foreach file specified on the commandline ...
1356 foreach my $filename ( @{$state->{args}} )
1357 {
1358 $filename = filecleanup($filename);
1359
1360 my $headmeta = $updater->getmeta($filename);
1361
1362 my $revisions = $updater->getlog($filename);
1363 my $totalrevisions = scalar(@$revisions);
1364
1365 if ( defined ( $minrev ) )
1366 {
1367 $log->debug("Removing revisions less than $minrev");
1368 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1369 {
1370 pop @$revisions;
1371 }
1372 }
1373 if ( defined ( $maxrev ) )
1374 {
1375 $log->debug("Removing revisions greater than $maxrev");
1376 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1377 {
1378 shift @$revisions;
1379 }
1380 }
1381
1382 next unless ( scalar(@$revisions) );
1383
1384 print "M \n";
1385 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1386 print "M Working file: $filename\n";
1387 print "M head: 1.$headmeta->{revision}\n";
1388 print "M branch:\n";
1389 print "M locks: strict\n";
1390 print "M access list:\n";
1391 print "M symbolic names:\n";
1392 print "M keyword substitution: kv\n";
1393 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1394 print "M description:\n";
1395
1396 foreach my $revision ( @$revisions )
1397 {
1398 print "M ----------------------------\n";
1399 print "M revision 1.$revision->{revision}\n";
1400 # reformat the date for log output
1401 $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}) );
1402 $revision->{author} =~ s/\s+.*//;
1403 $revision->{author} =~ s/^(.{8}).*/$1/;
1404 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1405 my $commitmessage = $updater->commitmessage($revision->{commithash});
1406 $commitmessage =~ s/^/M /mg;
1407 print $commitmessage . "\n";
1408 }
1409 print "M =============================================================================\n";
1410 }
1411
1412 print "ok\n";
1413}
1414
1415sub req_annotate
1416{
1417 my ( $cmd, $data ) = @_;
1418
1419 argsplit("annotate");
1420
1421 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1422 #$log->debug("status state : " . Dumper($state));
1423
1424 # Grab a handle to the SQLite db and do any necessary updates
1425 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1426 $updater->update();
1427
1428 # if no files were specified, we need to work out what files we should be providing annotate on ...
1429 argsfromdir($updater) if ( scalar ( @{$state->{args}} ) == 0 );
1430
1431 # we'll need a temporary checkout dir
1432 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1433 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1434 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1435
1436 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1437 $ENV{GIT_INDEX_FILE} = $file_index;
1438
1439 chdir $tmpdir;
1440
1441 # foreach file specified on the commandline ...
1442 foreach my $filename ( @{$state->{args}} )
1443 {
1444 $filename = filecleanup($filename);
1445
1446 my $meta = $updater->getmeta($filename);
1447
1448 next unless ( $meta->{revision} );
1449
1450 # get all the commits that this file was in
1451 # in dense format -- aka skip dead revisions
1452 my $revisions = $updater->gethistorydense($filename);
1453 my $lastseenin = $revisions->[0][2];
1454
1455 # populate the temporary index based on the latest commit were we saw
1456 # the file -- but do it cheaply without checking out any files
1457 # TODO: if we got a revision from the client, use that instead
1458 # to look up the commithash in sqlite (still good to default to
1459 # the current head as we do now)
1460 system("git-read-tree", $lastseenin);
1461 unless ($? == 0)
1462 {
1463 die "Error running git-read-tree $lastseenin $file_index $!";
1464 }
1465 $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1466
1467 # do a checkout of the file
1468 system('git-checkout-index', '-f', '-u', $filename);
1469 unless ($? == 0) {
1470 die "Error running git-checkout-index -f -u $filename : $!";
1471 }
1472
1473 $log->info("Annotate $filename");
1474
1475 # Prepare a file with the commits from the linearized
1476 # history that annotate should know about. This prevents
1477 # git-jsannotate telling us about commits we are hiding
1478 # from the client.
1479
1480 open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!";
1481 for (my $i=0; $i < @$revisions; $i++)
1482 {
1483 print ANNOTATEHINTS $revisions->[$i][2];
1484 if ($i+1 < @$revisions) { # have we got a parent?
1485 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1486 }
1487 print ANNOTATEHINTS "\n";
1488 }
1489
1490 print ANNOTATEHINTS "\n";
1491 close ANNOTATEHINTS;
1492
1493 my $annotatecmd = 'git-annotate';
1494 open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename)
1495 or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!";
1496 my $metadata = {};
1497 print "E Annotations for $filename\n";
1498 print "E ***************\n";
1499 while ( <ANNOTATE> )
1500 {
1501 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1502 {
1503 my $commithash = $1;
1504 my $data = $2;
1505 unless ( defined ( $metadata->{$commithash} ) )
1506 {
1507 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1508 $metadata->{$commithash}{author} =~ s/\s+.*//;
1509 $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
1510 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1511 }
1512 printf("M 1.%-5d (%-8s %10s): %s\n",
1513 $metadata->{$commithash}{revision},
1514 $metadata->{$commithash}{author},
1515 $metadata->{$commithash}{modified},
1516 $data
1517 );
1518 } else {
1519 $log->warn("Error in annotate output! LINE: $_");
1520 print "E Annotate error \n";
1521 next;
1522 }
1523 }
1524 close ANNOTATE;
1525 }
1526
1527 # done; get out of the tempdir
1528 chdir "/";
1529
1530 print "ok\n";
1531
1532}
1533
1534# This method takes the state->{arguments} array and produces two new arrays.
1535# The first is $state->{args} which is everything before the '--' argument, and
1536# the second is $state->{files} which is everything after it.
1537sub argsplit
1538{
1539 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1540
1541 my $type = shift;
1542
1543 $state->{args} = [];
1544 $state->{files} = [];
1545 $state->{opt} = {};
1546
1547 if ( defined($type) )
1548 {
1549 my $opt = {};
1550 $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" );
1551 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1552 $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" );
1553 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1554 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1555 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1556 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1557 $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" );
1558
1559
1560 while ( scalar ( @{$state->{arguments}} ) > 0 )
1561 {
1562 my $arg = shift @{$state->{arguments}};
1563
1564 next if ( $arg eq "--" );
1565 next unless ( $arg =~ /\S/ );
1566
1567 # if the argument looks like a switch
1568 if ( $arg =~ /^-(\w)(.*)/ )
1569 {
1570 # if it's a switch that takes an argument
1571 if ( $opt->{$1} )
1572 {
1573 # If this switch has already been provided
1574 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1575 {
1576 $state->{opt}{$1} = [ $state->{opt}{$1} ];
1577 if ( length($2) > 0 )
1578 {
1579 push @{$state->{opt}{$1}},$2;
1580 } else {
1581 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1582 }
1583 } else {
1584 # if there's extra data in the arg, use that as the argument for the switch
1585 if ( length($2) > 0 )
1586 {
1587 $state->{opt}{$1} = $2;
1588 } else {
1589 $state->{opt}{$1} = shift @{$state->{arguments}};
1590 }
1591 }
1592 } else {
1593 $state->{opt}{$1} = undef;
1594 }
1595 }
1596 else
1597 {
1598 push @{$state->{args}}, $arg;
1599 }
1600 }
1601 }
1602 else
1603 {
1604 my $mode = 0;
1605
1606 foreach my $value ( @{$state->{arguments}} )
1607 {
1608 if ( $value eq "--" )
1609 {
1610 $mode++;
1611 next;
1612 }
1613 push @{$state->{args}}, $value if ( $mode == 0 );
1614 push @{$state->{files}}, $value if ( $mode == 1 );
1615 }
1616 }
1617}
1618
1619# This method uses $state->{directory} to populate $state->{args} with a list of filenames
1620sub argsfromdir
1621{
1622 my $updater = shift;
1623
1624 $state->{args} = [];
1625
1626 foreach my $file ( @{$updater->gethead} )
1627 {
1628 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1629 next unless ( $file->{name} =~ s/^$state->{directory}// );
1630 push @{$state->{args}}, $file->{name};
1631 }
1632}
1633
1634# This method cleans up the $state variable after a command that uses arguments has run
1635sub statecleanup
1636{
1637 $state->{files} = [];
1638 $state->{args} = [];
1639 $state->{arguments} = [];
1640 $state->{entries} = {};
1641}
1642
1643sub revparse
1644{
1645 my $filename = shift;
1646
1647 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
1648
1649 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
1650 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
1651
1652 return undef;
1653}
1654
1655# This method takes a file hash and does a CVS "file transfer" which transmits the
1656# size of the file, and then the file contents.
1657# If a second argument $targetfile is given, the file is instead written out to
1658# a file by the name of $targetfile
1659sub transmitfile
1660{
1661 my $filehash = shift;
1662 my $targetfile = shift;
1663
1664 if ( defined ( $filehash ) and $filehash eq "deleted" )
1665 {
1666 $log->warn("filehash is 'deleted'");
1667 return;
1668 }
1669
1670 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
1671
1672 my $type = `git-cat-file -t $filehash`;
1673 chomp $type;
1674
1675 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
1676
1677 my $size = `git-cat-file -s $filehash`;
1678 chomp $size;
1679
1680 $log->debug("transmitfile($filehash) size=$size, type=$type");
1681
1682 if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
1683 {
1684 if ( defined ( $targetfile ) )
1685 {
1686 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
1687 print NEWFILE $_ while ( <$fh> );
1688 close NEWFILE;
1689 } else {
1690 print "$size\n";
1691 print while ( <$fh> );
1692 }
1693 close $fh or die ("Couldn't close filehandle for transmitfile()");
1694 } else {
1695 die("Couldn't execute git-cat-file");
1696 }
1697}
1698
1699# This method takes a file name, and returns ( $dirpart, $filepart ) which
1700# refers to the directory porition and the file portion of the filename
1701# respectively
1702sub filenamesplit
1703{
1704 my $filename = shift;
1705
1706 my ( $filepart, $dirpart ) = ( $filename, "." );
1707 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
1708 $dirpart .= "/";
1709
1710 return ( $filepart, $dirpart );
1711}
1712
1713sub filecleanup
1714{
1715 my $filename = shift;
1716
1717 return undef unless(defined($filename));
1718 if ( $filename =~ /^\// )
1719 {
1720 print "E absolute filenames '$filename' not supported by server\n";
1721 return undef;
1722 }
1723
1724 $filename =~ s/^\.\///g;
1725 $filename = $state->{directory} . $filename;
1726
1727 return $filename;
1728}
1729
1730package GITCVS::log;
1731
1732####
1733#### Copyright The Open University UK - 2006.
1734####
1735#### Authors: Martyn Smith <martyn@catalyst.net.nz>
1736#### Martin Langhoff <martin@catalyst.net.nz>
1737####
1738####
1739
1740use strict;
1741use warnings;
1742
1743=head1 NAME
1744
1745GITCVS::log
1746
1747=head1 DESCRIPTION
1748
1749This module provides very crude logging with a similar interface to
1750Log::Log4perl
1751
1752=head1 METHODS
1753
1754=cut
1755
1756=head2 new
1757
1758Creates a new log object, optionally you can specify a filename here to
1759indicate the file to log to. If no log file is specified, you can specifiy one
1760later with method setfile, or indicate you no longer want logging with method
1761nofile.
1762
1763Until one of these methods is called, all log calls will buffer messages ready
1764to write out.
1765
1766=cut
1767sub new
1768{
1769 my $class = shift;
1770 my $filename = shift;
1771
1772 my $self = {};
1773
1774 bless $self, $class;
1775
1776 if ( defined ( $filename ) )
1777 {
1778 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
1779 }
1780
1781 return $self;
1782}
1783
1784=head2 setfile
1785
1786This methods takes a filename, and attempts to open that file as the log file.
1787If successful, all buffered data is written out to the file, and any further
1788logging is written directly to the file.
1789
1790=cut
1791sub setfile
1792{
1793 my $self = shift;
1794 my $filename = shift;
1795
1796 if ( defined ( $filename ) )
1797 {
1798 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
1799 }
1800
1801 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
1802
1803 while ( my $line = shift @{$self->{buffer}} )
1804 {
1805 print {$self->{fh}} $line;
1806 }
1807}
1808
1809=head2 nofile
1810
1811This method indicates no logging is going to be used. It flushes any entries in
1812the internal buffer, and sets a flag to ensure no further data is put there.
1813
1814=cut
1815sub nofile
1816{
1817 my $self = shift;
1818
1819 $self->{nolog} = 1;
1820
1821 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
1822
1823 $self->{buffer} = [];
1824}
1825
1826=head2 _logopen
1827
1828Internal method. Returns true if the log file is open, false otherwise.
1829
1830=cut
1831sub _logopen
1832{
1833 my $self = shift;
1834
1835 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
1836 return 0;
1837}
1838
1839=head2 debug info warn fatal
1840
1841These four methods are wrappers to _log. They provide the actual interface for
1842logging data.
1843
1844=cut
1845sub debug { my $self = shift; $self->_log("debug", @_); }
1846sub info { my $self = shift; $self->_log("info" , @_); }
1847sub warn { my $self = shift; $self->_log("warn" , @_); }
1848sub fatal { my $self = shift; $self->_log("fatal", @_); }
1849
1850=head2 _log
1851
1852This is an internal method called by the logging functions. It generates a
1853timestamp and pushes the logged line either to file, or internal buffer.
1854
1855=cut
1856sub _log
1857{
1858 my $self = shift;
1859 my $level = shift;
1860
1861 return if ( $self->{nolog} );
1862
1863 my @time = localtime;
1864 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
1865 $time[5] + 1900,
1866 $time[4] + 1,
1867 $time[3],
1868 $time[2],
1869 $time[1],
1870 $time[0],
1871 uc $level,
1872 );
1873
1874 if ( $self->_logopen )
1875 {
1876 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
1877 } else {
1878 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
1879 }
1880}
1881
1882=head2 DESTROY
1883
1884This method simply closes the file handle if one is open
1885
1886=cut
1887sub DESTROY
1888{
1889 my $self = shift;
1890
1891 if ( $self->_logopen )
1892 {
1893 close $self->{fh};
1894 }
1895}
1896
1897package GITCVS::updater;
1898
1899####
1900#### Copyright The Open University UK - 2006.
1901####
1902#### Authors: Martyn Smith <martyn@catalyst.net.nz>
1903#### Martin Langhoff <martin@catalyst.net.nz>
1904####
1905####
1906
1907use strict;
1908use warnings;
1909use DBI;
1910
1911=head1 METHODS
1912
1913=cut
1914
1915=head2 new
1916
1917=cut
1918sub new
1919{
1920 my $class = shift;
1921 my $config = shift;
1922 my $module = shift;
1923 my $log = shift;
1924
1925 die "Need to specify a git repository" unless ( defined($config) and -d $config );
1926 die "Need to specify a module" unless ( defined($module) );
1927
1928 $class = ref($class) || $class;
1929
1930 my $self = {};
1931
1932 bless $self, $class;
1933
1934 $self->{dbdir} = $config . "/";
1935 die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} );
1936
1937 $self->{module} = $module;
1938 $self->{file} = $self->{dbdir} . "/gitcvs.$module.sqlite";
1939
1940 $self->{git_path} = $config . "/";
1941
1942 $self->{log} = $log;
1943
1944 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
1945
1946 $self->{dbh} = DBI->connect("dbi:SQLite:dbname=" . $self->{file},"","");
1947
1948 $self->{tables} = {};
1949 foreach my $table ( $self->{dbh}->tables )
1950 {
1951 $table =~ s/^"//;
1952 $table =~ s/"$//;
1953 $self->{tables}{$table} = 1;
1954 }
1955
1956 # Construct the revision table if required
1957 unless ( $self->{tables}{revision} )
1958 {
1959 $self->{dbh}->do("
1960 CREATE TABLE revision (
1961 name TEXT NOT NULL,
1962 revision INTEGER NOT NULL,
1963 filehash TEXT NOT NULL,
1964 commithash TEXT NOT NULL,
1965 author TEXT NOT NULL,
1966 modified TEXT NOT NULL,
1967 mode TEXT NOT NULL
1968 )
1969 ");
1970 }
1971
1972 # Construct the revision table if required
1973 unless ( $self->{tables}{head} )
1974 {
1975 $self->{dbh}->do("
1976 CREATE TABLE head (
1977 name TEXT NOT NULL,
1978 revision INTEGER NOT NULL,
1979 filehash TEXT NOT NULL,
1980 commithash TEXT NOT NULL,
1981 author TEXT NOT NULL,
1982 modified TEXT NOT NULL,
1983 mode TEXT NOT NULL
1984 )
1985 ");
1986 }
1987
1988 # Construct the properties table if required
1989 unless ( $self->{tables}{properties} )
1990 {
1991 $self->{dbh}->do("
1992 CREATE TABLE properties (
1993 key TEXT NOT NULL PRIMARY KEY,
1994 value TEXT
1995 )
1996 ");
1997 }
1998
1999 # Construct the commitmsgs table if required
2000 unless ( $self->{tables}{commitmsgs} )
2001 {
2002 $self->{dbh}->do("
2003 CREATE TABLE commitmsgs (
2004 key TEXT NOT NULL PRIMARY KEY,
2005 value TEXT
2006 )
2007 ");
2008 }
2009
2010 return $self;
2011}
2012
2013=head2 update
2014
2015=cut
2016sub update
2017{
2018 my $self = shift;
2019
2020 # first lets get the commit list
2021 $ENV{GIT_DIR} = $self->{git_path};
2022
2023 # prepare database queries
2024 my $db_insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2025 my $db_insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2026 my $db_delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
2027 my $db_insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2028
2029 my $commitinfo = `git-cat-file commit $self->{module} 2>&1`;
2030 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2031 {
2032 die("Invalid module '$self->{module}'");
2033 }
2034
2035
2036 my $git_log;
2037 my $lastcommit = $self->_get_prop("last_commit");
2038
2039 # Start exclusive lock here...
2040 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2041
2042 # TODO: log processing is memory bound
2043 # if we can parse into a 2nd file that is in reverse order
2044 # we can probably do something really efficient
2045 my @git_log_params = ('--parents', '--topo-order');
2046
2047 if (defined $lastcommit) {
2048 push @git_log_params, "$lastcommit..$self->{module}";
2049 } else {
2050 push @git_log_params, $self->{module};
2051 }
2052 open(GITLOG, '-|', 'git-log', @git_log_params) or die "Cannot call git-log: $!";
2053
2054 my @commits;
2055
2056 my %commit = ();
2057
2058 while ( <GITLOG> )
2059 {
2060 chomp;
2061 if (m/^commit\s+(.*)$/) {
2062 # on ^commit lines put the just seen commit in the stack
2063 # and prime things for the next one
2064 if (keys %commit) {
2065 my %copy = %commit;
2066 unshift @commits, \%copy;
2067 %commit = ();
2068 }
2069 my @parents = split(m/\s+/, $1);
2070 $commit{hash} = shift @parents;
2071 $commit{parents} = \@parents;
2072 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2073 # on rfc822-like lines seen before we see any message,
2074 # lowercase the entry and put it in the hash as key-value
2075 $commit{lc($1)} = $2;
2076 } else {
2077 # message lines - skip initial empty line
2078 # and trim whitespace
2079 if (!exists($commit{message}) && m/^\s*$/) {
2080 # define it to mark the end of headers
2081 $commit{message} = '';
2082 next;
2083 }
2084 s/^\s+//; s/\s+$//; # trim ws
2085 $commit{message} .= $_ . "\n";
2086 }
2087 }
2088 close GITLOG;
2089
2090 unshift @commits, \%commit if ( keys %commit );
2091
2092 # Now all the commits are in the @commits bucket
2093 # ordered by time DESC. for each commit that needs processing,
2094 # determine whether it's following the last head we've seen or if
2095 # it's on its own branch, grab a file list, and add whatever's changed
2096 # NOTE: $lastcommit refers to the last commit from previous run
2097 # $lastpicked is the last commit we picked in this run
2098 my $lastpicked;
2099 my $head = {};
2100 if (defined $lastcommit) {
2101 $lastpicked = $lastcommit;
2102 }
2103
2104 my $committotal = scalar(@commits);
2105 my $commitcount = 0;
2106
2107 # Load the head table into $head (for cached lookups during the update process)
2108 foreach my $file ( @{$self->gethead()} )
2109 {
2110 $head->{$file->{name}} = $file;
2111 }
2112
2113 foreach my $commit ( @commits )
2114 {
2115 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2116 if (defined $lastpicked)
2117 {
2118 if (!in_array($lastpicked, @{$commit->{parents}}))
2119 {
2120 # skip, we'll see this delta
2121 # as part of a merge later
2122 # warn "skipping off-track $commit->{hash}\n";
2123 next;
2124 } elsif (@{$commit->{parents}} > 1) {
2125 # it is a merge commit, for each parent that is
2126 # not $lastpicked, see if we can get a log
2127 # from the merge-base to that parent to put it
2128 # in the message as a merge summary.
2129 my @parents = @{$commit->{parents}};
2130 foreach my $parent (@parents) {
2131 # git-merge-base can potentially (but rarely) throw
2132 # several candidate merge bases. let's assume
2133 # that the first one is the best one.
2134 if ($parent eq $lastpicked) {
2135 next;
2136 }
2137 open my $p, 'git-merge-base '. $lastpicked . ' '
2138 . $parent . '|';
2139 my @output = (<$p>);
2140 close $p;
2141 my $base = join('', @output);
2142 chomp $base;
2143 if ($base) {
2144 my @merged;
2145 # print "want to log between $base $parent \n";
2146 open(GITLOG, '-|', 'git-log', "$base..$parent")
2147 or die "Cannot call git-log: $!";
2148 my $mergedhash;
2149 while (<GITLOG>) {
2150 chomp;
2151 if (!defined $mergedhash) {
2152 if (m/^commit\s+(.+)$/) {
2153 $mergedhash = $1;
2154 } else {
2155 next;
2156 }
2157 } else {
2158 # grab the first line that looks non-rfc822
2159 # aka has content after leading space
2160 if (m/^\s+(\S.*)$/) {
2161 my $title = $1;
2162 $title = substr($title,0,100); # truncate
2163 unshift @merged, "$mergedhash $title";
2164 undef $mergedhash;
2165 }
2166 }
2167 }
2168 close GITLOG;
2169 if (@merged) {
2170 $commit->{mergemsg} = $commit->{message};
2171 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
2172 foreach my $summary (@merged) {
2173 $commit->{mergemsg} .= "\t$summary\n";
2174 }
2175 $commit->{mergemsg} .= "\n\n";
2176 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2177 }
2178 }
2179 }
2180 }
2181 }
2182
2183 # convert the date to CVS-happy format
2184 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2185
2186 if ( defined ( $lastpicked ) )
2187 {
2188 my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
2189 while ( <FILELIST> )
2190 {
2191 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o )
2192 {
2193 die("Couldn't process git-diff-tree line : $_");
2194 }
2195
2196 # $log->debug("File mode=$1, hash=$2, change=$3, name=$4");
2197
2198 my $git_perms = "";
2199 $git_perms .= "r" if ( $1 & 4 );
2200 $git_perms .= "w" if ( $1 & 2 );
2201 $git_perms .= "x" if ( $1 & 1 );
2202 $git_perms = "rw" if ( $git_perms eq "" );
2203
2204 if ( $3 eq "D" )
2205 {
2206 #$log->debug("DELETE $4");
2207 $head->{$4} = {
2208 name => $4,
2209 revision => $head->{$4}{revision} + 1,
2210 filehash => "deleted",
2211 commithash => $commit->{hash},
2212 modified => $commit->{date},
2213 author => $commit->{author},
2214 mode => $git_perms,
2215 };
2216 $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2217 }
2218 elsif ( $3 eq "M" )
2219 {
2220 #$log->debug("MODIFIED $4");
2221 $head->{$4} = {
2222 name => $4,
2223 revision => $head->{$4}{revision} + 1,
2224 filehash => $2,
2225 commithash => $commit->{hash},
2226 modified => $commit->{date},
2227 author => $commit->{author},
2228 mode => $git_perms,
2229 };
2230 $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2231 }
2232 elsif ( $3 eq "A" )
2233 {
2234 #$log->debug("ADDED $4");
2235 $head->{$4} = {
2236 name => $4,
2237 revision => 1,
2238 filehash => $2,
2239 commithash => $commit->{hash},
2240 modified => $commit->{date},
2241 author => $commit->{author},
2242 mode => $git_perms,
2243 };
2244 $db_insert_rev->execute($4, $head->{$4}{revision}, $2, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2245 }
2246 else
2247 {
2248 $log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");
2249 die;
2250 }
2251 }
2252 close FILELIST;
2253 } else {
2254 # this is used to detect files removed from the repo
2255 my $seen_files = {};
2256
2257 my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
2258 while ( <FILELIST> )
2259 {
2260 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o )
2261 {
2262 die("Couldn't process git-ls-tree line : $_");
2263 }
2264
2265 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2266
2267 $seen_files->{$git_filename} = 1;
2268
2269 my ( $oldhash, $oldrevision, $oldmode ) = (
2270 $head->{$git_filename}{filehash},
2271 $head->{$git_filename}{revision},
2272 $head->{$git_filename}{mode}
2273 );
2274
2275 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2276 {
2277 $git_perms = "";
2278 $git_perms .= "r" if ( $1 & 4 );
2279 $git_perms .= "w" if ( $1 & 2 );
2280 $git_perms .= "x" if ( $1 & 1 );
2281 } else {
2282 $git_perms = "rw";
2283 }
2284
2285 # unless the file exists with the same hash, we need to update it ...
2286 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2287 {
2288 my $newrevision = ( $oldrevision or 0 ) + 1;
2289
2290 $head->{$git_filename} = {
2291 name => $git_filename,
2292 revision => $newrevision,
2293 filehash => $git_hash,
2294 commithash => $commit->{hash},
2295 modified => $commit->{date},
2296 author => $commit->{author},
2297 mode => $git_perms,
2298 };
2299
2300
2301 $db_insert_rev->execute($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
2302 }
2303 }
2304 close FILELIST;
2305
2306 # Detect deleted files
2307 foreach my $file ( keys %$head )
2308 {
2309 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
2310 {
2311 $head->{$file}{revision}++;
2312 $head->{$file}{filehash} = "deleted";
2313 $head->{$file}{commithash} = $commit->{hash};
2314 $head->{$file}{modified} = $commit->{date};
2315 $head->{$file}{author} = $commit->{author};
2316
2317 $db_insert_rev->execute($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
2318 }
2319 }
2320 # END : "Detect deleted files"
2321 }
2322
2323
2324 if (exists $commit->{mergemsg})
2325 {
2326 $db_insert_mergelog->execute($commit->{hash}, $commit->{mergemsg});
2327 }
2328
2329 $lastpicked = $commit->{hash};
2330
2331 $self->_set_prop("last_commit", $commit->{hash});
2332 }
2333
2334 $db_delete_head->execute();
2335 foreach my $file ( keys %$head )
2336 {
2337 $db_insert_head->execute(
2338 $file,
2339 $head->{$file}{revision},
2340 $head->{$file}{filehash},
2341 $head->{$file}{commithash},
2342 $head->{$file}{modified},
2343 $head->{$file}{author},
2344 $head->{$file}{mode},
2345 );
2346 }
2347 # invalidate the gethead cache
2348 $self->{gethead_cache} = undef;
2349
2350
2351 # Ending exclusive lock here
2352 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
2353}
2354
2355sub _headrev
2356{
2357 my $self = shift;
2358 my $filename = shift;
2359
2360 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2361 $db_query->execute($filename);
2362 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2363
2364 return ( $hash, $revision, $mode );
2365}
2366
2367sub _get_prop
2368{
2369 my $self = shift;
2370 my $key = shift;
2371
2372 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2373 $db_query->execute($key);
2374 my ( $value ) = $db_query->fetchrow_array;
2375
2376 return $value;
2377}
2378
2379sub _set_prop
2380{
2381 my $self = shift;
2382 my $key = shift;
2383 my $value = shift;
2384
2385 my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2386 $db_query->execute($value, $key);
2387
2388 unless ( $db_query->rows )
2389 {
2390 $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2391 $db_query->execute($key, $value);
2392 }
2393
2394 return $value;
2395}
2396
2397=head2 gethead
2398
2399=cut
2400
2401sub gethead
2402{
2403 my $self = shift;
2404
2405 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
2406
501c7372 2407 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
3fda8c4c
ML
2408 $db_query->execute();
2409
2410 my $tree = [];
2411 while ( my $file = $db_query->fetchrow_hashref )
2412 {
2413 push @$tree, $file;
2414 }
2415
2416 $self->{gethead_cache} = $tree;
2417
2418 return $tree;
2419}
2420
2421=head2 getlog
2422
2423=cut
2424
2425sub getlog
2426{
2427 my $self = shift;
2428 my $filename = shift;
2429
2430 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2431 $db_query->execute($filename);
2432
2433 my $tree = [];
2434 while ( my $file = $db_query->fetchrow_hashref )
2435 {
2436 push @$tree, $file;
2437 }
2438
2439 return $tree;
2440}
2441
2442=head2 getmeta
2443
2444This function takes a filename (with path) argument and returns a hashref of
2445metadata for that file.
2446
2447=cut
2448
2449sub getmeta
2450{
2451 my $self = shift;
2452 my $filename = shift;
2453 my $revision = shift;
2454
2455 my $db_query;
2456 if ( defined($revision) and $revision =~ /^\d+$/ )
2457 {
2458 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2459 $db_query->execute($filename, $revision);
2460 }
2461 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2462 {
2463 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2464 $db_query->execute($filename, $revision);
2465 } else {
2466 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2467 $db_query->execute($filename);
2468 }
2469
2470 return $db_query->fetchrow_hashref;
2471}
2472
2473=head2 commitmessage
2474
2475this function takes a commithash and returns the commit message for that commit
2476
2477=cut
2478sub commitmessage
2479{
2480 my $self = shift;
2481 my $commithash = shift;
2482
2483 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2484
2485 my $db_query;
2486 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2487 $db_query->execute($commithash);
2488
2489 my ( $message ) = $db_query->fetchrow_array;
2490
2491 if ( defined ( $message ) )
2492 {
2493 $message .= " " if ( $message =~ /\n$/ );
2494 return $message;
2495 }
2496
2497 my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
2498 shift @lines while ( $lines[0] =~ /\S/ );
2499 $message = join("",@lines);
2500 $message .= " " if ( $message =~ /\n$/ );
2501 return $message;
2502}
2503
2504=head2 gethistory
2505
2506This function takes a filename (with path) argument and returns an arrayofarrays
2507containing revision,filehash,commithash ordered by revision descending
2508
2509=cut
2510sub gethistory
2511{
2512 my $self = shift;
2513 my $filename = shift;
2514
2515 my $db_query;
2516 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2517 $db_query->execute($filename);
2518
2519 return $db_query->fetchall_arrayref;
2520}
2521
2522=head2 gethistorydense
2523
2524This function takes a filename (with path) argument and returns an arrayofarrays
2525containing revision,filehash,commithash ordered by revision descending.
2526
2527This version of gethistory skips deleted entries -- so it is useful for annotate.
2528The 'dense' part is a reference to a '--dense' option available for git-rev-list
2529and other git tools that depend on it.
2530
2531=cut
2532sub gethistorydense
2533{
2534 my $self = shift;
2535 my $filename = shift;
2536
2537 my $db_query;
2538 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2539 $db_query->execute($filename);
2540
2541 return $db_query->fetchall_arrayref;
2542}
2543
2544=head2 in_array()
2545
2546from Array::PAT - mimics the in_array() function
2547found in PHP. Yuck but works for small arrays.
2548
2549=cut
2550sub in_array
2551{
2552 my ($check, @array) = @_;
2553 my $retval = 0;
2554 foreach my $test (@array){
2555 if($check eq $test){
2556 $retval = 1;
2557 }
2558 }
2559 return $retval;
2560}
2561
2562=head2 safe_pipe_capture
2563
2564an alterative to `command` that allows input to be passed as an array
2565to work around shell problems with weird characters in arguments
2566
2567=cut
2568sub safe_pipe_capture {
2569
2570 my @output;
2571
2572 if (my $pid = open my $child, '-|') {
2573 @output = (<$child>);
2574 close $child or die join(' ',@_).": $! $?";
2575 } else {
2576 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
2577 }
2578 return wantarray ? @output : join('',@output);
2579}
2580
2581
25821;