]> git.ipfire.org Git - thirdparty/git.git/blame - git-cvsserver.perl
git-p4: Fix git-p4 submit to include only changed files in the perforce submit template.
[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;
4f88d3e0 20use bytes;
3fda8c4c
ML
21
22use Fcntl;
23use File::Temp qw/tempdir tempfile/;
24use File::Basename;
693b6327
FL
25use Getopt::Long qw(:config require_order no_ignore_case);
26
27my $VERSION = '@@GIT_VERSION@@';
3fda8c4c
ML
28
29my $log = GITCVS::log->new();
30my $cfg;
31
32my $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",
45};
46
47# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
48$| = 1;
49
50#### Definition and mappings of functions ####
51
52my $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,
7172aabb 60 'Questionable' => \&req_Questionable,
3fda8c4c
ML
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,
7172aabb 71 'rlog' => \&req_log,
3fda8c4c
ML
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,
81};
82
83##############################################
84
85
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.
42217f13 88my $state = { prependdir => '' };
3fda8c4c
ML
89$log->info("--------------- STARTING -----------------");
90
693b6327
FL
91my $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";
102
103my @opts = ( 'help|h|H', 'version|V',
104 'base-path=s', 'strict-paths', 'export-all' );
105GetOptions( $state, @opts )
106 or die $usage;
107
108if ($state->{version}) {
109 print "git-cvsserver version $VERSION\n";
110 exit;
111}
112if ($state->{help}) {
113 print $usage;
114 exit;
115}
116
3fda8c4c
ML
117my $TEMP_DIR = tempdir( CLEANUP => 1 );
118$log->debug("Temporary directory is '$TEMP_DIR'");
119
693b6327
FL
120$state->{method} = 'ext';
121if (@ARGV) {
122 if ($ARGV[0] eq 'pserver') {
123 $state->{method} = 'pserver';
124 shift @ARGV;
125 } elsif ($ARGV[0] eq 'server') {
126 shift @ARGV;
127 }
128}
129
130# everything else is a directory
131$state->{allowed_roots} = [ @ARGV ];
132
226bccb9
FL
133# don't export the whole system unless the users requests it
134if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
135 die "--export-all can only be used together with an explicit whitelist\n";
136}
137
91a6bf46 138# if we are called with a pserver argument,
5348b6e7 139# deal with the authentication cat before entering the
91a6bf46 140# main loop
693b6327 141if ($state->{method} eq 'pserver') {
91a6bf46 142 my $line = <STDIN>; chomp $line;
24a97d84 143 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
91a6bf46
ML
144 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
145 }
24a97d84 146 my $request = $1;
91a6bf46
ML
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";
e40a3045 154 exit 1;
91a6bf46
ML
155 }
156 $line = <STDIN>; chomp $line; # validate the password?
157 $line = <STDIN>; chomp $line;
24a97d84
FL
158 unless ($line eq "END $request REQUEST") {
159 die "E Do not understand $line -- expecting END $request REQUEST\n";
91a6bf46
ML
160 }
161 print "I LOVE YOU\n";
24a97d84 162 exit if $request eq 'VERIFICATION'; # cvs login
91a6bf46
ML
163 # and now back to our regular programme...
164}
165
3fda8c4c
ML
166# Keep going until the client closes the connection
167while (<STDIN>)
168{
169 chomp;
170
5348b6e7 171 # Check to see if we've seen this method, and call appropriate function.
3fda8c4c
ML
172 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
173 {
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 $_");
184 }
185}
186
187$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
188$log->info("--------------- FINISH -----------------");
189
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.
194sub req_CATCHALL
195{
196 my ( $cmd, $data ) = @_;
197 $log->warn("Unhandled command : req_$cmd : $data");
198}
199
200
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.
210sub req_Root
211{
212 my ( $cmd, $data ) = @_;
213 $log->debug("req_Root : $data");
214
4890888d
FL
215 unless ($data =~ m#^/#) {
216 print "error 1 Root must be an absolute pathname\n";
217 return 0;
218 }
219
fd1cd91e
FL
220 my $cvsroot = $state->{'base-path'} || '';
221 $cvsroot =~ s#/+$##;
222 $cvsroot .= $data;
223
4890888d 224 if ($state->{CVSROOT}
fd1cd91e 225 && ($state->{CVSROOT} ne $cvsroot)) {
4890888d
FL
226 print "error 1 Conflicting roots specified\n";
227 return 0;
228 }
229
fd1cd91e 230 $state->{CVSROOT} = $cvsroot;
3fda8c4c
ML
231
232 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
693b6327
FL
233
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;
243 }
244 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
245 $allowed = 1;
246 last;
247 }
248 }
249
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;
255 }
256 }
257
cdb6760e
ML
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";
693b6327
FL
260 print "E \n";
261 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
cdb6760e
ML
262 return 0;
263 }
3fda8c4c 264
e0d10e1c 265 my @gitvars = `git-config -l`;
cdb6760e 266 if ($?) {
e0d10e1c 267 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
cdb6760e 268 print "E \n";
e0d10e1c 269 print "error 1 - problem executing git-config\n";
cdb6760e
ML
270 return 0;
271 }
272 foreach my $line ( @gitvars )
3fda8c4c 273 {
f987afa8
FL
274 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/ );
275 unless ($2) {
276 $cfg->{$1}{$3} = $4;
92a39a14
FL
277 } else {
278 $cfg->{$1}{$2}{$3} = $4;
279 }
3fda8c4c
ML
280 }
281
523d12e5
JH
282 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
283 || $cfg->{gitcvs}{enabled});
226bccb9
FL
284 unless ($state->{'export-all'} ||
285 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
3fda8c4c
ML
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";
91a6bf46 290 return 0;
3fda8c4c
ML
291 }
292
d55820ce
FL
293 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
294 if ( $logfile )
3fda8c4c 295 {
d55820ce 296 $log->setfile($logfile);
3fda8c4c
ML
297 } else {
298 $log->nofile();
299 }
91a6bf46
ML
300
301 return 1;
3fda8c4c
ML
302}
303
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.
311sub req_Globaloption
312{
313 my ( $cmd, $data ) = @_;
314 $log->debug("req_Globaloption : $data");
7d90095a 315 $state->{globaloptions}{$data} = 1;
3fda8c4c
ML
316}
317
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.
321sub req_Validresponses
322{
323 my ( $cmd, $data ) = @_;
5348b6e7 324 $log->debug("req_Validresponses : $data");
3fda8c4c
ML
325
326 # TODO : re-enable this, currently it's not particularly useful
327 #$state->{validresponses} = [ split /\s+/, $data ];
328}
329
330# valid-requests \n
331# Response expected: yes. Ask the server to send back a Valid-requests
332# response.
333sub req_validrequests
334{
335 my ( $cmd, $data ) = @_;
336
337 $log->debug("req_validrequests");
338
339 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
340 $log->debug("SEND : ok");
341
342 print "Valid-requests " . join(" ",keys %$methods) . "\n";
343 print "ok\n";
344}
345
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.
357sub req_Directory
358{
359 my ( $cmd, $data ) = @_;
360
361 my $repository = <STDIN>;
362 chomp $repository;
363
364
365 $state->{localdir} = $data;
366 $state->{repository} = $repository;
7d90095a
MS
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/ );
371
372 $state->{directory} = $state->{localdir};
373 $state->{directory} = "" if ( $state->{directory} eq "." );
3fda8c4c
ML
374 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
375
d988b822 376 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
7d90095a
MS
377 {
378 $log->info("Setting prepend to '$state->{path}'");
379 $state->{prependdir} = $state->{path};
380 foreach my $entry ( keys %{$state->{entries}} )
381 {
382 $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
383 delete $state->{entries}{$entry};
384 }
385 }
386
387 if ( defined ( $state->{prependdir} ) )
388 {
389 $log->debug("Prepending '$state->{prependdir}' to state|directory");
390 $state->{directory} = $state->{prependdir} . $state->{directory}
391 }
82000d74 392 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
3fda8c4c
ML
393}
394
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.
406sub req_Entry
407{
408 my ( $cmd, $data ) = @_;
409
7d90095a 410 #$log->debug("req_Entry : $data");
3fda8c4c
ML
411
412 my @data = split(/\//, $data);
413
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],
419 };
7d90095a
MS
420
421 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
422}
423
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.
430sub req_Questionable
431{
432 my ( $cmd, $data ) = @_;
433
434 $log->debug("req_Questionable : $data");
435 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
3fda8c4c
ML
436}
437
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.
444sub req_add
445{
446 my ( $cmd, $data ) = @_;
447
448 argsplit("add");
449
4db0c8de
FL
450 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
451 $updater->update();
452
453 argsfromdir($updater);
454
3fda8c4c
ML
455 my $addcount = 0;
456
457 foreach my $filename ( @{$state->{args}} )
458 {
459 $filename = filecleanup($filename);
460
4db0c8de
FL
461 my $meta = $updater->getmeta($filename);
462 my $wrev = revparse($filename);
463
464 if ($wrev && $meta && ($wrev < 0))
465 {
466 # previously removed file, add back
467 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
468
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";
474
475 unless ( $state->{globaloptions}{-n} )
476 {
477 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
478
479 print "Created $dirpart\n";
480 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
481
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});
491 }
492
493 next;
494 }
495
3fda8c4c
ML
496 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
497 {
498 print "E cvs add: nothing known about `$filename'\n";
499 next;
500 }
501 # TODO : check we're not squashing an already existing file
502 if ( defined ( $state->{entries}{$filename}{revision} ) )
503 {
504 print "E cvs add: `$filename' has already been entered\n";
505 next;
506 }
507
7d90095a 508 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
509
510 print "E cvs add: scheduling file `$filename' for addition\n";
511
512 print "Checked-in $dirpart\n";
513 print "$filename\n";
8538e876
AP
514 my $kopts = kopts_from_path($filepart);
515 print "/$filepart/0//$kopts/\n";
3fda8c4c
ML
516
517 $addcount++;
518 }
519
520 if ( $addcount == 1 )
521 {
522 print "E cvs add: use `cvs commit' to add this file permanently\n";
523 }
524 elsif ( $addcount > 1 )
525 {
526 print "E cvs add: use `cvs commit' to add these files permanently\n";
527 }
528
529 print "ok\n";
530}
531
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.
543sub req_remove
544{
545 my ( $cmd, $data ) = @_;
546
547 argsplit("remove");
548
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();
552
553 #$log->debug("add state : " . Dumper($state));
554
555 my $rmcount = 0;
556
557 foreach my $filename ( @{$state->{args}} )
558 {
559 $filename = filecleanup($filename);
560
561 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
562 {
563 print "E cvs remove: file `$filename' still in working directory\n";
564 next;
565 }
566
567 my $meta = $updater->getmeta($filename);
568 my $wrev = revparse($filename);
569
570 unless ( defined ( $wrev ) )
571 {
572 print "E cvs remove: nothing known about `$filename'\n";
573 next;
574 }
575
576 if ( defined($wrev) and $wrev < 0 )
577 {
578 print "E cvs remove: file `$filename' already scheduled for removal\n";
579 next;
580 }
581
582 unless ( $wrev == $meta->{revision} )
583 {
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;
587 }
588
589
7d90095a 590 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
591
592 print "E cvs remove: scheduling `$filename' for removal\n";
593
594 print "Checked-in $dirpart\n";
595 print "$filename\n";
8538e876
AP
596 my $kopts = kopts_from_path($filepart);
597 print "/$filepart/-1.$wrev//$kopts/\n";
3fda8c4c
ML
598
599 $rmcount++;
600 }
601
602 if ( $rmcount == 1 )
603 {
604 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
605 }
606 elsif ( $rmcount > 1 )
607 {
608 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
609 }
610
611 print "ok\n";
612}
613
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.
621sub req_Modified
622{
623 my ( $cmd, $data ) = @_;
624
625 my $mode = <STDIN>;
a5e40798
JM
626 defined $mode
627 or (print "E end of file reading mode for $data\n"), return;
3fda8c4c
ML
628 chomp $mode;
629 my $size = <STDIN>;
a5e40798
JM
630 defined $size
631 or (print "E end of file reading size of $data\n"), return;
3fda8c4c
ML
632 chomp $size;
633
634 # Grab config information
635 my $blocksize = 8192;
636 my $bytesleft = $size;
637 my $tmp;
638
639 # Get a filehandle/name to write it to
640 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
641
642 # Loop over file data writing out to temporary file.
643 while ( $bytesleft )
644 {
645 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
646 read STDIN, $tmp, $blocksize;
647 print $fh $tmp;
648 $bytesleft -= $blocksize;
649 }
650
a5e40798
JM
651 close $fh
652 or (print "E failed to write temporary, $filename: $!\n"), return;
3fda8c4c
ML
653
654 # Ensure we have something sensible for the file mode
655 if ( $mode =~ /u=(\w+)/ )
656 {
657 $mode = $1;
658 } else {
659 $mode = "rw";
660 }
661
662 # Save the file data in $state
663 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
664 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
665 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`;
666 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
667
668 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
669}
670
671# Unchanged filename \n
672# Response expected: no. Tell the server that filename has not been
673# modified in the checked out directory. The filename is a file within the
674# most recent directory sent with Directory; it must not contain `/'.
675sub req_Unchanged
676{
677 my ( $cmd, $data ) = @_;
678
679 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
680
681 #$log->debug("req_Unchanged : $data");
682}
683
684# Argument text \n
685# Response expected: no. Save argument for use in a subsequent command.
686# Arguments accumulate until an argument-using command is given, at which
687# point they are forgotten.
688# Argumentx text \n
689# Response expected: no. Append \n followed by text to the current argument
690# being saved.
691sub req_Argument
692{
693 my ( $cmd, $data ) = @_;
694
2c3cff49 695 # Argumentx means: append to last Argument (with a newline in front)
3fda8c4c
ML
696
697 $log->debug("$cmd : $data");
698
2c3cff49
JS
699 if ( $cmd eq 'Argumentx') {
700 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
701 } else {
702 push @{$state->{arguments}}, $data;
703 }
3fda8c4c
ML
704}
705
706# expand-modules \n
707# Response expected: yes. Expand the modules which are specified in the
708# arguments. Returns the data in Module-expansion responses. Note that the
709# server can assume that this is checkout or export, not rtag or rdiff; the
710# latter do not access the working directory and thus have no need to
711# expand modules on the client side. Expand may not be the best word for
712# what this request does. It does not necessarily tell you all the files
713# contained in a module, for example. Basically it is a way of telling you
714# which working directories the server needs to know about in order to
715# handle a checkout of the specified modules. For example, suppose that the
716# server has a module defined by
717# aliasmodule -a 1dir
718# That is, one can check out aliasmodule and it will take 1dir in the
719# repository and check it out to 1dir in the working directory. Now suppose
720# the client already has this module checked out and is planning on using
721# the co request to update it. Without using expand-modules, the client
722# would have two bad choices: it could either send information about all
723# working directories under the current directory, which could be
724# unnecessarily slow, or it could be ignorant of the fact that aliasmodule
725# stands for 1dir, and neglect to send information for 1dir, which would
726# lead to incorrect operation. With expand-modules, the client would first
727# ask for the module to be expanded:
728sub req_expandmodules
729{
730 my ( $cmd, $data ) = @_;
731
732 argsplit();
733
734 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
735
736 unless ( ref $state->{arguments} eq "ARRAY" )
737 {
738 print "ok\n";
739 return;
740 }
741
742 foreach my $module ( @{$state->{arguments}} )
743 {
744 $log->debug("SEND : Module-expansion $module");
745 print "Module-expansion $module\n";
746 }
747
748 print "ok\n";
749 statecleanup();
750}
751
752# co \n
753# Response expected: yes. Get files from the repository. This uses any
754# previous Argument, Directory, Entry, or Modified requests, if they have
755# been sent. Arguments to this command are module names; the client cannot
756# know what directories they correspond to except by (1) just sending the
757# co request, and then seeing what directory names the server sends back in
758# its responses, and (2) the expand-modules request.
759sub req_co
760{
761 my ( $cmd, $data ) = @_;
762
763 argsplit("co");
764
765 my $module = $state->{args}[0];
766 my $checkout_path = $module;
767
768 # use the user specified directory if we're given it
769 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
770
771 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
772
773 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
774
775 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
776
777 # Grab a handle to the SQLite db and do any necessary updates
778 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
779 $updater->update();
780
c8c4f220
ML
781 $checkout_path =~ s|/$||; # get rid of trailing slashes
782
783 # Eclipse seems to need the Clear-sticky command
784 # to prepare the 'Entries' file for the new directory.
785 print "Clear-sticky $checkout_path/\n";
e74ee784 786 print $state->{CVSROOT} . "/$module/\n";
c8c4f220 787 print "Clear-static-directory $checkout_path/\n";
e74ee784 788 print $state->{CVSROOT} . "/$module/\n";
6be32d47
ML
789 print "Clear-sticky $checkout_path/\n"; # yes, twice
790 print $state->{CVSROOT} . "/$module/\n";
791 print "Template $checkout_path/\n";
792 print $state->{CVSROOT} . "/$module/\n";
793 print "0\n";
c8c4f220 794
3fda8c4c 795 # instruct the client that we're checking out to $checkout_path
c8c4f220
ML
796 print "E cvs checkout: Updating $checkout_path\n";
797
798 my %seendirs = ();
501c7372 799 my $lastdir ='';
3fda8c4c 800
6be32d47
ML
801 # recursive
802 sub prepdir {
803 my ($dir, $repodir, $remotedir, $seendirs) = @_;
804 my $parent = dirname($dir);
805 $dir =~ s|/+$||;
806 $repodir =~ s|/+$||;
807 $remotedir =~ s|/+$||;
808 $parent =~ s|/+$||;
809 $log->debug("announcedir $dir, $repodir, $remotedir" );
810
811 if ($parent eq '.' || $parent eq './') {
812 $parent = '';
813 }
814 # recurse to announce unseen parents first
815 if (length($parent) && !exists($seendirs->{$parent})) {
816 prepdir($parent, $repodir, $remotedir, $seendirs);
817 }
818 # Announce that we are going to modify at the parent level
819 if ($parent) {
820 print "E cvs checkout: Updating $remotedir/$parent\n";
821 } else {
822 print "E cvs checkout: Updating $remotedir\n";
823 }
824 print "Clear-sticky $remotedir/$parent/\n";
825 print "$repodir/$parent/\n";
826
827 print "Clear-static-directory $remotedir/$dir/\n";
828 print "$repodir/$dir/\n";
829 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
830 print "$repodir/$parent/\n";
831 print "Template $remotedir/$dir/\n";
832 print "$repodir/$dir/\n";
833 print "0\n";
834
835 $seendirs->{$dir} = 1;
836 }
837
3fda8c4c
ML
838 foreach my $git ( @{$updater->gethead} )
839 {
840 # Don't want to check out deleted files
841 next if ( $git->{filehash} eq "deleted" );
842
843 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
844
6be32d47
ML
845 if (length($git->{dir}) && $git->{dir} ne './'
846 && $git->{dir} ne $lastdir ) {
847 unless (exists($seendirs{$git->{dir}})) {
848 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
849 $checkout_path, \%seendirs);
850 $lastdir = $git->{dir};
851 $seendirs{$git->{dir}} = 1;
852 }
853 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
854 }
855
3fda8c4c
ML
856 # modification time of this file
857 print "Mod-time $git->{modified}\n";
858
859 # print some information to the client
3fda8c4c
ML
860 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
861 {
c8c4f220 862 print "M U $checkout_path/$git->{dir}$git->{name}\n";
3fda8c4c 863 } else {
c8c4f220 864 print "M U $checkout_path/$git->{name}\n";
3fda8c4c 865 }
c8c4f220 866
6be32d47
ML
867 # instruct client we're sending a file to put in this path
868 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
3fda8c4c 869
6be32d47 870 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
3fda8c4c
ML
871
872 # this is an "entries" line
8538e876
AP
873 my $kopts = kopts_from_path($git->{name});
874 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
3fda8c4c
ML
875 # permissions
876 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
877
878 # transmit file
879 transmitfile($git->{filehash});
880 }
881
882 print "ok\n";
883
884 statecleanup();
885}
886
887# update \n
888# Response expected: yes. Actually do a cvs update command. This uses any
889# previous Argument, Directory, Entry, or Modified requests, if they have
890# been sent. The last Directory sent specifies the working directory at the
891# time of the operation. The -I option is not used--files which the client
892# can decide whether to ignore are not mentioned and the client sends the
893# Questionable request for others.
894sub req_update
895{
896 my ( $cmd, $data ) = @_;
897
898 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
899
900 argsplit("update");
901
858cbfba 902 #
5348b6e7 903 # It may just be a client exploring the available heads/modules
858cbfba
ML
904 # in that case, list them as top level directories and leave it
905 # at that. Eclipse uses this technique to offer you a list of
906 # projects (heads in this case) to checkout.
907 #
908 if ($state->{module} eq '') {
a5e40798
JM
909 my $heads_dir = $state->{CVSROOT} . '/refs/heads';
910 if (!opendir HEADS, $heads_dir) {
911 print "E [server aborted]: Failed to open directory, "
912 . "$heads_dir: $!\nerror\n";
913 return 0;
914 }
858cbfba 915 print "E cvs update: Updating .\n";
858cbfba
ML
916 while (my $head = readdir(HEADS)) {
917 if (-f $state->{CVSROOT} . '/refs/heads/' . $head) {
918 print "E cvs update: New directory `$head'\n";
919 }
920 }
921 closedir HEADS;
922 print "ok\n";
923 return 1;
924 }
925
926
3fda8c4c
ML
927 # Grab a handle to the SQLite db and do any necessary updates
928 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
929
930 $updater->update();
931
7d90095a 932 argsfromdir($updater);
3fda8c4c
ML
933
934 #$log->debug("update state : " . Dumper($state));
935
addf88e4 936 # foreach file specified on the command line ...
3fda8c4c
ML
937 foreach my $filename ( @{$state->{args}} )
938 {
939 $filename = filecleanup($filename);
940
7d90095a
MS
941 $log->debug("Processing file $filename");
942
3fda8c4c
ML
943 # if we have a -C we should pretend we never saw modified stuff
944 if ( exists ( $state->{opt}{C} ) )
945 {
946 delete $state->{entries}{$filename}{modified_hash};
947 delete $state->{entries}{$filename}{modified_filename};
948 $state->{entries}{$filename}{unchanged} = 1;
949 }
950
951 my $meta;
952 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
953 {
954 $meta = $updater->getmeta($filename, $1);
955 } else {
956 $meta = $updater->getmeta($filename);
957 }
958
0a7a9a12
JS
959 if ( ! defined $meta )
960 {
961 $meta = {
962 name => $filename,
963 revision => 0,
964 filehash => 'added'
965 };
966 }
3fda8c4c
ML
967
968 my $oldmeta = $meta;
969
970 my $wrev = revparse($filename);
971
972 # If the working copy is an old revision, lets get that version too for comparison.
973 if ( defined($wrev) and $wrev != $meta->{revision} )
974 {
975 $oldmeta = $updater->getmeta($filename, $wrev);
976 }
977
978 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
979
ec58db15
ML
980 # Files are up to date if the working copy and repo copy have the same revision,
981 # and the working copy is unmodified _and_ the user hasn't specified -C
982 next if ( defined ( $wrev )
983 and defined($meta->{revision})
984 and $wrev == $meta->{revision}
985 and $state->{entries}{$filename}{unchanged}
986 and not exists ( $state->{opt}{C} ) );
987
988 # If the working copy and repo copy have the same revision,
989 # but the working copy is modified, tell the client it's modified
990 if ( defined ( $wrev )
991 and defined($meta->{revision})
992 and $wrev == $meta->{revision}
cb52d9a1 993 and defined($state->{entries}{$filename}{modified_hash})
ec58db15
ML
994 and not exists ( $state->{opt}{C} ) )
995 {
996 $log->info("Tell the client the file is modified");
0a7a9a12 997 print "MT text M \n";
ec58db15
ML
998 print "MT fname $filename\n";
999 print "MT newline\n";
1000 next;
1001 }
3fda8c4c
ML
1002
1003 if ( $meta->{filehash} eq "deleted" )
1004 {
7d90095a 1005 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
3fda8c4c
ML
1006
1007 $log->info("Removing '$filename' from working copy (no longer in the repo)");
1008
1009 print "E cvs update: `$filename' is no longer in the repository\n";
7d90095a
MS
1010 # Don't want to actually _DO_ the update if -n specified
1011 unless ( $state->{globaloptions}{-n} ) {
1012 print "Removed $dirpart\n";
1013 print "$filepart\n";
1014 }
3fda8c4c 1015 }
ec58db15 1016 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
0a7a9a12
JS
1017 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1018 or $meta->{filehash} eq 'added' )
3fda8c4c 1019 {
0a7a9a12
JS
1020 # normal update, just send the new revision (either U=Update,
1021 # or A=Add, or R=Remove)
1022 if ( defined($wrev) && $wrev < 0 )
1023 {
1024 $log->info("Tell the client the file is scheduled for removal");
1025 print "MT text R \n";
1026 print "MT fname $filename\n";
1027 print "MT newline\n";
1028 next;
1029 }
535514f1 1030 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
0a7a9a12 1031 {
535514f1 1032 $log->info("Tell the client the file is scheduled for addition");
0a7a9a12
JS
1033 print "MT text A \n";
1034 print "MT fname $filename\n";
1035 print "MT newline\n";
1036 next;
1037
1038 }
1039 else {
535514f1 1040 $log->info("Updating '$filename' to ".$meta->{revision});
0a7a9a12
JS
1041 print "MT +updated\n";
1042 print "MT text U \n";
1043 print "MT fname $filename\n";
1044 print "MT newline\n";
1045 print "MT -updated\n";
1046 }
3fda8c4c 1047
7d90095a
MS
1048 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1049
1050 # Don't want to actually _DO_ the update if -n specified
1051 unless ( $state->{globaloptions}{-n} )
1052 {
1053 if ( defined ( $wrev ) )
1054 {
1055 # instruct client we're sending a file to put in this path as a replacement
1056 print "Update-existing $dirpart\n";
1057 $log->debug("Updating existing file 'Update-existing $dirpart'");
1058 } else {
1059 # instruct client we're sending a file to put in this path as a new file
1060 print "Clear-static-directory $dirpart\n";
1061 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1062 print "Clear-sticky $dirpart\n";
1063 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1064
1065 $log->debug("Creating new file 'Created $dirpart'");
1066 print "Created $dirpart\n";
1067 }
1068 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1069
1070 # this is an "entries" line
8538e876
AP
1071 my $kopts = kopts_from_path($filepart);
1072 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1073 print "/$filepart/1.$meta->{revision}//$kopts/\n";
7d90095a
MS
1074
1075 # permissions
1076 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1077 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1078
1079 # transmit file
1080 transmitfile($meta->{filehash});
1081 }
3fda8c4c 1082 } else {
ec58db15 1083 $log->info("Updating '$filename'");
7d90095a 1084 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
3fda8c4c
ML
1085
1086 my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/";
1087
1088 chdir $dir;
1089 my $file_local = $filepart . ".mine";
1090 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1091 my $file_old = $filepart . "." . $oldmeta->{revision};
1092 transmitfile($oldmeta->{filehash}, $file_old);
1093 my $file_new = $filepart . "." . $meta->{revision};
1094 transmitfile($meta->{filehash}, $file_new);
1095
1096 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1097 $log->info("Merging $file_local, $file_old, $file_new");
459bad77 1098 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
3fda8c4c
ML
1099
1100 $log->debug("Temporary directory for merge is $dir");
1101
c6b4fa96 1102 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
3fda8c4c
ML
1103 $return >>= 8;
1104
1105 if ( $return == 0 )
1106 {
1107 $log->info("Merged successfully");
1108 print "M M $filename\n";
53877846 1109 $log->debug("Merged $dirpart");
7d90095a
MS
1110
1111 # Don't want to actually _DO_ the update if -n specified
1112 unless ( $state->{globaloptions}{-n} )
1113 {
53877846 1114 print "Merged $dirpart\n";
7d90095a
MS
1115 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1116 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
8538e876
AP
1117 my $kopts = kopts_from_path($filepart);
1118 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1119 print "/$filepart/1.$meta->{revision}//$kopts/\n";
7d90095a 1120 }
3fda8c4c
ML
1121 }
1122 elsif ( $return == 1 )
1123 {
1124 $log->info("Merged with conflicts");
459bad77 1125 print "E cvs update: conflicts found in $filename\n";
3fda8c4c 1126 print "M C $filename\n";
7d90095a
MS
1127
1128 # Don't want to actually _DO_ the update if -n specified
1129 unless ( $state->{globaloptions}{-n} )
1130 {
53877846 1131 print "Merged $dirpart\n";
7d90095a 1132 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
8538e876
AP
1133 my $kopts = kopts_from_path($filepart);
1134 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
7d90095a 1135 }
3fda8c4c
ML
1136 }
1137 else
1138 {
1139 $log->warn("Merge failed");
1140 next;
1141 }
1142
7d90095a
MS
1143 # Don't want to actually _DO_ the update if -n specified
1144 unless ( $state->{globaloptions}{-n} )
1145 {
1146 # permissions
1147 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1148 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1149
1150 # transmit file, format is single integer on a line by itself (file
1151 # size) followed by the file contents
1152 # TODO : we should copy files in blocks
1153 my $data = `cat $file_local`;
1154 $log->debug("File size : " . length($data));
1155 print length($data) . "\n";
1156 print $data;
1157 }
3fda8c4c
ML
1158
1159 chdir "/";
1160 }
1161
1162 }
1163
1164 print "ok\n";
1165}
1166
1167sub req_ci
1168{
1169 my ( $cmd, $data ) = @_;
1170
1171 argsplit("ci");
1172
1173 #$log->debug("State : " . Dumper($state));
1174
1175 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1176
80573bae 1177 if ( $state->{method} eq 'pserver')
91a6bf46
ML
1178 {
1179 print "error 1 pserver access cannot commit\n";
1180 exit;
1181 }
1182
3fda8c4c
ML
1183 if ( -e $state->{CVSROOT} . "/index" )
1184 {
568907f5 1185 $log->warn("file 'index' already exists in the git repository");
3fda8c4c
ML
1186 print "error 1 Index already exists in git repo\n";
1187 exit;
1188 }
1189
3fda8c4c
ML
1190 # Grab a handle to the SQLite db and do any necessary updates
1191 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1192 $updater->update();
1193
1194 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1195 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
ada5ef3b 1196 $log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");
3fda8c4c
ML
1197
1198 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1199 $ENV{GIT_INDEX_FILE} = $file_index;
1200
ada5ef3b
JH
1201 # Remember where the head was at the beginning.
1202 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1203 chomp $parenthash;
1204 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1205 print "error 1 pserver cannot find the current HEAD of module";
1206 exit;
1207 }
1208
3fda8c4c
ML
1209 chdir $tmpdir;
1210
1211 # populate the temporary index based
ada5ef3b 1212 system("git-read-tree", $parenthash);
3fda8c4c
ML
1213 unless ($? == 0)
1214 {
1215 die "Error running git-read-tree $state->{module} $file_index $!";
1216 }
1217 $log->info("Created index '$file_index' with for head $state->{module} - exit status $?");
1218
3fda8c4c 1219 my @committedfiles = ();
392e2817 1220 my %oldmeta;
3fda8c4c 1221
addf88e4 1222 # foreach file specified on the command line ...
3fda8c4c
ML
1223 foreach my $filename ( @{$state->{args}} )
1224 {
7d90095a 1225 my $committedfile = $filename;
3fda8c4c
ML
1226 $filename = filecleanup($filename);
1227
1228 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1229
1230 my $meta = $updater->getmeta($filename);
392e2817 1231 $oldmeta{$filename} = $meta;
3fda8c4c
ML
1232
1233 my $wrev = revparse($filename);
1234
1235 my ( $filepart, $dirpart ) = filenamesplit($filename);
1236
1237 # do a checkout of the file if it part of this tree
1238 if ($wrev) {
1239 system('git-checkout-index', '-f', '-u', $filename);
1240 unless ($? == 0) {
1241 die "Error running git-checkout-index -f -u $filename : $!";
1242 }
1243 }
1244
1245 my $addflag = 0;
1246 my $rmflag = 0;
1247 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1248 $addflag = 1 unless ( -e $filename );
1249
1250 # Do up to date checking
1251 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1252 {
1253 # fail everything if an up to date check fails
1254 print "error 1 Up to date check failed for $filename\n";
3fda8c4c
ML
1255 chdir "/";
1256 exit;
1257 }
1258
7d90095a 1259 push @committedfiles, $committedfile;
3fda8c4c
ML
1260 $log->info("Committing $filename");
1261
1262 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1263
1264 unless ( $rmflag )
1265 {
1266 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1267 rename $state->{entries}{$filename}{modified_filename},$filename;
1268
1269 # Calculate modes to remove
1270 my $invmode = "";
1271 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1272
1273 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1274 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1275 }
1276
1277 if ( $rmflag )
1278 {
1279 $log->info("Removing file '$filename'");
1280 unlink($filename);
1281 system("git-update-index", "--remove", $filename);
1282 }
1283 elsif ( $addflag )
1284 {
1285 $log->info("Adding file '$filename'");
1286 system("git-update-index", "--add", $filename);
1287 } else {
1288 $log->info("Updating file '$filename'");
1289 system("git-update-index", $filename);
1290 }
1291 }
1292
1293 unless ( scalar(@committedfiles) > 0 )
1294 {
1295 print "E No files to commit\n";
1296 print "ok\n";
3fda8c4c
ML
1297 chdir "/";
1298 return;
1299 }
1300
1301 my $treehash = `git-write-tree`;
3fda8c4c 1302 chomp $treehash;
3fda8c4c
ML
1303
1304 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1305
1306 # write our commit message out if we have one ...
1307 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1308 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
1309 print $msg_fh "\n\nvia git-CVS emulator\n";
1310 close $msg_fh;
1311
1312 my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`;
1872adab 1313 chomp($commithash);
3fda8c4c
ML
1314 $log->info("Commit hash : $commithash");
1315
1316 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1317 {
1318 $log->warn("Commit failed (Invalid commit hash)");
1319 print "error 1 Commit failed (unknown reason)\n";
3fda8c4c
ML
1320 chdir "/";
1321 exit;
1322 }
1323
b2741f63
AP
1324 # Check that this is allowed, just as we would with a receive-pack
1325 my @cmd = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1326 $parenthash, $commithash );
1327 if( -x $cmd[0] ) {
1328 unless( system( @cmd ) == 0 )
1329 {
1330 $log->warn("Commit failed (update hook declined to update ref)");
1331 print "error 1 Commit failed (update hook declined)\n";
b2741f63
AP
1332 chdir "/";
1333 exit;
1334 }
1335 }
1336
ada5ef3b
JH
1337 if (system(qw(git update-ref -m), "cvsserver ci",
1338 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1339 $log->warn("update-ref for $state->{module} failed.");
1340 print "error 1 Cannot commit -- update first\n";
1341 exit;
1342 }
3fda8c4c
ML
1343
1344 $updater->update();
1345
addf88e4 1346 # foreach file specified on the command line ...
3fda8c4c
ML
1347 foreach my $filename ( @committedfiles )
1348 {
1349 $filename = filecleanup($filename);
1350
1351 my $meta = $updater->getmeta($filename);
3486595b
ML
1352 unless (defined $meta->{revision}) {
1353 $meta->{revision} = 1;
1354 }
3fda8c4c 1355
7d90095a 1356 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
1357
1358 $log->debug("Checked-in $dirpart : $filename");
1359
392e2817 1360 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
3486595b 1361 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
3fda8c4c 1362 {
392e2817 1363 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
3fda8c4c
ML
1364 print "Remove-entry $dirpart\n";
1365 print "$filename\n";
1366 } else {
459bad77
FL
1367 if ($meta->{revision} == 1) {
1368 print "M initial revision: 1.1\n";
1369 } else {
392e2817 1370 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
459bad77 1371 }
3fda8c4c
ML
1372 print "Checked-in $dirpart\n";
1373 print "$filename\n";
8538e876
AP
1374 my $kopts = kopts_from_path($filepart);
1375 print "/$filepart/1.$meta->{revision}//$kopts/\n";
3fda8c4c
ML
1376 }
1377 }
1378
3fda8c4c 1379 chdir "/";
3fda8c4c
ML
1380 print "ok\n";
1381}
1382
1383sub req_status
1384{
1385 my ( $cmd, $data ) = @_;
1386
1387 argsplit("status");
1388
1389 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1390 #$log->debug("status state : " . Dumper($state));
1391
1392 # Grab a handle to the SQLite db and do any necessary updates
1393 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1394 $updater->update();
1395
1396 # if no files were specified, we need to work out what files we should be providing status on ...
7d90095a 1397 argsfromdir($updater);
3fda8c4c 1398
addf88e4 1399 # foreach file specified on the command line ...
3fda8c4c
ML
1400 foreach my $filename ( @{$state->{args}} )
1401 {
1402 $filename = filecleanup($filename);
1403
1404 my $meta = $updater->getmeta($filename);
1405 my $oldmeta = $meta;
1406
1407 my $wrev = revparse($filename);
1408
1409 # If the working copy is an old revision, lets get that version too for comparison.
1410 if ( defined($wrev) and $wrev != $meta->{revision} )
1411 {
1412 $oldmeta = $updater->getmeta($filename, $wrev);
1413 }
1414
1415 # TODO : All possible statuses aren't yet implemented
1416 my $status;
1417 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1418 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1419 and
1420 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1421 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1422 );
1423
1424 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1425 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1426 and
1427 ( $state->{entries}{$filename}{unchanged}
1428 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1429 );
1430
1431 # Need checkout if it exists in the repo but doesn't have a working copy
1432 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1433
1434 # Locally modified if working copy and repo copy have the same revision but there are local changes
1435 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1436
1437 # Needs Merge if working copy revision is less than repo copy and there are local changes
1438 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1439
1440 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1441 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1442 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1443 $status ||= "File had conflicts on merge" if ( 0 );
1444
1445 $status ||= "Unknown";
1446
1447 print "M ===================================================================\n";
1448 print "M File: $filename\tStatus: $status\n";
1449 if ( defined($state->{entries}{$filename}{revision}) )
1450 {
1451 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1452 } else {
1453 print "M Working revision:\tNo entry for $filename\n";
1454 }
1455 if ( defined($meta->{revision}) )
1456 {
392e2817 1457 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
3fda8c4c
ML
1458 print "M Sticky Tag:\t\t(none)\n";
1459 print "M Sticky Date:\t\t(none)\n";
1460 print "M Sticky Options:\t\t(none)\n";
1461 } else {
1462 print "M Repository revision:\tNo revision control file\n";
1463 }
1464 print "M\n";
1465 }
1466
1467 print "ok\n";
1468}
1469
1470sub req_diff
1471{
1472 my ( $cmd, $data ) = @_;
1473
1474 argsplit("diff");
1475
1476 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1477 #$log->debug("status state : " . Dumper($state));
1478
1479 my ($revision1, $revision2);
1480 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1481 {
1482 $revision1 = $state->{opt}{r}[0];
1483 $revision2 = $state->{opt}{r}[1];
1484 } else {
1485 $revision1 = $state->{opt}{r};
1486 }
1487
1488 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1489 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1490
1491 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
1492
1493 # Grab a handle to the SQLite db and do any necessary updates
1494 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1495 $updater->update();
1496
1497 # if no files were specified, we need to work out what files we should be providing status on ...
7d90095a 1498 argsfromdir($updater);
3fda8c4c 1499
addf88e4 1500 # foreach file specified on the command line ...
3fda8c4c
ML
1501 foreach my $filename ( @{$state->{args}} )
1502 {
1503 $filename = filecleanup($filename);
1504
1505 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1506
1507 my $wrev = revparse($filename);
1508
1509 # We need _something_ to diff against
1510 next unless ( defined ( $wrev ) );
1511
1512 # if we have a -r switch, use it
1513 if ( defined ( $revision1 ) )
1514 {
1515 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1516 $meta1 = $updater->getmeta($filename, $revision1);
1517 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1518 {
1519 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1520 next;
1521 }
1522 transmitfile($meta1->{filehash}, $file1);
1523 }
1524 # otherwise we just use the working copy revision
1525 else
1526 {
1527 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1528 $meta1 = $updater->getmeta($filename, $wrev);
1529 transmitfile($meta1->{filehash}, $file1);
1530 }
1531
1532 # if we have a second -r switch, use it too
1533 if ( defined ( $revision2 ) )
1534 {
1535 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1536 $meta2 = $updater->getmeta($filename, $revision2);
1537
1538 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1539 {
1540 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1541 next;
1542 }
1543
1544 transmitfile($meta2->{filehash}, $file2);
1545 }
1546 # otherwise we just use the working copy
1547 else
1548 {
1549 $file2 = $state->{entries}{$filename}{modified_filename};
1550 }
1551
1552 # if we have been given -r, and we don't have a $file2 yet, lets get one
1553 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1554 {
1555 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1556 $meta2 = $updater->getmeta($filename, $wrev);
1557 transmitfile($meta2->{filehash}, $file2);
1558 }
1559
1560 # We need to have retrieved something useful
1561 next unless ( defined ( $meta1 ) );
1562
1563 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1564 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1565 and
1566 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1567 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1568 );
1569
1570 # Apparently we only show diffs for locally modified files
1571 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1572
1573 print "M Index: $filename\n";
1574 print "M ===================================================================\n";
1575 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1576 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1577 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1578 print "M diff ";
1579 foreach my $opt ( keys %{$state->{opt}} )
1580 {
1581 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1582 {
1583 foreach my $value ( @{$state->{opt}{$opt}} )
1584 {
1585 print "-$opt $value ";
1586 }
1587 } else {
1588 print "-$opt ";
1589 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1590 }
1591 }
1592 print "$filename\n";
1593
1594 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1595
1596 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1597
1598 if ( exists $state->{opt}{u} )
1599 {
1600 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1601 } else {
1602 system("diff $file1 $file2 > $filediff");
1603 }
1604
1605 while ( <$fh> )
1606 {
1607 print "M $_";
1608 }
1609 close $fh;
1610 }
1611
1612 print "ok\n";
1613}
1614
1615sub req_log
1616{
1617 my ( $cmd, $data ) = @_;
1618
1619 argsplit("log");
1620
1621 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1622 #$log->debug("log state : " . Dumper($state));
1623
1624 my ( $minrev, $maxrev );
1625 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1626 {
1627 my $control = $2;
1628 $minrev = $1;
1629 $maxrev = $3;
1630 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1631 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1632 $minrev++ if ( defined($minrev) and $control eq "::" );
1633 }
1634
1635 # Grab a handle to the SQLite db and do any necessary updates
1636 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1637 $updater->update();
1638
1639 # if no files were specified, we need to work out what files we should be providing status on ...
7d90095a 1640 argsfromdir($updater);
3fda8c4c 1641
addf88e4 1642 # foreach file specified on the command line ...
3fda8c4c
ML
1643 foreach my $filename ( @{$state->{args}} )
1644 {
1645 $filename = filecleanup($filename);
1646
1647 my $headmeta = $updater->getmeta($filename);
1648
1649 my $revisions = $updater->getlog($filename);
1650 my $totalrevisions = scalar(@$revisions);
1651
1652 if ( defined ( $minrev ) )
1653 {
1654 $log->debug("Removing revisions less than $minrev");
1655 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1656 {
1657 pop @$revisions;
1658 }
1659 }
1660 if ( defined ( $maxrev ) )
1661 {
1662 $log->debug("Removing revisions greater than $maxrev");
1663 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1664 {
1665 shift @$revisions;
1666 }
1667 }
1668
1669 next unless ( scalar(@$revisions) );
1670
1671 print "M \n";
1672 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1673 print "M Working file: $filename\n";
1674 print "M head: 1.$headmeta->{revision}\n";
1675 print "M branch:\n";
1676 print "M locks: strict\n";
1677 print "M access list:\n";
1678 print "M symbolic names:\n";
1679 print "M keyword substitution: kv\n";
1680 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1681 print "M description:\n";
1682
1683 foreach my $revision ( @$revisions )
1684 {
1685 print "M ----------------------------\n";
1686 print "M revision 1.$revision->{revision}\n";
1687 # reformat the date for log output
1688 $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}) );
1689 $revision->{author} =~ s/\s+.*//;
1690 $revision->{author} =~ s/^(.{8}).*/$1/;
1691 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1692 my $commitmessage = $updater->commitmessage($revision->{commithash});
1693 $commitmessage =~ s/^/M /mg;
1694 print $commitmessage . "\n";
1695 }
1696 print "M =============================================================================\n";
1697 }
1698
1699 print "ok\n";
1700}
1701
1702sub req_annotate
1703{
1704 my ( $cmd, $data ) = @_;
1705
1706 argsplit("annotate");
1707
1708 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1709 #$log->debug("status state : " . Dumper($state));
1710
1711 # Grab a handle to the SQLite db and do any necessary updates
1712 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1713 $updater->update();
1714
1715 # if no files were specified, we need to work out what files we should be providing annotate on ...
7d90095a 1716 argsfromdir($updater);
3fda8c4c
ML
1717
1718 # we'll need a temporary checkout dir
1719 my $tmpdir = tempdir ( DIR => $TEMP_DIR );
1720 my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
1721 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");
1722
1723 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
1724 $ENV{GIT_INDEX_FILE} = $file_index;
1725
1726 chdir $tmpdir;
1727
addf88e4 1728 # foreach file specified on the command line ...
3fda8c4c
ML
1729 foreach my $filename ( @{$state->{args}} )
1730 {
1731 $filename = filecleanup($filename);
1732
1733 my $meta = $updater->getmeta($filename);
1734
1735 next unless ( $meta->{revision} );
1736
1737 # get all the commits that this file was in
1738 # in dense format -- aka skip dead revisions
1739 my $revisions = $updater->gethistorydense($filename);
1740 my $lastseenin = $revisions->[0][2];
1741
1742 # populate the temporary index based on the latest commit were we saw
1743 # the file -- but do it cheaply without checking out any files
1744 # TODO: if we got a revision from the client, use that instead
1745 # to look up the commithash in sqlite (still good to default to
1746 # the current head as we do now)
1747 system("git-read-tree", $lastseenin);
1748 unless ($? == 0)
1749 {
a5e40798
JM
1750 print "E error running git-read-tree $lastseenin $file_index $!\n";
1751 return;
3fda8c4c
ML
1752 }
1753 $log->info("Created index '$file_index' with commit $lastseenin - exit status $?");
1754
1755 # do a checkout of the file
1756 system('git-checkout-index', '-f', '-u', $filename);
1757 unless ($? == 0) {
a5e40798
JM
1758 print "E error running git-checkout-index -f -u $filename : $!\n";
1759 return;
3fda8c4c
ML
1760 }
1761
1762 $log->info("Annotate $filename");
1763
1764 # Prepare a file with the commits from the linearized
1765 # history that annotate should know about. This prevents
1766 # git-jsannotate telling us about commits we are hiding
1767 # from the client.
1768
a5e40798
JM
1769 my $a_hints = "$tmpdir/.annotate_hints";
1770 if (!open(ANNOTATEHINTS, '>', $a_hints)) {
1771 print "E failed to open '$a_hints' for writing: $!\n";
1772 return;
1773 }
3fda8c4c
ML
1774 for (my $i=0; $i < @$revisions; $i++)
1775 {
1776 print ANNOTATEHINTS $revisions->[$i][2];
1777 if ($i+1 < @$revisions) { # have we got a parent?
1778 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1779 }
1780 print ANNOTATEHINTS "\n";
1781 }
1782
1783 print ANNOTATEHINTS "\n";
a5e40798
JM
1784 close ANNOTATEHINTS
1785 or (print "E failed to write $a_hints: $!\n"), return;
3fda8c4c 1786
a5e40798
JM
1787 my @cmd = (qw(git-annotate -l -S), $a_hints, $filename);
1788 if (!open(ANNOTATE, "-|", @cmd)) {
1789 print "E error invoking ". join(' ',@cmd) .": $!\n";
1790 return;
1791 }
3fda8c4c
ML
1792 my $metadata = {};
1793 print "E Annotations for $filename\n";
1794 print "E ***************\n";
1795 while ( <ANNOTATE> )
1796 {
1797 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1798 {
1799 my $commithash = $1;
1800 my $data = $2;
1801 unless ( defined ( $metadata->{$commithash} ) )
1802 {
1803 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
1804 $metadata->{$commithash}{author} =~ s/\s+.*//;
1805 $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/;
1806 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1807 }
1808 printf("M 1.%-5d (%-8s %10s): %s\n",
1809 $metadata->{$commithash}{revision},
1810 $metadata->{$commithash}{author},
1811 $metadata->{$commithash}{modified},
1812 $data
1813 );
1814 } else {
1815 $log->warn("Error in annotate output! LINE: $_");
1816 print "E Annotate error \n";
1817 next;
1818 }
1819 }
1820 close ANNOTATE;
1821 }
1822
1823 # done; get out of the tempdir
1824 chdir "/";
1825
1826 print "ok\n";
1827
1828}
1829
1830# This method takes the state->{arguments} array and produces two new arrays.
1831# The first is $state->{args} which is everything before the '--' argument, and
1832# the second is $state->{files} which is everything after it.
1833sub argsplit
1834{
3fda8c4c
ML
1835 $state->{args} = [];
1836 $state->{files} = [];
1837 $state->{opt} = {};
1838
1e76b702
FL
1839 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1840
1841 my $type = shift;
1842
3fda8c4c
ML
1843 if ( defined($type) )
1844 {
1845 my $opt = {};
1846 $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" );
1847 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1848 $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" );
1849 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1850 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1851 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1852 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1853 $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" );
1854
1855
1856 while ( scalar ( @{$state->{arguments}} ) > 0 )
1857 {
1858 my $arg = shift @{$state->{arguments}};
1859
1860 next if ( $arg eq "--" );
1861 next unless ( $arg =~ /\S/ );
1862
1863 # if the argument looks like a switch
1864 if ( $arg =~ /^-(\w)(.*)/ )
1865 {
1866 # if it's a switch that takes an argument
1867 if ( $opt->{$1} )
1868 {
1869 # If this switch has already been provided
1870 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
1871 {
1872 $state->{opt}{$1} = [ $state->{opt}{$1} ];
1873 if ( length($2) > 0 )
1874 {
1875 push @{$state->{opt}{$1}},$2;
1876 } else {
1877 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
1878 }
1879 } else {
1880 # if there's extra data in the arg, use that as the argument for the switch
1881 if ( length($2) > 0 )
1882 {
1883 $state->{opt}{$1} = $2;
1884 } else {
1885 $state->{opt}{$1} = shift @{$state->{arguments}};
1886 }
1887 }
1888 } else {
1889 $state->{opt}{$1} = undef;
1890 }
1891 }
1892 else
1893 {
1894 push @{$state->{args}}, $arg;
1895 }
1896 }
1897 }
1898 else
1899 {
1900 my $mode = 0;
1901
1902 foreach my $value ( @{$state->{arguments}} )
1903 {
1904 if ( $value eq "--" )
1905 {
1906 $mode++;
1907 next;
1908 }
1909 push @{$state->{args}}, $value if ( $mode == 0 );
1910 push @{$state->{files}}, $value if ( $mode == 1 );
1911 }
1912 }
1913}
1914
1915# This method uses $state->{directory} to populate $state->{args} with a list of filenames
1916sub argsfromdir
1917{
1918 my $updater = shift;
1919
7d90095a
MS
1920 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
1921
82000d74 1922 return if ( scalar ( @{$state->{args}} ) > 1 );
7d90095a 1923
0a7a9a12
JS
1924 my @gethead = @{$updater->gethead};
1925
1926 # push added files
1927 foreach my $file (keys %{$state->{entries}}) {
1928 if ( exists $state->{entries}{$file}{revision} &&
1929 $state->{entries}{$file}{revision} == 0 )
1930 {
1931 push @gethead, { name => $file, filehash => 'added' };
1932 }
1933 }
1934
82000d74
MS
1935 if ( scalar(@{$state->{args}}) == 1 )
1936 {
1937 my $arg = $state->{args}[0];
1938 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
7d90095a 1939
82000d74 1940 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
3fda8c4c 1941
0a7a9a12 1942 foreach my $file ( @gethead )
82000d74
MS
1943 {
1944 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1945 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
1946 push @{$state->{args}}, $file->{name};
1947 }
1948
1949 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
1950 } else {
1951 $log->info("Only one arg specified, populating file list automatically");
1952
1953 $state->{args} = [];
1954
0a7a9a12 1955 foreach my $file ( @gethead )
82000d74
MS
1956 {
1957 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
1958 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
1959 push @{$state->{args}}, $file->{name};
1960 }
3fda8c4c
ML
1961 }
1962}
1963
1964# This method cleans up the $state variable after a command that uses arguments has run
1965sub statecleanup
1966{
1967 $state->{files} = [];
1968 $state->{args} = [];
1969 $state->{arguments} = [];
1970 $state->{entries} = {};
1971}
1972
1973sub revparse
1974{
1975 my $filename = shift;
1976
1977 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
1978
1979 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
1980 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
1981
1982 return undef;
1983}
1984
1985# This method takes a file hash and does a CVS "file transfer" which transmits the
1986# size of the file, and then the file contents.
1987# If a second argument $targetfile is given, the file is instead written out to
1988# a file by the name of $targetfile
1989sub transmitfile
1990{
1991 my $filehash = shift;
1992 my $targetfile = shift;
1993
1994 if ( defined ( $filehash ) and $filehash eq "deleted" )
1995 {
1996 $log->warn("filehash is 'deleted'");
1997 return;
1998 }
1999
2000 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2001
2002 my $type = `git-cat-file -t $filehash`;
2003 chomp $type;
2004
2005 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2006
2007 my $size = `git-cat-file -s $filehash`;
2008 chomp $size;
2009
2010 $log->debug("transmitfile($filehash) size=$size, type=$type");
2011
2012 if ( open my $fh, '-|', "git-cat-file", "blob", $filehash )
2013 {
2014 if ( defined ( $targetfile ) )
2015 {
2016 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2017 print NEWFILE $_ while ( <$fh> );
a5e40798 2018 close NEWFILE or die("Failed to write '$targetfile': $!");
3fda8c4c
ML
2019 } else {
2020 print "$size\n";
2021 print while ( <$fh> );
2022 }
a5e40798 2023 close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
3fda8c4c
ML
2024 } else {
2025 die("Couldn't execute git-cat-file");
2026 }
2027}
2028
2029# This method takes a file name, and returns ( $dirpart, $filepart ) which
5348b6e7 2030# refers to the directory portion and the file portion of the filename
3fda8c4c
ML
2031# respectively
2032sub filenamesplit
2033{
2034 my $filename = shift;
7d90095a 2035 my $fixforlocaldir = shift;
3fda8c4c
ML
2036
2037 my ( $filepart, $dirpart ) = ( $filename, "." );
2038 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2039 $dirpart .= "/";
2040
7d90095a
MS
2041 if ( $fixforlocaldir )
2042 {
2043 $dirpart =~ s/^$state->{prependdir}//;
2044 }
2045
3fda8c4c
ML
2046 return ( $filepart, $dirpart );
2047}
2048
2049sub filecleanup
2050{
2051 my $filename = shift;
2052
2053 return undef unless(defined($filename));
2054 if ( $filename =~ /^\// )
2055 {
2056 print "E absolute filenames '$filename' not supported by server\n";
2057 return undef;
2058 }
2059
2060 $filename =~ s/^\.\///g;
82000d74 2061 $filename = $state->{prependdir} . $filename;
3fda8c4c
ML
2062 return $filename;
2063}
2064
8538e876
AP
2065# Given a path, this function returns a string containing the kopts
2066# that should go into that path's Entries line. For example, a binary
2067# file should get -kb.
2068sub kopts_from_path
2069{
2070 my ($path) = @_;
2071
2072 # Once it exists, the git attributes system should be used to look up
2073 # what attributes apply to this path.
2074
2075 # Until then, take the setting from the config file
2076 unless ( defined ( $cfg->{gitcvs}{allbinary} ) and $cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i )
2077 {
2078 # Return "" to give no special treatment to any path
2079 return "";
2080 } else {
2081 # Alternatively, to have all files treated as if they are binary (which
2082 # is more like git itself), always return the "-kb" option
2083 return "-kb";
2084 }
2085}
2086
3fda8c4c
ML
2087package GITCVS::log;
2088
2089####
2090#### Copyright The Open University UK - 2006.
2091####
2092#### Authors: Martyn Smith <martyn@catalyst.net.nz>
2093#### Martin Langhoff <martin@catalyst.net.nz>
2094####
2095####
2096
2097use strict;
2098use warnings;
2099
2100=head1 NAME
2101
2102GITCVS::log
2103
2104=head1 DESCRIPTION
2105
2106This module provides very crude logging with a similar interface to
2107Log::Log4perl
2108
2109=head1 METHODS
2110
2111=cut
2112
2113=head2 new
2114
2115Creates a new log object, optionally you can specify a filename here to
5348b6e7 2116indicate the file to log to. If no log file is specified, you can specify one
3fda8c4c
ML
2117later with method setfile, or indicate you no longer want logging with method
2118nofile.
2119
2120Until one of these methods is called, all log calls will buffer messages ready
2121to write out.
2122
2123=cut
2124sub new
2125{
2126 my $class = shift;
2127 my $filename = shift;
2128
2129 my $self = {};
2130
2131 bless $self, $class;
2132
2133 if ( defined ( $filename ) )
2134 {
2135 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2136 }
2137
2138 return $self;
2139}
2140
2141=head2 setfile
2142
2143This methods takes a filename, and attempts to open that file as the log file.
2144If successful, all buffered data is written out to the file, and any further
2145logging is written directly to the file.
2146
2147=cut
2148sub setfile
2149{
2150 my $self = shift;
2151 my $filename = shift;
2152
2153 if ( defined ( $filename ) )
2154 {
2155 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2156 }
2157
2158 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2159
2160 while ( my $line = shift @{$self->{buffer}} )
2161 {
2162 print {$self->{fh}} $line;
2163 }
2164}
2165
2166=head2 nofile
2167
2168This method indicates no logging is going to be used. It flushes any entries in
2169the internal buffer, and sets a flag to ensure no further data is put there.
2170
2171=cut
2172sub nofile
2173{
2174 my $self = shift;
2175
2176 $self->{nolog} = 1;
2177
2178 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2179
2180 $self->{buffer} = [];
2181}
2182
2183=head2 _logopen
2184
2185Internal method. Returns true if the log file is open, false otherwise.
2186
2187=cut
2188sub _logopen
2189{
2190 my $self = shift;
2191
2192 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2193 return 0;
2194}
2195
2196=head2 debug info warn fatal
2197
2198These four methods are wrappers to _log. They provide the actual interface for
2199logging data.
2200
2201=cut
2202sub debug { my $self = shift; $self->_log("debug", @_); }
2203sub info { my $self = shift; $self->_log("info" , @_); }
2204sub warn { my $self = shift; $self->_log("warn" , @_); }
2205sub fatal { my $self = shift; $self->_log("fatal", @_); }
2206
2207=head2 _log
2208
2209This is an internal method called by the logging functions. It generates a
2210timestamp and pushes the logged line either to file, or internal buffer.
2211
2212=cut
2213sub _log
2214{
2215 my $self = shift;
2216 my $level = shift;
2217
2218 return if ( $self->{nolog} );
2219
2220 my @time = localtime;
2221 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2222 $time[5] + 1900,
2223 $time[4] + 1,
2224 $time[3],
2225 $time[2],
2226 $time[1],
2227 $time[0],
2228 uc $level,
2229 );
2230
2231 if ( $self->_logopen )
2232 {
2233 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2234 } else {
2235 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2236 }
2237}
2238
2239=head2 DESTROY
2240
2241This method simply closes the file handle if one is open
2242
2243=cut
2244sub DESTROY
2245{
2246 my $self = shift;
2247
2248 if ( $self->_logopen )
2249 {
2250 close $self->{fh};
2251 }
2252}
2253
2254package GITCVS::updater;
2255
2256####
2257#### Copyright The Open University UK - 2006.
2258####
2259#### Authors: Martyn Smith <martyn@catalyst.net.nz>
2260#### Martin Langhoff <martin@catalyst.net.nz>
2261####
2262####
2263
2264use strict;
2265use warnings;
2266use DBI;
2267
2268=head1 METHODS
2269
2270=cut
2271
2272=head2 new
2273
2274=cut
2275sub new
2276{
2277 my $class = shift;
2278 my $config = shift;
2279 my $module = shift;
2280 my $log = shift;
2281
2282 die "Need to specify a git repository" unless ( defined($config) and -d $config );
2283 die "Need to specify a module" unless ( defined($module) );
2284
2285 $class = ref($class) || $class;
2286
2287 my $self = {};
2288
2289 bless $self, $class;
2290
3fda8c4c 2291 $self->{module} = $module;
3fda8c4c
ML
2292 $self->{git_path} = $config . "/";
2293
2294 $self->{log} = $log;
2295
2296 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2297
eb1780d4 2298 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
473937ed 2299 $cfg->{gitcvs}{dbdriver} || "SQLite";
eb1780d4
FL
2300 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2301 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2302 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2303 $cfg->{gitcvs}{dbuser} || "";
2304 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2305 $cfg->{gitcvs}{dbpass} || "";
2306 my %mapping = ( m => $module,
2307 a => $state->{method},
2308 u => getlogin || getpwuid($<) || $<,
2309 G => $self->{git_path},
2310 g => mangle_dirname($self->{git_path}),
2311 );
2312 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2313 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
2314
473937ed
FL
2315 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2316 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2317 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
eb1780d4
FL
2318 $self->{dbuser},
2319 $self->{dbpass});
920a449a 2320 die "Error connecting to database\n" unless defined $self->{dbh};
3fda8c4c
ML
2321
2322 $self->{tables} = {};
0cf611a3 2323 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
3fda8c4c 2324 {
3fda8c4c
ML
2325 $self->{tables}{$table} = 1;
2326 }
2327
2328 # Construct the revision table if required
2329 unless ( $self->{tables}{revision} )
2330 {
2331 $self->{dbh}->do("
2332 CREATE TABLE revision (
2333 name TEXT NOT NULL,
2334 revision INTEGER NOT NULL,
2335 filehash TEXT NOT NULL,
2336 commithash TEXT NOT NULL,
2337 author TEXT NOT NULL,
2338 modified TEXT NOT NULL,
2339 mode TEXT NOT NULL
2340 )
2341 ");
178e015c
SP
2342 $self->{dbh}->do("
2343 CREATE INDEX revision_ix1
2344 ON revision (name,revision)
2345 ");
2346 $self->{dbh}->do("
2347 CREATE INDEX revision_ix2
2348 ON revision (name,commithash)
2349 ");
3fda8c4c
ML
2350 }
2351
178e015c 2352 # Construct the head table if required
3fda8c4c
ML
2353 unless ( $self->{tables}{head} )
2354 {
2355 $self->{dbh}->do("
2356 CREATE TABLE head (
2357 name TEXT NOT NULL,
2358 revision INTEGER NOT NULL,
2359 filehash TEXT NOT NULL,
2360 commithash TEXT NOT NULL,
2361 author TEXT NOT NULL,
2362 modified TEXT NOT NULL,
2363 mode TEXT NOT NULL
2364 )
2365 ");
178e015c
SP
2366 $self->{dbh}->do("
2367 CREATE INDEX head_ix1
2368 ON head (name)
2369 ");
3fda8c4c
ML
2370 }
2371
2372 # Construct the properties table if required
2373 unless ( $self->{tables}{properties} )
2374 {
2375 $self->{dbh}->do("
2376 CREATE TABLE properties (
2377 key TEXT NOT NULL PRIMARY KEY,
2378 value TEXT
2379 )
2380 ");
2381 }
2382
2383 # Construct the commitmsgs table if required
2384 unless ( $self->{tables}{commitmsgs} )
2385 {
2386 $self->{dbh}->do("
2387 CREATE TABLE commitmsgs (
2388 key TEXT NOT NULL PRIMARY KEY,
2389 value TEXT
2390 )
2391 ");
2392 }
2393
2394 return $self;
2395}
2396
2397=head2 update
2398
2399=cut
2400sub update
2401{
2402 my $self = shift;
2403
2404 # first lets get the commit list
2405 $ENV{GIT_DIR} = $self->{git_path};
2406
49fb940e
ML
2407 my $commitsha1 = `git rev-parse $self->{module}`;
2408 chomp $commitsha1;
2409
2410 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
3fda8c4c
ML
2411 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
2412 {
2413 die("Invalid module '$self->{module}'");
2414 }
2415
2416
2417 my $git_log;
2418 my $lastcommit = $self->_get_prop("last_commit");
2419
49fb940e
ML
2420 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
2421 return 1;
2422 }
2423
3fda8c4c
ML
2424 # Start exclusive lock here...
2425 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
2426
2427 # TODO: log processing is memory bound
2428 # if we can parse into a 2nd file that is in reverse order
2429 # we can probably do something really efficient
a248c961 2430 my @git_log_params = ('--pretty', '--parents', '--topo-order');
3fda8c4c
ML
2431
2432 if (defined $lastcommit) {
2433 push @git_log_params, "$lastcommit..$self->{module}";
2434 } else {
2435 push @git_log_params, $self->{module};
2436 }
a248c961
ML
2437 # git-rev-list is the backend / plumbing version of git-log
2438 open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
3fda8c4c
ML
2439
2440 my @commits;
2441
2442 my %commit = ();
2443
2444 while ( <GITLOG> )
2445 {
2446 chomp;
2447 if (m/^commit\s+(.*)$/) {
2448 # on ^commit lines put the just seen commit in the stack
2449 # and prime things for the next one
2450 if (keys %commit) {
2451 my %copy = %commit;
2452 unshift @commits, \%copy;
2453 %commit = ();
2454 }
2455 my @parents = split(m/\s+/, $1);
2456 $commit{hash} = shift @parents;
2457 $commit{parents} = \@parents;
2458 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
2459 # on rfc822-like lines seen before we see any message,
2460 # lowercase the entry and put it in the hash as key-value
2461 $commit{lc($1)} = $2;
2462 } else {
2463 # message lines - skip initial empty line
2464 # and trim whitespace
2465 if (!exists($commit{message}) && m/^\s*$/) {
2466 # define it to mark the end of headers
2467 $commit{message} = '';
2468 next;
2469 }
2470 s/^\s+//; s/\s+$//; # trim ws
2471 $commit{message} .= $_ . "\n";
2472 }
2473 }
2474 close GITLOG;
2475
2476 unshift @commits, \%commit if ( keys %commit );
2477
2478 # Now all the commits are in the @commits bucket
2479 # ordered by time DESC. for each commit that needs processing,
2480 # determine whether it's following the last head we've seen or if
2481 # it's on its own branch, grab a file list, and add whatever's changed
2482 # NOTE: $lastcommit refers to the last commit from previous run
2483 # $lastpicked is the last commit we picked in this run
2484 my $lastpicked;
2485 my $head = {};
2486 if (defined $lastcommit) {
2487 $lastpicked = $lastcommit;
2488 }
2489
2490 my $committotal = scalar(@commits);
2491 my $commitcount = 0;
2492
2493 # Load the head table into $head (for cached lookups during the update process)
2494 foreach my $file ( @{$self->gethead()} )
2495 {
2496 $head->{$file->{name}} = $file;
2497 }
2498
2499 foreach my $commit ( @commits )
2500 {
2501 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
2502 if (defined $lastpicked)
2503 {
2504 if (!in_array($lastpicked, @{$commit->{parents}}))
2505 {
2506 # skip, we'll see this delta
2507 # as part of a merge later
2508 # warn "skipping off-track $commit->{hash}\n";
2509 next;
2510 } elsif (@{$commit->{parents}} > 1) {
2511 # it is a merge commit, for each parent that is
2512 # not $lastpicked, see if we can get a log
2513 # from the merge-base to that parent to put it
2514 # in the message as a merge summary.
2515 my @parents = @{$commit->{parents}};
2516 foreach my $parent (@parents) {
2517 # git-merge-base can potentially (but rarely) throw
2518 # several candidate merge bases. let's assume
2519 # that the first one is the best one.
2520 if ($parent eq $lastpicked) {
2521 next;
2522 }
a5e40798
JM
2523 my $base = safe_pipe_capture('git-merge-base',
2524 $lastpicked, $parent);
3fda8c4c
ML
2525 chomp $base;
2526 if ($base) {
2527 my @merged;
2528 # print "want to log between $base $parent \n";
2529 open(GITLOG, '-|', 'git-log', "$base..$parent")
a5e40798 2530 or die "Cannot call git-log: $!";
3fda8c4c
ML
2531 my $mergedhash;
2532 while (<GITLOG>) {
2533 chomp;
2534 if (!defined $mergedhash) {
2535 if (m/^commit\s+(.+)$/) {
2536 $mergedhash = $1;
2537 } else {
2538 next;
2539 }
2540 } else {
2541 # grab the first line that looks non-rfc822
2542 # aka has content after leading space
2543 if (m/^\s+(\S.*)$/) {
2544 my $title = $1;
2545 $title = substr($title,0,100); # truncate
2546 unshift @merged, "$mergedhash $title";
2547 undef $mergedhash;
2548 }
2549 }
2550 }
2551 close GITLOG;
2552 if (@merged) {
2553 $commit->{mergemsg} = $commit->{message};
2554 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
2555 foreach my $summary (@merged) {
2556 $commit->{mergemsg} .= "\t$summary\n";
2557 }
2558 $commit->{mergemsg} .= "\n\n";
2559 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
2560 }
2561 }
2562 }
2563 }
2564 }
2565
2566 # convert the date to CVS-happy format
2567 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
2568
2569 if ( defined ( $lastpicked ) )
2570 {
e02cd638
JH
2571 my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
2572 local ($/) = "\0";
3fda8c4c
ML
2573 while ( <FILELIST> )
2574 {
e02cd638
JH
2575 chomp;
2576 unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
3fda8c4c
ML
2577 {
2578 die("Couldn't process git-diff-tree line : $_");
2579 }
e02cd638
JH
2580 my ($mode, $hash, $change) = ($1, $2, $3);
2581 my $name = <FILELIST>;
2582 chomp($name);
3fda8c4c 2583
e02cd638 2584 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3fda8c4c
ML
2585
2586 my $git_perms = "";
e02cd638
JH
2587 $git_perms .= "r" if ( $mode & 4 );
2588 $git_perms .= "w" if ( $mode & 2 );
2589 $git_perms .= "x" if ( $mode & 1 );
3fda8c4c
ML
2590 $git_perms = "rw" if ( $git_perms eq "" );
2591
e02cd638 2592 if ( $change eq "D" )
3fda8c4c 2593 {
e02cd638
JH
2594 #$log->debug("DELETE $name");
2595 $head->{$name} = {
2596 name => $name,
2597 revision => $head->{$name}{revision} + 1,
3fda8c4c
ML
2598 filehash => "deleted",
2599 commithash => $commit->{hash},
2600 modified => $commit->{date},
2601 author => $commit->{author},
2602 mode => $git_perms,
2603 };
e02cd638 2604 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c 2605 }
e02cd638 2606 elsif ( $change eq "M" )
3fda8c4c 2607 {
e02cd638
JH
2608 #$log->debug("MODIFIED $name");
2609 $head->{$name} = {
2610 name => $name,
2611 revision => $head->{$name}{revision} + 1,
2612 filehash => $hash,
3fda8c4c
ML
2613 commithash => $commit->{hash},
2614 modified => $commit->{date},
2615 author => $commit->{author},
2616 mode => $git_perms,
2617 };
e02cd638 2618 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c 2619 }
e02cd638 2620 elsif ( $change eq "A" )
3fda8c4c 2621 {
e02cd638
JH
2622 #$log->debug("ADDED $name");
2623 $head->{$name} = {
2624 name => $name,
a7da9adb 2625 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
e02cd638 2626 filehash => $hash,
3fda8c4c
ML
2627 commithash => $commit->{hash},
2628 modified => $commit->{date},
2629 author => $commit->{author},
2630 mode => $git_perms,
2631 };
e02cd638 2632 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c
ML
2633 }
2634 else
2635 {
e02cd638 2636 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3fda8c4c
ML
2637 die;
2638 }
2639 }
2640 close FILELIST;
2641 } else {
2642 # this is used to detect files removed from the repo
2643 my $seen_files = {};
2644
e02cd638
JH
2645 my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
2646 local $/ = "\0";
3fda8c4c
ML
2647 while ( <FILELIST> )
2648 {
e02cd638
JH
2649 chomp;
2650 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3fda8c4c
ML
2651 {
2652 die("Couldn't process git-ls-tree line : $_");
2653 }
2654
2655 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
2656
2657 $seen_files->{$git_filename} = 1;
2658
2659 my ( $oldhash, $oldrevision, $oldmode ) = (
2660 $head->{$git_filename}{filehash},
2661 $head->{$git_filename}{revision},
2662 $head->{$git_filename}{mode}
2663 );
2664
2665 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
2666 {
2667 $git_perms = "";
2668 $git_perms .= "r" if ( $1 & 4 );
2669 $git_perms .= "w" if ( $1 & 2 );
2670 $git_perms .= "x" if ( $1 & 1 );
2671 } else {
2672 $git_perms = "rw";
2673 }
2674
2675 # unless the file exists with the same hash, we need to update it ...
2676 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
2677 {
2678 my $newrevision = ( $oldrevision or 0 ) + 1;
2679
2680 $head->{$git_filename} = {
2681 name => $git_filename,
2682 revision => $newrevision,
2683 filehash => $git_hash,
2684 commithash => $commit->{hash},
2685 modified => $commit->{date},
2686 author => $commit->{author},
2687 mode => $git_perms,
2688 };
2689
2690
96256bba 2691 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c
ML
2692 }
2693 }
2694 close FILELIST;
2695
2696 # Detect deleted files
2697 foreach my $file ( keys %$head )
2698 {
2699 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
2700 {
2701 $head->{$file}{revision}++;
2702 $head->{$file}{filehash} = "deleted";
2703 $head->{$file}{commithash} = $commit->{hash};
2704 $head->{$file}{modified} = $commit->{date};
2705 $head->{$file}{author} = $commit->{author};
2706
96256bba 2707 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
3fda8c4c
ML
2708 }
2709 }
2710 # END : "Detect deleted files"
2711 }
2712
2713
2714 if (exists $commit->{mergemsg})
2715 {
96256bba 2716 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3fda8c4c
ML
2717 }
2718
2719 $lastpicked = $commit->{hash};
2720
2721 $self->_set_prop("last_commit", $commit->{hash});
2722 }
2723
96256bba 2724 $self->delete_head();
3fda8c4c
ML
2725 foreach my $file ( keys %$head )
2726 {
96256bba 2727 $self->insert_head(
3fda8c4c
ML
2728 $file,
2729 $head->{$file}{revision},
2730 $head->{$file}{filehash},
2731 $head->{$file}{commithash},
2732 $head->{$file}{modified},
2733 $head->{$file}{author},
2734 $head->{$file}{mode},
2735 );
2736 }
2737 # invalidate the gethead cache
2738 $self->{gethead_cache} = undef;
2739
2740
2741 # Ending exclusive lock here
2742 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
2743}
2744
96256bba
JS
2745sub insert_rev
2746{
2747 my $self = shift;
2748 my $name = shift;
2749 my $revision = shift;
2750 my $filehash = shift;
2751 my $commithash = shift;
2752 my $modified = shift;
2753 my $author = shift;
2754 my $mode = shift;
2755
2756 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2757 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2758}
2759
2760sub insert_mergelog
2761{
2762 my $self = shift;
2763 my $key = shift;
2764 my $value = shift;
2765
2766 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);
2767 $insert_mergelog->execute($key, $value);
2768}
2769
2770sub delete_head
2771{
2772 my $self = shift;
2773
2774 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1);
2775 $delete_head->execute();
2776}
2777
2778sub insert_head
2779{
2780 my $self = shift;
2781 my $name = shift;
2782 my $revision = shift;
2783 my $filehash = shift;
2784 my $commithash = shift;
2785 my $modified = shift;
2786 my $author = shift;
2787 my $mode = shift;
2788
2789 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
2790 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
2791}
2792
3fda8c4c
ML
2793sub _headrev
2794{
2795 my $self = shift;
2796 my $filename = shift;
2797
2798 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);
2799 $db_query->execute($filename);
2800 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
2801
2802 return ( $hash, $revision, $mode );
2803}
2804
2805sub _get_prop
2806{
2807 my $self = shift;
2808 my $key = shift;
2809
2810 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);
2811 $db_query->execute($key);
2812 my ( $value ) = $db_query->fetchrow_array;
2813
2814 return $value;
2815}
2816
2817sub _set_prop
2818{
2819 my $self = shift;
2820 my $key = shift;
2821 my $value = shift;
2822
2823 my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);
2824 $db_query->execute($value, $key);
2825
2826 unless ( $db_query->rows )
2827 {
2828 $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);
2829 $db_query->execute($key, $value);
2830 }
2831
2832 return $value;
2833}
2834
2835=head2 gethead
2836
2837=cut
2838
2839sub gethead
2840{
2841 my $self = shift;
2842
2843 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
2844
501c7372 2845 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);
3fda8c4c
ML
2846 $db_query->execute();
2847
2848 my $tree = [];
2849 while ( my $file = $db_query->fetchrow_hashref )
2850 {
2851 push @$tree, $file;
2852 }
2853
2854 $self->{gethead_cache} = $tree;
2855
2856 return $tree;
2857}
2858
2859=head2 getlog
2860
2861=cut
2862
2863sub getlog
2864{
2865 my $self = shift;
2866 my $filename = shift;
2867
2868 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2869 $db_query->execute($filename);
2870
2871 my $tree = [];
2872 while ( my $file = $db_query->fetchrow_hashref )
2873 {
2874 push @$tree, $file;
2875 }
2876
2877 return $tree;
2878}
2879
2880=head2 getmeta
2881
2882This function takes a filename (with path) argument and returns a hashref of
2883metadata for that file.
2884
2885=cut
2886
2887sub getmeta
2888{
2889 my $self = shift;
2890 my $filename = shift;
2891 my $revision = shift;
2892
2893 my $db_query;
2894 if ( defined($revision) and $revision =~ /^\d+$/ )
2895 {
2896 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);
2897 $db_query->execute($filename, $revision);
2898 }
2899 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
2900 {
2901 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);
2902 $db_query->execute($filename, $revision);
2903 } else {
2904 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);
2905 $db_query->execute($filename);
2906 }
2907
2908 return $db_query->fetchrow_hashref;
2909}
2910
2911=head2 commitmessage
2912
2913this function takes a commithash and returns the commit message for that commit
2914
2915=cut
2916sub commitmessage
2917{
2918 my $self = shift;
2919 my $commithash = shift;
2920
2921 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
2922
2923 my $db_query;
2924 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);
2925 $db_query->execute($commithash);
2926
2927 my ( $message ) = $db_query->fetchrow_array;
2928
2929 if ( defined ( $message ) )
2930 {
2931 $message .= " " if ( $message =~ /\n$/ );
2932 return $message;
2933 }
2934
2935 my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash);
2936 shift @lines while ( $lines[0] =~ /\S/ );
2937 $message = join("",@lines);
2938 $message .= " " if ( $message =~ /\n$/ );
2939 return $message;
2940}
2941
2942=head2 gethistory
2943
2944This function takes a filename (with path) argument and returns an arrayofarrays
2945containing revision,filehash,commithash ordered by revision descending
2946
2947=cut
2948sub gethistory
2949{
2950 my $self = shift;
2951 my $filename = shift;
2952
2953 my $db_query;
2954 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);
2955 $db_query->execute($filename);
2956
2957 return $db_query->fetchall_arrayref;
2958}
2959
2960=head2 gethistorydense
2961
2962This function takes a filename (with path) argument and returns an arrayofarrays
2963containing revision,filehash,commithash ordered by revision descending.
2964
2965This version of gethistory skips deleted entries -- so it is useful for annotate.
2966The 'dense' part is a reference to a '--dense' option available for git-rev-list
2967and other git tools that depend on it.
2968
2969=cut
2970sub gethistorydense
2971{
2972 my $self = shift;
2973 my $filename = shift;
2974
2975 my $db_query;
2976 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
2977 $db_query->execute($filename);
2978
2979 return $db_query->fetchall_arrayref;
2980}
2981
2982=head2 in_array()
2983
2984from Array::PAT - mimics the in_array() function
2985found in PHP. Yuck but works for small arrays.
2986
2987=cut
2988sub in_array
2989{
2990 my ($check, @array) = @_;
2991 my $retval = 0;
2992 foreach my $test (@array){
2993 if($check eq $test){
2994 $retval = 1;
2995 }
2996 }
2997 return $retval;
2998}
2999
3000=head2 safe_pipe_capture
3001
5348b6e7 3002an alternative to `command` that allows input to be passed as an array
3fda8c4c
ML
3003to work around shell problems with weird characters in arguments
3004
3005=cut
3006sub safe_pipe_capture {
3007
3008 my @output;
3009
3010 if (my $pid = open my $child, '-|') {
3011 @output = (<$child>);
3012 close $child or die join(' ',@_).": $! $?";
3013 } else {
3014 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3015 }
3016 return wantarray ? @output : join('',@output);
3017}
3018
eb1780d4
FL
3019=head2 mangle_dirname
3020
3021create a string from a directory name that is suitable to use as
3022part of a filename, mainly by converting all chars except \w.- to _
3023
3024=cut
3025sub mangle_dirname {
3026 my $dirname = shift;
3027 return unless defined $dirname;
3028
3029 $dirname =~ s/[^\w.-]/_/g;
3030
3031 return $dirname;
3032}
3fda8c4c
ML
3033
30341;