]> git.ipfire.org Git - thirdparty/git.git/blame - git-cvsserver.perl
git-cvsserver: authentication support for pserver
[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/;
044182ef 24use File::Path qw/rmtree/;
3fda8c4c 25use File::Basename;
693b6327
FL
26use Getopt::Long qw(:config require_order no_ignore_case);
27
28my $VERSION = '@@GIT_VERSION@@';
3fda8c4c
ML
29
30my $log = GITCVS::log->new();
31my $cfg;
32
33my $DATE_LIST = {
34 Jan => "01",
35 Feb => "02",
36 Mar => "03",
37 Apr => "04",
38 May => "05",
39 Jun => "06",
40 Jul => "07",
41 Aug => "08",
42 Sep => "09",
43 Oct => "10",
44 Nov => "11",
45 Dec => "12",
46};
47
48# Enable autoflush for STDOUT (otherwise the whole thing falls apart)
49$| = 1;
50
51#### Definition and mappings of functions ####
52
53my $methods = {
54 'Root' => \&req_Root,
55 'Valid-responses' => \&req_Validresponses,
56 'valid-requests' => \&req_validrequests,
57 'Directory' => \&req_Directory,
58 'Entry' => \&req_Entry,
59 'Modified' => \&req_Modified,
60 'Unchanged' => \&req_Unchanged,
7172aabb 61 'Questionable' => \&req_Questionable,
3fda8c4c
ML
62 'Argument' => \&req_Argument,
63 'Argumentx' => \&req_Argument,
64 'expand-modules' => \&req_expandmodules,
65 'add' => \&req_add,
66 'remove' => \&req_remove,
67 'co' => \&req_co,
68 'update' => \&req_update,
69 'ci' => \&req_ci,
70 'diff' => \&req_diff,
71 'log' => \&req_log,
7172aabb 72 'rlog' => \&req_log,
3fda8c4c
ML
73 'tag' => \&req_CATCHALL,
74 'status' => \&req_status,
75 'admin' => \&req_CATCHALL,
76 'history' => \&req_CATCHALL,
38bcd31a
DD
77 'watchers' => \&req_EMPTY,
78 'editors' => \&req_EMPTY,
499cc56a 79 'noop' => \&req_EMPTY,
3fda8c4c
ML
80 'annotate' => \&req_annotate,
81 'Global_option' => \&req_Globaloption,
82 #'annotate' => \&req_CATCHALL,
83};
84
85##############################################
86
87
88# $state holds all the bits of information the clients sends us that could
89# potentially be useful when it comes to actually _doing_ something.
42217f13 90my $state = { prependdir => '' };
044182ef
MO
91
92# Work is for managing temporary working directory
93my $work =
94 {
95 state => undef, # undef, 1 (empty), 2 (with stuff)
96 workDir => undef,
97 index => undef,
98 emptyDir => undef,
99 tmpDir => undef
100 };
101
3fda8c4c
ML
102$log->info("--------------- STARTING -----------------");
103
693b6327 104my $usage =
1b1dd23f 105 "Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n".
693b6327 106 " --base-path <path> : Prepend to requested CVSROOT\n".
03bd0d60 107 " Can be read from GIT_CVSSERVER_BASE_PATH\n".
693b6327
FL
108 " --strict-paths : Don't allow recursing into subdirectories\n".
109 " --export-all : Don't check for gitcvs.enabled in config\n".
110 " --version, -V : Print version information and exit\n".
111 " --help, -h, -H : Print usage information and exit\n".
112 "\n".
113 "<directory> ... is a list of allowed directories. If no directories\n".
114 "are given, all are allowed. This is an additional restriction, gitcvs\n".
03bd0d60
PM
115 "access still needs to be enabled by the gitcvs.enabled config option.\n".
116 "Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n";
693b6327
FL
117
118my @opts = ( 'help|h|H', 'version|V',
119 'base-path=s', 'strict-paths', 'export-all' );
120GetOptions( $state, @opts )
121 or die $usage;
122
123if ($state->{version}) {
124 print "git-cvsserver version $VERSION\n";
125 exit;
126}
127if ($state->{help}) {
128 print $usage;
129 exit;
130}
131
3fda8c4c
ML
132my $TEMP_DIR = tempdir( CLEANUP => 1 );
133$log->debug("Temporary directory is '$TEMP_DIR'");
134
693b6327
FL
135$state->{method} = 'ext';
136if (@ARGV) {
137 if ($ARGV[0] eq 'pserver') {
138 $state->{method} = 'pserver';
139 shift @ARGV;
140 } elsif ($ARGV[0] eq 'server') {
141 shift @ARGV;
142 }
143}
144
145# everything else is a directory
146$state->{allowed_roots} = [ @ARGV ];
147
226bccb9
FL
148# don't export the whole system unless the users requests it
149if ($state->{'export-all'} && !@{$state->{allowed_roots}}) {
150 die "--export-all can only be used together with an explicit whitelist\n";
151}
152
03bd0d60
PM
153# Environment handling for running under git-shell
154if (exists $ENV{GIT_CVSSERVER_BASE_PATH}) {
155 if ($state->{'base-path'}) {
156 die "Cannot specify base path both ways.\n";
157 }
158 my $base_path = $ENV{GIT_CVSSERVER_BASE_PATH};
159 $state->{'base-path'} = $base_path;
160 $log->debug("Picked up base path '$base_path' from environment.\n");
161}
162if (exists $ENV{GIT_CVSSERVER_ROOT}) {
163 if (@{$state->{allowed_roots}}) {
164 die "Cannot specify roots both ways: @ARGV\n";
165 }
166 my $allowed_root = $ENV{GIT_CVSSERVER_ROOT};
167 $state->{allowed_roots} = [ $allowed_root ];
168 $log->debug("Picked up allowed root '$allowed_root' from environment.\n");
169}
170
91a6bf46 171# if we are called with a pserver argument,
5348b6e7 172# deal with the authentication cat before entering the
91a6bf46 173# main loop
693b6327 174if ($state->{method} eq 'pserver') {
91a6bf46 175 my $line = <STDIN>; chomp $line;
24a97d84 176 unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) {
91a6bf46
ML
177 die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n";
178 }
24a97d84 179 my $request = $1;
91a6bf46 180 $line = <STDIN>; chomp $line;
2a4b5d5a
BG
181 unless (req_Root('root', $line)) { # reuse Root
182 print "E Invalid root $line \n";
183 exit 1;
184 }
91a6bf46 185 $line = <STDIN>; chomp $line;
031a027a
ÆAB
186 my $user = $line;
187 $line = <STDIN>; chomp $line;
188 my $password = $line;
189
190 unless ($user eq 'anonymous') {
191 # Trying to authenticate a user
192 if (not exists $cfg->{gitcvs}->{users}) {
193 print "E the repo config file needs a [gitcvs.users] section with user/password key-value pairs\n";
194 print "I HATE YOU\n";
195 exit 1;
196 } elsif (exists $cfg->{gitcvs}->{users} and not exists $cfg->{gitcvs}->{users}->{$user}) {
197 #print "E the repo config file has a [gitcvs.users] section but the user $user is not defined in it\n";
198 print "I HATE YOU\n";
199 exit 1;
200 } else {
201 my $descrambled_password = descramble($password);
202 my $cleartext_password = $cfg->{gitcvs}->{users}->{$user};
203 if ($descrambled_password ne $cleartext_password) {
204 #print "E The password supplied for user $user was incorrect\n";
205 print "I HATE YOU\n";
206 exit 1;
207 }
208 # else fall through to LOVE
209 }
91a6bf46 210 }
031a027a
ÆAB
211
212 # For checking whether the user is anonymous on commit
213 $state->{user} = $user;
214
91a6bf46 215 $line = <STDIN>; chomp $line;
24a97d84
FL
216 unless ($line eq "END $request REQUEST") {
217 die "E Do not understand $line -- expecting END $request REQUEST\n";
91a6bf46
ML
218 }
219 print "I LOVE YOU\n";
24a97d84 220 exit if $request eq 'VERIFICATION'; # cvs login
91a6bf46
ML
221 # and now back to our regular programme...
222}
223
3fda8c4c
ML
224# Keep going until the client closes the connection
225while (<STDIN>)
226{
227 chomp;
228
5348b6e7 229 # Check to see if we've seen this method, and call appropriate function.
3fda8c4c
ML
230 if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) )
231 {
232 # use the $methods hash to call the appropriate sub for this command
233 #$log->info("Method : $1");
234 &{$methods->{$1}}($1,$2);
235 } else {
236 # log fatal because we don't understand this function. If this happens
237 # we're fairly screwed because we don't know if the client is expecting
238 # a response. If it is, the client will hang, we'll hang, and the whole
239 # thing will be custard.
240 $log->fatal("Don't understand command $_\n");
241 die("Unknown command $_");
242 }
243}
244
245$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]);
246$log->info("--------------- FINISH -----------------");
247
044182ef
MO
248chdir '/';
249exit 0;
250
3fda8c4c
ML
251# Magic catchall method.
252# This is the method that will handle all commands we haven't yet
253# implemented. It simply sends a warning to the log file indicating a
254# command that hasn't been implemented has been invoked.
255sub req_CATCHALL
256{
257 my ( $cmd, $data ) = @_;
258 $log->warn("Unhandled command : req_$cmd : $data");
259}
260
38bcd31a
DD
261# This method invariably succeeds with an empty response.
262sub req_EMPTY
263{
264 print "ok\n";
265}
3fda8c4c
ML
266
267# Root pathname \n
268# Response expected: no. Tell the server which CVSROOT to use. Note that
269# pathname is a local directory and not a fully qualified CVSROOT variable.
270# pathname must already exist; if creating a new root, use the init
271# request, not Root. pathname does not include the hostname of the server,
272# how to access the server, etc.; by the time the CVS protocol is in use,
273# connection, authentication, etc., are already taken care of. The Root
274# request must be sent only once, and it must be sent before any requests
275# other than Valid-responses, valid-requests, UseUnchanged, Set or init.
276sub req_Root
277{
278 my ( $cmd, $data ) = @_;
279 $log->debug("req_Root : $data");
280
4890888d
FL
281 unless ($data =~ m#^/#) {
282 print "error 1 Root must be an absolute pathname\n";
283 return 0;
284 }
285
fd1cd91e
FL
286 my $cvsroot = $state->{'base-path'} || '';
287 $cvsroot =~ s#/+$##;
288 $cvsroot .= $data;
289
4890888d 290 if ($state->{CVSROOT}
fd1cd91e 291 && ($state->{CVSROOT} ne $cvsroot)) {
4890888d
FL
292 print "error 1 Conflicting roots specified\n";
293 return 0;
294 }
295
fd1cd91e 296 $state->{CVSROOT} = $cvsroot;
3fda8c4c
ML
297
298 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
693b6327
FL
299
300 if (@{$state->{allowed_roots}}) {
301 my $allowed = 0;
302 foreach my $dir (@{$state->{allowed_roots}}) {
303 next unless $dir =~ m#^/#;
304 $dir =~ s#/+$##;
305 if ($state->{'strict-paths'}) {
306 if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) {
307 $allowed = 1;
308 last;
309 }
310 } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) {
311 $allowed = 1;
312 last;
313 }
314 }
315
316 unless ($allowed) {
317 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
318 print "E \n";
319 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
320 return 0;
321 }
322 }
323
cdb6760e
ML
324 unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') {
325 print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n";
693b6327
FL
326 print "E \n";
327 print "error 1 $ENV{GIT_DIR} is not a valid repository\n";
cdb6760e
ML
328 return 0;
329 }
3fda8c4c 330
d2feb01a 331 my @gitvars = `git config -l`;
cdb6760e 332 if ($?) {
e0d10e1c 333 print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n";
cdb6760e 334 print "E \n";
e0d10e1c 335 print "error 1 - problem executing git-config\n";
cdb6760e
ML
336 return 0;
337 }
338 foreach my $line ( @gitvars )
3fda8c4c 339 {
031a027a 340 next unless ( $line =~ /^(gitcvs)\.(?:(ext|pserver|users)\.)?([\w-]+)=(.*)$/ );
f987afa8
FL
341 unless ($2) {
342 $cfg->{$1}{$3} = $4;
92a39a14
FL
343 } else {
344 $cfg->{$1}{$2}{$3} = $4;
345 }
3fda8c4c
ML
346 }
347
523d12e5
JH
348 my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled}
349 || $cfg->{gitcvs}{enabled});
226bccb9
FL
350 unless ($state->{'export-all'} ||
351 ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) {
3fda8c4c
ML
352 print "E GITCVS emulation needs to be enabled on this repo\n";
353 print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n";
354 print "E \n";
355 print "error 1 GITCVS emulation disabled\n";
91a6bf46 356 return 0;
3fda8c4c
ML
357 }
358
d55820ce
FL
359 my $logfile = $cfg->{gitcvs}{$state->{method}}{logfile} || $cfg->{gitcvs}{logfile};
360 if ( $logfile )
3fda8c4c 361 {
d55820ce 362 $log->setfile($logfile);
3fda8c4c
ML
363 } else {
364 $log->nofile();
365 }
91a6bf46
ML
366
367 return 1;
3fda8c4c
ML
368}
369
370# Global_option option \n
371# Response expected: no. Transmit one of the global options `-q', `-Q',
372# `-l', `-t', `-r', or `-n'. option must be one of those strings, no
373# variations (such as combining of options) are allowed. For graceful
374# handling of valid-requests, it is probably better to make new global
375# options separate requests, rather than trying to add them to this
376# request.
377sub req_Globaloption
378{
379 my ( $cmd, $data ) = @_;
380 $log->debug("req_Globaloption : $data");
7d90095a 381 $state->{globaloptions}{$data} = 1;
3fda8c4c
ML
382}
383
384# Valid-responses request-list \n
385# Response expected: no. Tell the server what responses the client will
386# accept. request-list is a space separated list of tokens.
387sub req_Validresponses
388{
389 my ( $cmd, $data ) = @_;
5348b6e7 390 $log->debug("req_Validresponses : $data");
3fda8c4c
ML
391
392 # TODO : re-enable this, currently it's not particularly useful
393 #$state->{validresponses} = [ split /\s+/, $data ];
394}
395
396# valid-requests \n
397# Response expected: yes. Ask the server to send back a Valid-requests
398# response.
399sub req_validrequests
400{
401 my ( $cmd, $data ) = @_;
402
403 $log->debug("req_validrequests");
404
405 $log->debug("SEND : Valid-requests " . join(" ",keys %$methods));
406 $log->debug("SEND : ok");
407
408 print "Valid-requests " . join(" ",keys %$methods) . "\n";
409 print "ok\n";
410}
411
412# Directory local-directory \n
413# Additional data: repository \n. Response expected: no. Tell the server
414# what directory to use. The repository should be a directory name from a
415# previous server response. Note that this both gives a default for Entry
416# and Modified and also for ci and the other commands; normal usage is to
417# send Directory for each directory in which there will be an Entry or
418# Modified, and then a final Directory for the original directory, then the
419# command. The local-directory is relative to the top level at which the
420# command is occurring (i.e. the last Directory which is sent before the
421# command); to indicate that top level, `.' should be sent for
422# local-directory.
423sub req_Directory
424{
425 my ( $cmd, $data ) = @_;
426
427 my $repository = <STDIN>;
428 chomp $repository;
429
430
431 $state->{localdir} = $data;
432 $state->{repository} = $repository;
7d90095a 433 $state->{path} = $repository;
f9acaeae 434 $state->{path} =~ s/^\Q$state->{CVSROOT}\E\///;
7d90095a
MS
435 $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//);
436 $state->{path} .= "/" if ( $state->{path} =~ /\S/ );
437
438 $state->{directory} = $state->{localdir};
439 $state->{directory} = "" if ( $state->{directory} eq "." );
3fda8c4c
ML
440 $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ );
441
d988b822 442 if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ )
7d90095a
MS
443 {
444 $log->info("Setting prepend to '$state->{path}'");
445 $state->{prependdir} = $state->{path};
446 foreach my $entry ( keys %{$state->{entries}} )
447 {
448 $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
449 delete $state->{entries}{$entry};
450 }
451 }
452
453 if ( defined ( $state->{prependdir} ) )
454 {
455 $log->debug("Prepending '$state->{prependdir}' to state|directory");
456 $state->{directory} = $state->{prependdir} . $state->{directory}
457 }
82000d74 458 $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
3fda8c4c
ML
459}
460
461# Entry entry-line \n
462# Response expected: no. Tell the server what version of a file is on the
463# local machine. The name in entry-line is a name relative to the directory
464# most recently specified with Directory. If the user is operating on only
465# some files in a directory, Entry requests for only those files need be
466# included. If an Entry request is sent without Modified, Is-modified, or
467# Unchanged, it means the file is lost (does not exist in the working
468# directory). If both Entry and one of Modified, Is-modified, or Unchanged
469# are sent for the same file, Entry must be sent first. For a given file,
470# one can send Modified, Is-modified, or Unchanged, but not more than one
471# of these three.
472sub req_Entry
473{
474 my ( $cmd, $data ) = @_;
475
7d90095a 476 #$log->debug("req_Entry : $data");
3fda8c4c
ML
477
478 my @data = split(/\//, $data);
479
480 $state->{entries}{$state->{directory}.$data[1]} = {
481 revision => $data[2],
482 conflict => $data[3],
483 options => $data[4],
484 tag_or_date => $data[5],
485 };
7d90095a
MS
486
487 $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
488}
489
490# Questionable filename \n
491# Response expected: no. Additional data: no. Tell the server to check
492# whether filename should be ignored, and if not, next time the server
493# sends responses, send (in a M response) `?' followed by the directory and
494# filename. filename must not contain `/'; it needs to be a file in the
495# directory named by the most recent Directory request.
496sub req_Questionable
497{
498 my ( $cmd, $data ) = @_;
499
500 $log->debug("req_Questionable : $data");
501 $state->{entries}{$state->{directory}.$data}{questionable} = 1;
3fda8c4c
ML
502}
503
504# add \n
505# Response expected: yes. Add a file or directory. This uses any previous
506# Argument, Directory, Entry, or Modified requests, if they have been sent.
507# The last Directory sent specifies the working directory at the time of
508# the operation. To add a directory, send the directory to be added using
509# Directory and Argument requests.
510sub req_add
511{
512 my ( $cmd, $data ) = @_;
513
514 argsplit("add");
515
4db0c8de
FL
516 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
517 $updater->update();
518
519 argsfromdir($updater);
520
3fda8c4c
ML
521 my $addcount = 0;
522
523 foreach my $filename ( @{$state->{args}} )
524 {
525 $filename = filecleanup($filename);
526
4db0c8de
FL
527 my $meta = $updater->getmeta($filename);
528 my $wrev = revparse($filename);
529
530 if ($wrev && $meta && ($wrev < 0))
531 {
532 # previously removed file, add back
533 $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
534
535 print "MT +updated\n";
536 print "MT text U \n";
537 print "MT fname $filename\n";
538 print "MT newline\n";
539 print "MT -updated\n";
540
541 unless ( $state->{globaloptions}{-n} )
542 {
543 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
544
545 print "Created $dirpart\n";
546 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
547
548 # this is an "entries" line
90948a42 549 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
4db0c8de
FL
550 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
551 print "/$filepart/1.$meta->{revision}//$kopts/\n";
552 # permissions
553 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
554 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
555 # transmit file
556 transmitfile($meta->{filehash});
557 }
558
559 next;
560 }
561
3fda8c4c
ML
562 unless ( defined ( $state->{entries}{$filename}{modified_filename} ) )
563 {
564 print "E cvs add: nothing known about `$filename'\n";
565 next;
566 }
567 # TODO : check we're not squashing an already existing file
568 if ( defined ( $state->{entries}{$filename}{revision} ) )
569 {
570 print "E cvs add: `$filename' has already been entered\n";
571 next;
572 }
573
7d90095a 574 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
575
576 print "E cvs add: scheduling file `$filename' for addition\n";
577
578 print "Checked-in $dirpart\n";
579 print "$filename\n";
90948a42
MO
580 my $kopts = kopts_from_path($filename,"file",
581 $state->{entries}{$filename}{modified_filename});
8538e876 582 print "/$filepart/0//$kopts/\n";
3fda8c4c 583
8a06a632
MO
584 my $requestedKopts = $state->{opt}{k};
585 if(defined($requestedKopts))
586 {
587 $requestedKopts = "-k$requestedKopts";
588 }
589 else
590 {
591 $requestedKopts = "";
592 }
593 if( $kopts ne $requestedKopts )
594 {
595 $log->warn("Ignoring requested -k='$requestedKopts'"
596 . " for '$filename'; detected -k='$kopts' instead");
597 #TODO: Also have option to send warning to user?
598 }
599
3fda8c4c
ML
600 $addcount++;
601 }
602
603 if ( $addcount == 1 )
604 {
605 print "E cvs add: use `cvs commit' to add this file permanently\n";
606 }
607 elsif ( $addcount > 1 )
608 {
609 print "E cvs add: use `cvs commit' to add these files permanently\n";
610 }
611
612 print "ok\n";
613}
614
615# remove \n
616# Response expected: yes. Remove a file. This uses any previous Argument,
617# Directory, Entry, or Modified requests, if they have been sent. The last
618# Directory sent specifies the working directory at the time of the
619# operation. Note that this request does not actually do anything to the
620# repository; the only effect of a successful remove request is to supply
621# the client with a new entries line containing `-' to indicate a removed
622# file. In fact, the client probably could perform this operation without
623# contacting the server, although using remove may cause the server to
624# perform a few more checks. The client sends a subsequent ci request to
625# actually record the removal in the repository.
626sub req_remove
627{
628 my ( $cmd, $data ) = @_;
629
630 argsplit("remove");
631
632 # Grab a handle to the SQLite db and do any necessary updates
633 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
634 $updater->update();
635
636 #$log->debug("add state : " . Dumper($state));
637
638 my $rmcount = 0;
639
640 foreach my $filename ( @{$state->{args}} )
641 {
642 $filename = filecleanup($filename);
643
644 if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) )
645 {
646 print "E cvs remove: file `$filename' still in working directory\n";
647 next;
648 }
649
650 my $meta = $updater->getmeta($filename);
651 my $wrev = revparse($filename);
652
653 unless ( defined ( $wrev ) )
654 {
655 print "E cvs remove: nothing known about `$filename'\n";
656 next;
657 }
658
659 if ( defined($wrev) and $wrev < 0 )
660 {
661 print "E cvs remove: file `$filename' already scheduled for removal\n";
662 next;
663 }
664
665 unless ( $wrev == $meta->{revision} )
666 {
667 # TODO : not sure if the format of this message is quite correct.
668 print "E cvs remove: Up to date check failed for `$filename'\n";
669 next;
670 }
671
672
7d90095a 673 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
674
675 print "E cvs remove: scheduling `$filename' for removal\n";
676
677 print "Checked-in $dirpart\n";
678 print "$filename\n";
90948a42 679 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
8538e876 680 print "/$filepart/-1.$wrev//$kopts/\n";
3fda8c4c
ML
681
682 $rmcount++;
683 }
684
685 if ( $rmcount == 1 )
686 {
687 print "E cvs remove: use `cvs commit' to remove this file permanently\n";
688 }
689 elsif ( $rmcount > 1 )
690 {
691 print "E cvs remove: use `cvs commit' to remove these files permanently\n";
692 }
693
694 print "ok\n";
695}
696
697# Modified filename \n
698# Response expected: no. Additional data: mode, \n, file transmission. Send
699# the server a copy of one locally modified file. filename is a file within
700# the most recent directory sent with Directory; it must not contain `/'.
701# If the user is operating on only some files in a directory, only those
702# files need to be included. This can also be sent without Entry, if there
703# is no entry for the file.
704sub req_Modified
705{
706 my ( $cmd, $data ) = @_;
707
708 my $mode = <STDIN>;
a5e40798
JM
709 defined $mode
710 or (print "E end of file reading mode for $data\n"), return;
3fda8c4c
ML
711 chomp $mode;
712 my $size = <STDIN>;
a5e40798
JM
713 defined $size
714 or (print "E end of file reading size of $data\n"), return;
3fda8c4c
ML
715 chomp $size;
716
717 # Grab config information
718 my $blocksize = 8192;
719 my $bytesleft = $size;
720 my $tmp;
721
722 # Get a filehandle/name to write it to
723 my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR );
724
725 # Loop over file data writing out to temporary file.
726 while ( $bytesleft )
727 {
728 $blocksize = $bytesleft if ( $bytesleft < $blocksize );
729 read STDIN, $tmp, $blocksize;
730 print $fh $tmp;
731 $bytesleft -= $blocksize;
732 }
733
a5e40798
JM
734 close $fh
735 or (print "E failed to write temporary, $filename: $!\n"), return;
3fda8c4c
ML
736
737 # Ensure we have something sensible for the file mode
738 if ( $mode =~ /u=(\w+)/ )
739 {
740 $mode = $1;
741 } else {
742 $mode = "rw";
743 }
744
745 # Save the file data in $state
746 $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename;
747 $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode;
d2feb01a 748 $state->{entries}{$state->{directory}.$data}{modified_hash} = `git hash-object $filename`;
3fda8c4c
ML
749 $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s;
750
751 #$log->debug("req_Modified : file=$data mode=$mode size=$size");
752}
753
754# Unchanged filename \n
755# Response expected: no. Tell the server that filename has not been
756# modified in the checked out directory. The filename is a file within the
757# most recent directory sent with Directory; it must not contain `/'.
758sub req_Unchanged
759{
760 my ( $cmd, $data ) = @_;
761
762 $state->{entries}{$state->{directory}.$data}{unchanged} = 1;
763
764 #$log->debug("req_Unchanged : $data");
765}
766
767# Argument text \n
768# Response expected: no. Save argument for use in a subsequent command.
769# Arguments accumulate until an argument-using command is given, at which
770# point they are forgotten.
771# Argumentx text \n
772# Response expected: no. Append \n followed by text to the current argument
773# being saved.
774sub req_Argument
775{
776 my ( $cmd, $data ) = @_;
777
2c3cff49 778 # Argumentx means: append to last Argument (with a newline in front)
3fda8c4c
ML
779
780 $log->debug("$cmd : $data");
781
2c3cff49
JS
782 if ( $cmd eq 'Argumentx') {
783 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data;
784 } else {
785 push @{$state->{arguments}}, $data;
786 }
3fda8c4c
ML
787}
788
789# expand-modules \n
790# Response expected: yes. Expand the modules which are specified in the
791# arguments. Returns the data in Module-expansion responses. Note that the
792# server can assume that this is checkout or export, not rtag or rdiff; the
793# latter do not access the working directory and thus have no need to
794# expand modules on the client side. Expand may not be the best word for
795# what this request does. It does not necessarily tell you all the files
796# contained in a module, for example. Basically it is a way of telling you
797# which working directories the server needs to know about in order to
798# handle a checkout of the specified modules. For example, suppose that the
799# server has a module defined by
800# aliasmodule -a 1dir
801# That is, one can check out aliasmodule and it will take 1dir in the
802# repository and check it out to 1dir in the working directory. Now suppose
803# the client already has this module checked out and is planning on using
804# the co request to update it. Without using expand-modules, the client
805# would have two bad choices: it could either send information about all
806# working directories under the current directory, which could be
807# unnecessarily slow, or it could be ignorant of the fact that aliasmodule
808# stands for 1dir, and neglect to send information for 1dir, which would
809# lead to incorrect operation. With expand-modules, the client would first
810# ask for the module to be expanded:
811sub req_expandmodules
812{
813 my ( $cmd, $data ) = @_;
814
815 argsplit();
816
817 $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) );
818
819 unless ( ref $state->{arguments} eq "ARRAY" )
820 {
821 print "ok\n";
822 return;
823 }
824
825 foreach my $module ( @{$state->{arguments}} )
826 {
827 $log->debug("SEND : Module-expansion $module");
828 print "Module-expansion $module\n";
829 }
830
831 print "ok\n";
832 statecleanup();
833}
834
835# co \n
836# Response expected: yes. Get files from the repository. This uses any
837# previous Argument, Directory, Entry, or Modified requests, if they have
838# been sent. Arguments to this command are module names; the client cannot
839# know what directories they correspond to except by (1) just sending the
840# co request, and then seeing what directory names the server sends back in
841# its responses, and (2) the expand-modules request.
842sub req_co
843{
844 my ( $cmd, $data ) = @_;
845
846 argsplit("co");
847
89a9167f
LN
848 # Provide list of modules, if -c was used.
849 if (exists $state->{opt}{c}) {
850 my $showref = `git show-ref --heads`;
851 for my $line (split '\n', $showref) {
852 if ( $line =~ m% refs/heads/(.*)$% ) {
853 print "M $1\t$1\n";
854 }
855 }
856 print "ok\n";
857 return 1;
858 }
859
3fda8c4c 860 my $module = $state->{args}[0];
8a06a632 861 $state->{module} = $module;
3fda8c4c
ML
862 my $checkout_path = $module;
863
864 # use the user specified directory if we're given it
865 $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) );
866
867 $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) );
868
869 $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'");
870
871 $ENV{GIT_DIR} = $state->{CVSROOT} . "/";
872
873 # Grab a handle to the SQLite db and do any necessary updates
874 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
875 $updater->update();
876
c8c4f220
ML
877 $checkout_path =~ s|/$||; # get rid of trailing slashes
878
879 # Eclipse seems to need the Clear-sticky command
880 # to prepare the 'Entries' file for the new directory.
881 print "Clear-sticky $checkout_path/\n";
e74ee784 882 print $state->{CVSROOT} . "/$module/\n";
c8c4f220 883 print "Clear-static-directory $checkout_path/\n";
e74ee784 884 print $state->{CVSROOT} . "/$module/\n";
6be32d47
ML
885 print "Clear-sticky $checkout_path/\n"; # yes, twice
886 print $state->{CVSROOT} . "/$module/\n";
887 print "Template $checkout_path/\n";
888 print $state->{CVSROOT} . "/$module/\n";
889 print "0\n";
c8c4f220 890
3fda8c4c 891 # instruct the client that we're checking out to $checkout_path
c8c4f220
ML
892 print "E cvs checkout: Updating $checkout_path\n";
893
894 my %seendirs = ();
501c7372 895 my $lastdir ='';
3fda8c4c 896
6be32d47
ML
897 # recursive
898 sub prepdir {
899 my ($dir, $repodir, $remotedir, $seendirs) = @_;
900 my $parent = dirname($dir);
901 $dir =~ s|/+$||;
902 $repodir =~ s|/+$||;
903 $remotedir =~ s|/+$||;
904 $parent =~ s|/+$||;
905 $log->debug("announcedir $dir, $repodir, $remotedir" );
906
907 if ($parent eq '.' || $parent eq './') {
908 $parent = '';
909 }
910 # recurse to announce unseen parents first
911 if (length($parent) && !exists($seendirs->{$parent})) {
912 prepdir($parent, $repodir, $remotedir, $seendirs);
913 }
914 # Announce that we are going to modify at the parent level
915 if ($parent) {
916 print "E cvs checkout: Updating $remotedir/$parent\n";
917 } else {
918 print "E cvs checkout: Updating $remotedir\n";
919 }
920 print "Clear-sticky $remotedir/$parent/\n";
921 print "$repodir/$parent/\n";
922
923 print "Clear-static-directory $remotedir/$dir/\n";
924 print "$repodir/$dir/\n";
925 print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
926 print "$repodir/$parent/\n";
927 print "Template $remotedir/$dir/\n";
928 print "$repodir/$dir/\n";
929 print "0\n";
930
931 $seendirs->{$dir} = 1;
932 }
933
3fda8c4c
ML
934 foreach my $git ( @{$updater->gethead} )
935 {
936 # Don't want to check out deleted files
937 next if ( $git->{filehash} eq "deleted" );
938
8a06a632 939 my $fullName = $git->{name};
3fda8c4c
ML
940 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
941
6be32d47
ML
942 if (length($git->{dir}) && $git->{dir} ne './'
943 && $git->{dir} ne $lastdir ) {
944 unless (exists($seendirs{$git->{dir}})) {
945 prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
946 $checkout_path, \%seendirs);
947 $lastdir = $git->{dir};
948 $seendirs{$git->{dir}} = 1;
949 }
950 print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
951 }
952
3fda8c4c
ML
953 # modification time of this file
954 print "Mod-time $git->{modified}\n";
955
956 # print some information to the client
3fda8c4c
ML
957 if ( defined ( $git->{dir} ) and $git->{dir} ne "./" )
958 {
c8c4f220 959 print "M U $checkout_path/$git->{dir}$git->{name}\n";
3fda8c4c 960 } else {
c8c4f220 961 print "M U $checkout_path/$git->{name}\n";
3fda8c4c 962 }
c8c4f220 963
6be32d47
ML
964 # instruct client we're sending a file to put in this path
965 print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n";
3fda8c4c 966
6be32d47 967 print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n";
3fda8c4c
ML
968
969 # this is an "entries" line
90948a42 970 my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
8538e876 971 print "/$git->{name}/1.$git->{revision}//$kopts/\n";
3fda8c4c
ML
972 # permissions
973 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
974
975 # transmit file
976 transmitfile($git->{filehash});
977 }
978
979 print "ok\n";
980
981 statecleanup();
982}
983
984# update \n
985# Response expected: yes. Actually do a cvs update command. This uses any
986# previous Argument, Directory, Entry, or Modified requests, if they have
987# been sent. The last Directory sent specifies the working directory at the
988# time of the operation. The -I option is not used--files which the client
989# can decide whether to ignore are not mentioned and the client sends the
990# Questionable request for others.
991sub req_update
992{
993 my ( $cmd, $data ) = @_;
994
995 $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" ));
996
997 argsplit("update");
998
858cbfba 999 #
5348b6e7 1000 # It may just be a client exploring the available heads/modules
858cbfba
ML
1001 # in that case, list them as top level directories and leave it
1002 # at that. Eclipse uses this technique to offer you a list of
1003 # projects (heads in this case) to checkout.
1004 #
1005 if ($state->{module} eq '') {
b20171eb 1006 my $showref = `git show-ref --heads`;
858cbfba 1007 print "E cvs update: Updating .\n";
b20171eb
LN
1008 for my $line (split '\n', $showref) {
1009 if ( $line =~ m% refs/heads/(.*)$% ) {
1010 print "E cvs update: New directory `$1'\n";
1011 }
1012 }
1013 print "ok\n";
1014 return 1;
858cbfba
ML
1015 }
1016
1017
3fda8c4c
ML
1018 # Grab a handle to the SQLite db and do any necessary updates
1019 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1020
1021 $updater->update();
1022
7d90095a 1023 argsfromdir($updater);
3fda8c4c
ML
1024
1025 #$log->debug("update state : " . Dumper($state));
1026
8e4c4e7d
SO
1027 my $last_dirname = "///";
1028
addf88e4 1029 # foreach file specified on the command line ...
3fda8c4c
ML
1030 foreach my $filename ( @{$state->{args}} )
1031 {
1032 $filename = filecleanup($filename);
1033
7d90095a
MS
1034 $log->debug("Processing file $filename");
1035
8e4c4e7d
SO
1036 unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
1037 {
1038 my $cur_dirname = dirname($filename);
1039 if ( $cur_dirname ne $last_dirname )
1040 {
1041 $last_dirname = $cur_dirname;
1042 if ( $cur_dirname eq "" )
1043 {
1044 $cur_dirname = ".";
1045 }
1046 print "E cvs update: Updating $cur_dirname\n";
1047 }
1048 }
1049
3fda8c4c
ML
1050 # if we have a -C we should pretend we never saw modified stuff
1051 if ( exists ( $state->{opt}{C} ) )
1052 {
1053 delete $state->{entries}{$filename}{modified_hash};
1054 delete $state->{entries}{$filename}{modified_filename};
1055 $state->{entries}{$filename}{unchanged} = 1;
1056 }
1057
1058 my $meta;
1059 if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
1060 {
1061 $meta = $updater->getmeta($filename, $1);
1062 } else {
1063 $meta = $updater->getmeta($filename);
1064 }
1065
e78f69a3
DD
1066 # If -p was given, "print" the contents of the requested revision.
1067 if ( exists ( $state->{opt}{p} ) ) {
1068 if ( defined ( $meta->{revision} ) ) {
1069 $log->info("Printing '$filename' revision " . $meta->{revision});
1070
1071 transmitfile($meta->{filehash}, { print => 1 });
1072 }
1073
1074 next;
1075 }
1076
0a7a9a12
JS
1077 if ( ! defined $meta )
1078 {
1079 $meta = {
1080 name => $filename,
1081 revision => 0,
1082 filehash => 'added'
1083 };
1084 }
3fda8c4c
ML
1085
1086 my $oldmeta = $meta;
1087
1088 my $wrev = revparse($filename);
1089
1090 # If the working copy is an old revision, lets get that version too for comparison.
1091 if ( defined($wrev) and $wrev != $meta->{revision} )
1092 {
1093 $oldmeta = $updater->getmeta($filename, $wrev);
1094 }
1095
1096 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
1097
ec58db15
ML
1098 # Files are up to date if the working copy and repo copy have the same revision,
1099 # and the working copy is unmodified _and_ the user hasn't specified -C
1100 next if ( defined ( $wrev )
1101 and defined($meta->{revision})
1102 and $wrev == $meta->{revision}
1103 and $state->{entries}{$filename}{unchanged}
1104 and not exists ( $state->{opt}{C} ) );
1105
1106 # If the working copy and repo copy have the same revision,
1107 # but the working copy is modified, tell the client it's modified
1108 if ( defined ( $wrev )
1109 and defined($meta->{revision})
1110 and $wrev == $meta->{revision}
cb52d9a1 1111 and defined($state->{entries}{$filename}{modified_hash})
ec58db15
ML
1112 and not exists ( $state->{opt}{C} ) )
1113 {
1114 $log->info("Tell the client the file is modified");
0a7a9a12 1115 print "MT text M \n";
ec58db15
ML
1116 print "MT fname $filename\n";
1117 print "MT newline\n";
1118 next;
1119 }
3fda8c4c
ML
1120
1121 if ( $meta->{filehash} eq "deleted" )
1122 {
7d90095a 1123 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
3fda8c4c
ML
1124
1125 $log->info("Removing '$filename' from working copy (no longer in the repo)");
1126
1127 print "E cvs update: `$filename' is no longer in the repository\n";
7d90095a
MS
1128 # Don't want to actually _DO_ the update if -n specified
1129 unless ( $state->{globaloptions}{-n} ) {
1130 print "Removed $dirpart\n";
1131 print "$filepart\n";
1132 }
3fda8c4c 1133 }
ec58db15 1134 elsif ( not defined ( $state->{entries}{$filename}{modified_hash} )
0a7a9a12
JS
1135 or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash}
1136 or $meta->{filehash} eq 'added' )
3fda8c4c 1137 {
0a7a9a12
JS
1138 # normal update, just send the new revision (either U=Update,
1139 # or A=Add, or R=Remove)
1140 if ( defined($wrev) && $wrev < 0 )
1141 {
1142 $log->info("Tell the client the file is scheduled for removal");
1143 print "MT text R \n";
1144 print "MT fname $filename\n";
1145 print "MT newline\n";
1146 next;
1147 }
535514f1 1148 elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
0a7a9a12 1149 {
535514f1 1150 $log->info("Tell the client the file is scheduled for addition");
0a7a9a12
JS
1151 print "MT text A \n";
1152 print "MT fname $filename\n";
1153 print "MT newline\n";
1154 next;
1155
1156 }
1157 else {
535514f1 1158 $log->info("Updating '$filename' to ".$meta->{revision});
0a7a9a12
JS
1159 print "MT +updated\n";
1160 print "MT text U \n";
1161 print "MT fname $filename\n";
1162 print "MT newline\n";
1163 print "MT -updated\n";
1164 }
3fda8c4c 1165
7d90095a
MS
1166 my ( $filepart, $dirpart ) = filenamesplit($filename,1);
1167
1168 # Don't want to actually _DO_ the update if -n specified
1169 unless ( $state->{globaloptions}{-n} )
1170 {
1171 if ( defined ( $wrev ) )
1172 {
1173 # instruct client we're sending a file to put in this path as a replacement
1174 print "Update-existing $dirpart\n";
1175 $log->debug("Updating existing file 'Update-existing $dirpart'");
1176 } else {
1177 # instruct client we're sending a file to put in this path as a new file
1178 print "Clear-static-directory $dirpart\n";
1179 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1180 print "Clear-sticky $dirpart\n";
1181 print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1182
1183 $log->debug("Creating new file 'Created $dirpart'");
1184 print "Created $dirpart\n";
1185 }
1186 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1187
1188 # this is an "entries" line
90948a42 1189 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
8538e876
AP
1190 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1191 print "/$filepart/1.$meta->{revision}//$kopts/\n";
7d90095a
MS
1192
1193 # permissions
1194 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1195 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1196
1197 # transmit file
1198 transmitfile($meta->{filehash});
1199 }
3fda8c4c 1200 } else {
ec58db15 1201 $log->info("Updating '$filename'");
7d90095a 1202 my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
3fda8c4c 1203
044182ef 1204 my $mergeDir = setupTmpDir();
3fda8c4c 1205
3fda8c4c 1206 my $file_local = $filepart . ".mine";
044182ef 1207 my $mergedFile = "$mergeDir/$file_local";
3fda8c4c
ML
1208 system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local);
1209 my $file_old = $filepart . "." . $oldmeta->{revision};
e78f69a3 1210 transmitfile($oldmeta->{filehash}, { targetfile => $file_old });
3fda8c4c 1211 my $file_new = $filepart . "." . $meta->{revision};
e78f69a3 1212 transmitfile($meta->{filehash}, { targetfile => $file_new });
3fda8c4c
ML
1213
1214 # we need to merge with the local changes ( M=successful merge, C=conflict merge )
1215 $log->info("Merging $file_local, $file_old, $file_new");
459bad77 1216 print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
3fda8c4c 1217
044182ef 1218 $log->debug("Temporary directory for merge is $mergeDir");
3fda8c4c 1219
c6b4fa96 1220 my $return = system("git", "merge-file", $file_local, $file_old, $file_new);
3fda8c4c
ML
1221 $return >>= 8;
1222
044182ef
MO
1223 cleanupTmpDir();
1224
3fda8c4c
ML
1225 if ( $return == 0 )
1226 {
1227 $log->info("Merged successfully");
1228 print "M M $filename\n";
53877846 1229 $log->debug("Merged $dirpart");
7d90095a
MS
1230
1231 # Don't want to actually _DO_ the update if -n specified
1232 unless ( $state->{globaloptions}{-n} )
1233 {
53877846 1234 print "Merged $dirpart\n";
7d90095a
MS
1235 $log->debug($state->{CVSROOT} . "/$state->{module}/$filename");
1236 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
90948a42
MO
1237 my $kopts = kopts_from_path("$dirpart/$filepart",
1238 "file",$mergedFile);
8538e876
AP
1239 $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
1240 print "/$filepart/1.$meta->{revision}//$kopts/\n";
7d90095a 1241 }
3fda8c4c
ML
1242 }
1243 elsif ( $return == 1 )
1244 {
1245 $log->info("Merged with conflicts");
459bad77 1246 print "E cvs update: conflicts found in $filename\n";
3fda8c4c 1247 print "M C $filename\n";
7d90095a
MS
1248
1249 # Don't want to actually _DO_ the update if -n specified
1250 unless ( $state->{globaloptions}{-n} )
1251 {
53877846 1252 print "Merged $dirpart\n";
7d90095a 1253 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
90948a42
MO
1254 my $kopts = kopts_from_path("$dirpart/$filepart",
1255 "file",$mergedFile);
8538e876 1256 print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
7d90095a 1257 }
3fda8c4c
ML
1258 }
1259 else
1260 {
1261 $log->warn("Merge failed");
1262 next;
1263 }
1264
7d90095a
MS
1265 # Don't want to actually _DO_ the update if -n specified
1266 unless ( $state->{globaloptions}{-n} )
1267 {
1268 # permissions
1269 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
1270 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
1271
1272 # transmit file, format is single integer on a line by itself (file
1273 # size) followed by the file contents
1274 # TODO : we should copy files in blocks
044182ef 1275 my $data = `cat $mergedFile`;
7d90095a
MS
1276 $log->debug("File size : " . length($data));
1277 print length($data) . "\n";
1278 print $data;
1279 }
3fda8c4c
ML
1280 }
1281
1282 }
1283
1284 print "ok\n";
1285}
1286
1287sub req_ci
1288{
1289 my ( $cmd, $data ) = @_;
1290
1291 argsplit("ci");
1292
1293 #$log->debug("State : " . Dumper($state));
1294
1295 $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" ));
1296
031a027a 1297 if ( $state->{method} eq 'pserver' and $state->{user} eq 'anonymous' )
91a6bf46 1298 {
031a027a 1299 print "error 1 anonymous user cannot commit via pserver\n";
044182ef 1300 cleanupWorkTree();
91a6bf46
ML
1301 exit;
1302 }
1303
3fda8c4c
ML
1304 if ( -e $state->{CVSROOT} . "/index" )
1305 {
568907f5 1306 $log->warn("file 'index' already exists in the git repository");
3fda8c4c 1307 print "error 1 Index already exists in git repo\n";
044182ef 1308 cleanupWorkTree();
3fda8c4c
ML
1309 exit;
1310 }
1311
3fda8c4c
ML
1312 # Grab a handle to the SQLite db and do any necessary updates
1313 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1314 $updater->update();
1315
ada5ef3b
JH
1316 # Remember where the head was at the beginning.
1317 my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1318 chomp $parenthash;
1319 if ($parenthash !~ /^[0-9a-f]{40}$/) {
1320 print "error 1 pserver cannot find the current HEAD of module";
044182ef 1321 cleanupWorkTree();
ada5ef3b
JH
1322 exit;
1323 }
1324
044182ef 1325 setupWorkTree($parenthash);
3fda8c4c 1326
044182ef
MO
1327 $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1328
1329 $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
3fda8c4c 1330
3fda8c4c 1331 my @committedfiles = ();
392e2817 1332 my %oldmeta;
3fda8c4c 1333
addf88e4 1334 # foreach file specified on the command line ...
3fda8c4c
ML
1335 foreach my $filename ( @{$state->{args}} )
1336 {
7d90095a 1337 my $committedfile = $filename;
3fda8c4c
ML
1338 $filename = filecleanup($filename);
1339
1340 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1341
1342 my $meta = $updater->getmeta($filename);
392e2817 1343 $oldmeta{$filename} = $meta;
3fda8c4c
ML
1344
1345 my $wrev = revparse($filename);
1346
1347 my ( $filepart, $dirpart ) = filenamesplit($filename);
1348
cdf63284 1349 # do a checkout of the file if it is part of this tree
3fda8c4c 1350 if ($wrev) {
d2feb01a 1351 system('git', 'checkout-index', '-f', '-u', $filename);
3fda8c4c
ML
1352 unless ($? == 0) {
1353 die "Error running git-checkout-index -f -u $filename : $!";
1354 }
1355 }
1356
1357 my $addflag = 0;
1358 my $rmflag = 0;
1359 $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
1360 $addflag = 1 unless ( -e $filename );
1361
1362 # Do up to date checking
1363 unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
1364 {
1365 # fail everything if an up to date check fails
1366 print "error 1 Up to date check failed for $filename\n";
044182ef 1367 cleanupWorkTree();
3fda8c4c
ML
1368 exit;
1369 }
1370
7d90095a 1371 push @committedfiles, $committedfile;
3fda8c4c
ML
1372 $log->info("Committing $filename");
1373
1374 system("mkdir","-p",$dirpart) unless ( -d $dirpart );
1375
1376 unless ( $rmflag )
1377 {
1378 $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename");
1379 rename $state->{entries}{$filename}{modified_filename},$filename;
1380
1381 # Calculate modes to remove
1382 my $invmode = "";
1383 foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); }
1384
1385 $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename");
1386 system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename);
1387 }
1388
1389 if ( $rmflag )
1390 {
1391 $log->info("Removing file '$filename'");
1392 unlink($filename);
d2feb01a 1393 system("git", "update-index", "--remove", $filename);
3fda8c4c
ML
1394 }
1395 elsif ( $addflag )
1396 {
1397 $log->info("Adding file '$filename'");
d2feb01a 1398 system("git", "update-index", "--add", $filename);
3fda8c4c
ML
1399 } else {
1400 $log->info("Updating file '$filename'");
d2feb01a 1401 system("git", "update-index", $filename);
3fda8c4c
ML
1402 }
1403 }
1404
1405 unless ( scalar(@committedfiles) > 0 )
1406 {
1407 print "E No files to commit\n";
1408 print "ok\n";
044182ef 1409 cleanupWorkTree();
3fda8c4c
ML
1410 return;
1411 }
1412
d2feb01a 1413 my $treehash = `git write-tree`;
3fda8c4c 1414 chomp $treehash;
3fda8c4c
ML
1415
1416 $log->debug("Treehash : $treehash, Parenthash : $parenthash");
1417
1418 # write our commit message out if we have one ...
1419 my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR );
1420 print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) );
280514e1
FE
1421 if ( defined ( $cfg->{gitcvs}{commitmsgannotation} ) ) {
1422 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/ ) {
1423 print $msg_fh "\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"
1424 }
1425 } else {
1426 print $msg_fh "\n\nvia git-CVS emulator\n";
1427 }
3fda8c4c
ML
1428 close $msg_fh;
1429
d2feb01a 1430 my $commithash = `git commit-tree $treehash -p $parenthash < $msg_filename`;
1872adab 1431 chomp($commithash);
3fda8c4c
ML
1432 $log->info("Commit hash : $commithash");
1433
1434 unless ( $commithash =~ /[a-zA-Z0-9]{40}/ )
1435 {
1436 $log->warn("Commit failed (Invalid commit hash)");
1437 print "error 1 Commit failed (unknown reason)\n";
044182ef 1438 cleanupWorkTree();
3fda8c4c
ML
1439 exit;
1440 }
1441
cdf63284
MW
1442 ### Emulate git-receive-pack by running hooks/update
1443 my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
b2741f63 1444 $parenthash, $commithash );
cdf63284
MW
1445 if( -x $hook[0] ) {
1446 unless( system( @hook ) == 0 )
b2741f63
AP
1447 {
1448 $log->warn("Commit failed (update hook declined to update ref)");
1449 print "error 1 Commit failed (update hook declined)\n";
044182ef 1450 cleanupWorkTree();
b2741f63
AP
1451 exit;
1452 }
1453 }
1454
cdf63284 1455 ### Update the ref
ada5ef3b
JH
1456 if (system(qw(git update-ref -m), "cvsserver ci",
1457 "refs/heads/$state->{module}", $commithash, $parenthash)) {
1458 $log->warn("update-ref for $state->{module} failed.");
1459 print "error 1 Cannot commit -- update first\n";
044182ef 1460 cleanupWorkTree();
ada5ef3b
JH
1461 exit;
1462 }
3fda8c4c 1463
cdf63284
MW
1464 ### Emulate git-receive-pack by running hooks/post-receive
1465 my $hook = $ENV{GIT_DIR}.'hooks/post-receive';
1466 if( -x $hook ) {
1467 open(my $pipe, "| $hook") || die "can't fork $!";
1468
1469 local $SIG{PIPE} = sub { die 'pipe broke' };
1470
1471 print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
1472
1473 close $pipe || die "bad pipe: $! $?";
1474 }
1475
ad8c3477
SK
1476 $updater->update();
1477
394d66d4
JH
1478 ### Then hooks/post-update
1479 $hook = $ENV{GIT_DIR}.'hooks/post-update';
1480 if (-x $hook) {
1481 system($hook, "refs/heads/$state->{module}");
1482 }
1483
addf88e4 1484 # foreach file specified on the command line ...
3fda8c4c
ML
1485 foreach my $filename ( @committedfiles )
1486 {
1487 $filename = filecleanup($filename);
1488
1489 my $meta = $updater->getmeta($filename);
3486595b
ML
1490 unless (defined $meta->{revision}) {
1491 $meta->{revision} = 1;
1492 }
3fda8c4c 1493
7d90095a 1494 my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
3fda8c4c
ML
1495
1496 $log->debug("Checked-in $dirpart : $filename");
1497
392e2817 1498 print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
3486595b 1499 if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
3fda8c4c 1500 {
392e2817 1501 print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
3fda8c4c
ML
1502 print "Remove-entry $dirpart\n";
1503 print "$filename\n";
1504 } else {
459bad77
FL
1505 if ($meta->{revision} == 1) {
1506 print "M initial revision: 1.1\n";
1507 } else {
392e2817 1508 print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
459bad77 1509 }
3fda8c4c
ML
1510 print "Checked-in $dirpart\n";
1511 print "$filename\n";
90948a42 1512 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
8538e876 1513 print "/$filepart/1.$meta->{revision}//$kopts/\n";
3fda8c4c
ML
1514 }
1515 }
1516
044182ef 1517 cleanupWorkTree();
3fda8c4c
ML
1518 print "ok\n";
1519}
1520
1521sub req_status
1522{
1523 my ( $cmd, $data ) = @_;
1524
1525 argsplit("status");
1526
1527 $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" ));
1528 #$log->debug("status state : " . Dumper($state));
1529
1530 # Grab a handle to the SQLite db and do any necessary updates
1531 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1532 $updater->update();
1533
1534 # if no files were specified, we need to work out what files we should be providing status on ...
7d90095a 1535 argsfromdir($updater);
3fda8c4c 1536
addf88e4 1537 # foreach file specified on the command line ...
3fda8c4c
ML
1538 foreach my $filename ( @{$state->{args}} )
1539 {
1540 $filename = filecleanup($filename);
1541
852b921c
DD
1542 next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0;
1543
3fda8c4c
ML
1544 my $meta = $updater->getmeta($filename);
1545 my $oldmeta = $meta;
1546
1547 my $wrev = revparse($filename);
1548
1549 # If the working copy is an old revision, lets get that version too for comparison.
1550 if ( defined($wrev) and $wrev != $meta->{revision} )
1551 {
1552 $oldmeta = $updater->getmeta($filename, $wrev);
1553 }
1554
1555 # TODO : All possible statuses aren't yet implemented
1556 my $status;
1557 # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1558 $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
1559 and
1560 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1561 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
1562 );
1563
1564 # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
1565 $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
1566 and
1567 ( $state->{entries}{$filename}{unchanged}
1568 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
1569 );
1570
1571 # Need checkout if it exists in the repo but doesn't have a working copy
1572 $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
1573
1574 # Locally modified if working copy and repo copy have the same revision but there are local changes
1575 $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
1576
1577 # Needs Merge if working copy revision is less than repo copy and there are local changes
1578 $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
1579
1580 $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
1581 $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
1582 $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
1583 $status ||= "File had conflicts on merge" if ( 0 );
1584
1585 $status ||= "Unknown";
1586
23b7180f
DD
1587 my ($filepart) = filenamesplit($filename);
1588
3fda8c4c 1589 print "M ===================================================================\n";
23b7180f 1590 print "M File: $filepart\tStatus: $status\n";
3fda8c4c
ML
1591 if ( defined($state->{entries}{$filename}{revision}) )
1592 {
1593 print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
1594 } else {
1595 print "M Working revision:\tNo entry for $filename\n";
1596 }
1597 if ( defined($meta->{revision}) )
1598 {
392e2817 1599 print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
3fda8c4c
ML
1600 print "M Sticky Tag:\t\t(none)\n";
1601 print "M Sticky Date:\t\t(none)\n";
1602 print "M Sticky Options:\t\t(none)\n";
1603 } else {
1604 print "M Repository revision:\tNo revision control file\n";
1605 }
1606 print "M\n";
1607 }
1608
1609 print "ok\n";
1610}
1611
1612sub req_diff
1613{
1614 my ( $cmd, $data ) = @_;
1615
1616 argsplit("diff");
1617
1618 $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" ));
1619 #$log->debug("status state : " . Dumper($state));
1620
1621 my ($revision1, $revision2);
1622 if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" )
1623 {
1624 $revision1 = $state->{opt}{r}[0];
1625 $revision2 = $state->{opt}{r}[1];
1626 } else {
1627 $revision1 = $state->{opt}{r};
1628 }
1629
1630 $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
1631 $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
1632
1633 $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
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 ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
1648
1649 my $wrev = revparse($filename);
1650
1651 # We need _something_ to diff against
1652 next unless ( defined ( $wrev ) );
1653
1654 # if we have a -r switch, use it
1655 if ( defined ( $revision1 ) )
1656 {
1657 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1658 $meta1 = $updater->getmeta($filename, $revision1);
1659 unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
1660 {
1661 print "E File $filename at revision 1.$revision1 doesn't exist\n";
1662 next;
1663 }
e78f69a3 1664 transmitfile($meta1->{filehash}, { targetfile => $file1 });
3fda8c4c
ML
1665 }
1666 # otherwise we just use the working copy revision
1667 else
1668 {
1669 ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1670 $meta1 = $updater->getmeta($filename, $wrev);
e78f69a3 1671 transmitfile($meta1->{filehash}, { targetfile => $file1 });
3fda8c4c
ML
1672 }
1673
1674 # if we have a second -r switch, use it too
1675 if ( defined ( $revision2 ) )
1676 {
1677 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1678 $meta2 = $updater->getmeta($filename, $revision2);
1679
1680 unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1681 {
1682 print "E File $filename at revision 1.$revision2 doesn't exist\n";
1683 next;
1684 }
1685
e78f69a3 1686 transmitfile($meta2->{filehash}, { targetfile => $file2 });
3fda8c4c
ML
1687 }
1688 # otherwise we just use the working copy
1689 else
1690 {
1691 $file2 = $state->{entries}{$filename}{modified_filename};
1692 }
1693
1694 # if we have been given -r, and we don't have a $file2 yet, lets get one
1695 if ( defined ( $revision1 ) and not defined ( $file2 ) )
1696 {
1697 ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1698 $meta2 = $updater->getmeta($filename, $wrev);
e78f69a3 1699 transmitfile($meta2->{filehash}, { targetfile => $file2 });
3fda8c4c
ML
1700 }
1701
1702 # We need to have retrieved something useful
1703 next unless ( defined ( $meta1 ) );
1704
1705 # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
1706 next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
1707 and
1708 ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
1709 or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
1710 );
1711
1712 # Apparently we only show diffs for locally modified files
1713 next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
1714
1715 print "M Index: $filename\n";
1716 print "M ===================================================================\n";
1717 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1718 print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
1719 print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
1720 print "M diff ";
1721 foreach my $opt ( keys %{$state->{opt}} )
1722 {
1723 if ( ref $state->{opt}{$opt} eq "ARRAY" )
1724 {
1725 foreach my $value ( @{$state->{opt}{$opt}} )
1726 {
1727 print "-$opt $value ";
1728 }
1729 } else {
1730 print "-$opt ";
1731 print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
1732 }
1733 }
1734 print "$filename\n";
1735
1736 $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
1737
1738 ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1739
1740 if ( exists $state->{opt}{u} )
1741 {
1742 system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
1743 } else {
1744 system("diff $file1 $file2 > $filediff");
1745 }
1746
1747 while ( <$fh> )
1748 {
1749 print "M $_";
1750 }
1751 close $fh;
1752 }
1753
1754 print "ok\n";
1755}
1756
1757sub req_log
1758{
1759 my ( $cmd, $data ) = @_;
1760
1761 argsplit("log");
1762
1763 $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
1764 #$log->debug("log state : " . Dumper($state));
1765
1766 my ( $minrev, $maxrev );
1767 if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
1768 {
1769 my $control = $2;
1770 $minrev = $1;
1771 $maxrev = $3;
1772 $minrev =~ s/^1\.// if ( defined ( $minrev ) );
1773 $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
1774 $minrev++ if ( defined($minrev) and $control eq "::" );
1775 }
1776
1777 # Grab a handle to the SQLite db and do any necessary updates
1778 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1779 $updater->update();
1780
1781 # if no files were specified, we need to work out what files we should be providing status on ...
7d90095a 1782 argsfromdir($updater);
3fda8c4c 1783
addf88e4 1784 # foreach file specified on the command line ...
3fda8c4c
ML
1785 foreach my $filename ( @{$state->{args}} )
1786 {
1787 $filename = filecleanup($filename);
1788
1789 my $headmeta = $updater->getmeta($filename);
1790
1791 my $revisions = $updater->getlog($filename);
1792 my $totalrevisions = scalar(@$revisions);
1793
1794 if ( defined ( $minrev ) )
1795 {
1796 $log->debug("Removing revisions less than $minrev");
1797 while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
1798 {
1799 pop @$revisions;
1800 }
1801 }
1802 if ( defined ( $maxrev ) )
1803 {
1804 $log->debug("Removing revisions greater than $maxrev");
1805 while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
1806 {
1807 shift @$revisions;
1808 }
1809 }
1810
1811 next unless ( scalar(@$revisions) );
1812
1813 print "M \n";
1814 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1815 print "M Working file: $filename\n";
1816 print "M head: 1.$headmeta->{revision}\n";
1817 print "M branch:\n";
1818 print "M locks: strict\n";
1819 print "M access list:\n";
1820 print "M symbolic names:\n";
1821 print "M keyword substitution: kv\n";
1822 print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
1823 print "M description:\n";
1824
1825 foreach my $revision ( @$revisions )
1826 {
1827 print "M ----------------------------\n";
1828 print "M revision 1.$revision->{revision}\n";
1829 # reformat the date for log output
1830 $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}) );
c1bc3061 1831 $revision->{author} = cvs_author($revision->{author});
3fda8c4c
ML
1832 print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
1833 my $commitmessage = $updater->commitmessage($revision->{commithash});
1834 $commitmessage =~ s/^/M /mg;
1835 print $commitmessage . "\n";
1836 }
1837 print "M =============================================================================\n";
1838 }
1839
1840 print "ok\n";
1841}
1842
1843sub req_annotate
1844{
1845 my ( $cmd, $data ) = @_;
1846
1847 argsplit("annotate");
1848
1849 $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" ));
1850 #$log->debug("status state : " . Dumper($state));
1851
1852 # Grab a handle to the SQLite db and do any necessary updates
1853 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1854 $updater->update();
1855
1856 # if no files were specified, we need to work out what files we should be providing annotate on ...
7d90095a 1857 argsfromdir($updater);
3fda8c4c
ML
1858
1859 # we'll need a temporary checkout dir
044182ef 1860 setupWorkTree();
3fda8c4c 1861
044182ef 1862 $log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");
3fda8c4c 1863
addf88e4 1864 # foreach file specified on the command line ...
3fda8c4c
ML
1865 foreach my $filename ( @{$state->{args}} )
1866 {
1867 $filename = filecleanup($filename);
1868
1869 my $meta = $updater->getmeta($filename);
1870
1871 next unless ( $meta->{revision} );
1872
1873 # get all the commits that this file was in
1874 # in dense format -- aka skip dead revisions
1875 my $revisions = $updater->gethistorydense($filename);
1876 my $lastseenin = $revisions->[0][2];
1877
1878 # populate the temporary index based on the latest commit were we saw
1879 # the file -- but do it cheaply without checking out any files
1880 # TODO: if we got a revision from the client, use that instead
1881 # to look up the commithash in sqlite (still good to default to
1882 # the current head as we do now)
d2feb01a 1883 system("git", "read-tree", $lastseenin);
3fda8c4c
ML
1884 unless ($? == 0)
1885 {
044182ef 1886 print "E error running git-read-tree $lastseenin $ENV{GIT_INDEX_FILE} $!\n";
a5e40798 1887 return;
3fda8c4c 1888 }
044182ef 1889 $log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit $lastseenin - exit status $?");
3fda8c4c
ML
1890
1891 # do a checkout of the file
d2feb01a 1892 system('git', 'checkout-index', '-f', '-u', $filename);
3fda8c4c 1893 unless ($? == 0) {
a5e40798
JM
1894 print "E error running git-checkout-index -f -u $filename : $!\n";
1895 return;
3fda8c4c
ML
1896 }
1897
1898 $log->info("Annotate $filename");
1899
1900 # Prepare a file with the commits from the linearized
1901 # history that annotate should know about. This prevents
1902 # git-jsannotate telling us about commits we are hiding
1903 # from the client.
1904
044182ef 1905 my $a_hints = "$work->{workDir}/.annotate_hints";
a5e40798
JM
1906 if (!open(ANNOTATEHINTS, '>', $a_hints)) {
1907 print "E failed to open '$a_hints' for writing: $!\n";
1908 return;
1909 }
3fda8c4c
ML
1910 for (my $i=0; $i < @$revisions; $i++)
1911 {
1912 print ANNOTATEHINTS $revisions->[$i][2];
1913 if ($i+1 < @$revisions) { # have we got a parent?
1914 print ANNOTATEHINTS ' ' . $revisions->[$i+1][2];
1915 }
1916 print ANNOTATEHINTS "\n";
1917 }
1918
1919 print ANNOTATEHINTS "\n";
a5e40798
JM
1920 close ANNOTATEHINTS
1921 or (print "E failed to write $a_hints: $!\n"), return;
3fda8c4c 1922
d2feb01a 1923 my @cmd = (qw(git annotate -l -S), $a_hints, $filename);
a5e40798
JM
1924 if (!open(ANNOTATE, "-|", @cmd)) {
1925 print "E error invoking ". join(' ',@cmd) .": $!\n";
1926 return;
1927 }
3fda8c4c
ML
1928 my $metadata = {};
1929 print "E Annotations for $filename\n";
1930 print "E ***************\n";
1931 while ( <ANNOTATE> )
1932 {
1933 if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)
1934 {
1935 my $commithash = $1;
1936 my $data = $2;
1937 unless ( defined ( $metadata->{$commithash} ) )
1938 {
1939 $metadata->{$commithash} = $updater->getmeta($filename, $commithash);
c1bc3061 1940 $metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
3fda8c4c
ML
1941 $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
1942 }
1943 printf("M 1.%-5d (%-8s %10s): %s\n",
1944 $metadata->{$commithash}{revision},
1945 $metadata->{$commithash}{author},
1946 $metadata->{$commithash}{modified},
1947 $data
1948 );
1949 } else {
1950 $log->warn("Error in annotate output! LINE: $_");
1951 print "E Annotate error \n";
1952 next;
1953 }
1954 }
1955 close ANNOTATE;
1956 }
1957
1958 # done; get out of the tempdir
df4b3abc 1959 cleanupWorkTree();
3fda8c4c
ML
1960
1961 print "ok\n";
1962
1963}
1964
1965# This method takes the state->{arguments} array and produces two new arrays.
1966# The first is $state->{args} which is everything before the '--' argument, and
1967# the second is $state->{files} which is everything after it.
1968sub argsplit
1969{
3fda8c4c
ML
1970 $state->{args} = [];
1971 $state->{files} = [];
1972 $state->{opt} = {};
1973
1e76b702
FL
1974 return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" );
1975
1976 my $type = shift;
1977
3fda8c4c
ML
1978 if ( defined($type) )
1979 {
1980 my $opt = {};
1981 $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" );
1982 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
1983 $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" );
1984 $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
1985 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
1986 $opt = { k => 1, m => 1 } if ( $type eq "add" );
1987 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
1988 $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" );
1989
1990
1991 while ( scalar ( @{$state->{arguments}} ) > 0 )
1992 {
1993 my $arg = shift @{$state->{arguments}};
1994
1995 next if ( $arg eq "--" );
1996 next unless ( $arg =~ /\S/ );
1997
1998 # if the argument looks like a switch
1999 if ( $arg =~ /^-(\w)(.*)/ )
2000 {
2001 # if it's a switch that takes an argument
2002 if ( $opt->{$1} )
2003 {
2004 # If this switch has already been provided
2005 if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) )
2006 {
2007 $state->{opt}{$1} = [ $state->{opt}{$1} ];
2008 if ( length($2) > 0 )
2009 {
2010 push @{$state->{opt}{$1}},$2;
2011 } else {
2012 push @{$state->{opt}{$1}}, shift @{$state->{arguments}};
2013 }
2014 } else {
2015 # if there's extra data in the arg, use that as the argument for the switch
2016 if ( length($2) > 0 )
2017 {
2018 $state->{opt}{$1} = $2;
2019 } else {
2020 $state->{opt}{$1} = shift @{$state->{arguments}};
2021 }
2022 }
2023 } else {
2024 $state->{opt}{$1} = undef;
2025 }
2026 }
2027 else
2028 {
2029 push @{$state->{args}}, $arg;
2030 }
2031 }
2032 }
2033 else
2034 {
2035 my $mode = 0;
2036
2037 foreach my $value ( @{$state->{arguments}} )
2038 {
2039 if ( $value eq "--" )
2040 {
2041 $mode++;
2042 next;
2043 }
2044 push @{$state->{args}}, $value if ( $mode == 0 );
2045 push @{$state->{files}}, $value if ( $mode == 1 );
2046 }
2047 }
2048}
2049
2050# This method uses $state->{directory} to populate $state->{args} with a list of filenames
2051sub argsfromdir
2052{
2053 my $updater = shift;
2054
7d90095a
MS
2055 $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
2056
82000d74 2057 return if ( scalar ( @{$state->{args}} ) > 1 );
7d90095a 2058
0a7a9a12
JS
2059 my @gethead = @{$updater->gethead};
2060
2061 # push added files
2062 foreach my $file (keys %{$state->{entries}}) {
2063 if ( exists $state->{entries}{$file}{revision} &&
2064 $state->{entries}{$file}{revision} == 0 )
2065 {
2066 push @gethead, { name => $file, filehash => 'added' };
2067 }
2068 }
2069
82000d74
MS
2070 if ( scalar(@{$state->{args}}) == 1 )
2071 {
2072 my $arg = $state->{args}[0];
2073 $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
7d90095a 2074
82000d74 2075 $log->info("Only one arg specified, checking for directory expansion on '$arg'");
3fda8c4c 2076
0a7a9a12 2077 foreach my $file ( @gethead )
82000d74
MS
2078 {
2079 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2080 next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
2081 push @{$state->{args}}, $file->{name};
2082 }
2083
2084 shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
2085 } else {
2086 $log->info("Only one arg specified, populating file list automatically");
2087
2088 $state->{args} = [];
2089
0a7a9a12 2090 foreach my $file ( @gethead )
82000d74
MS
2091 {
2092 next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
2093 next unless ( $file->{name} =~ s/^$state->{prependdir}// );
2094 push @{$state->{args}}, $file->{name};
2095 }
3fda8c4c
ML
2096 }
2097}
2098
2099# This method cleans up the $state variable after a command that uses arguments has run
2100sub statecleanup
2101{
2102 $state->{files} = [];
2103 $state->{args} = [];
2104 $state->{arguments} = [];
2105 $state->{entries} = {};
2106}
2107
2108sub revparse
2109{
2110 my $filename = shift;
2111
2112 return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
2113
2114 return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
2115 return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
2116
2117 return undef;
2118}
2119
e78f69a3
DD
2120# This method takes a file hash and does a CVS "file transfer". Its
2121# exact behaviour depends on a second, optional hash table argument:
2122# - If $options->{targetfile}, dump the contents to that file;
2123# - If $options->{print}, use M/MT to transmit the contents one line
2124# at a time;
2125# - Otherwise, transmit the size of the file, followed by the file
2126# contents.
3fda8c4c
ML
2127sub transmitfile
2128{
2129 my $filehash = shift;
e78f69a3 2130 my $options = shift;
3fda8c4c
ML
2131
2132 if ( defined ( $filehash ) and $filehash eq "deleted" )
2133 {
2134 $log->warn("filehash is 'deleted'");
2135 return;
2136 }
2137
2138 die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ );
2139
d2feb01a 2140 my $type = `git cat-file -t $filehash`;
3fda8c4c
ML
2141 chomp $type;
2142
2143 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" );
2144
d2feb01a 2145 my $size = `git cat-file -s $filehash`;
3fda8c4c
ML
2146 chomp $size;
2147
2148 $log->debug("transmitfile($filehash) size=$size, type=$type");
2149
d2feb01a 2150 if ( open my $fh, '-|', "git", "cat-file", "blob", $filehash )
3fda8c4c 2151 {
e78f69a3 2152 if ( defined ( $options->{targetfile} ) )
3fda8c4c 2153 {
e78f69a3 2154 my $targetfile = $options->{targetfile};
3fda8c4c
ML
2155 open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!");
2156 print NEWFILE $_ while ( <$fh> );
a5e40798 2157 close NEWFILE or die("Failed to write '$targetfile': $!");
e78f69a3
DD
2158 } elsif ( defined ( $options->{print} ) && $options->{print} ) {
2159 while ( <$fh> ) {
2160 if( /\n\z/ ) {
2161 print 'M ', $_;
2162 } else {
2163 print 'MT text ', $_, "\n";
2164 }
2165 }
3fda8c4c
ML
2166 } else {
2167 print "$size\n";
2168 print while ( <$fh> );
2169 }
a5e40798 2170 close $fh or die ("Couldn't close filehandle for transmitfile(): $!");
3fda8c4c
ML
2171 } else {
2172 die("Couldn't execute git-cat-file");
2173 }
2174}
2175
2176# This method takes a file name, and returns ( $dirpart, $filepart ) which
5348b6e7 2177# refers to the directory portion and the file portion of the filename
3fda8c4c
ML
2178# respectively
2179sub filenamesplit
2180{
2181 my $filename = shift;
7d90095a 2182 my $fixforlocaldir = shift;
3fda8c4c
ML
2183
2184 my ( $filepart, $dirpart ) = ( $filename, "." );
2185 ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ );
2186 $dirpart .= "/";
2187
7d90095a
MS
2188 if ( $fixforlocaldir )
2189 {
2190 $dirpart =~ s/^$state->{prependdir}//;
2191 }
2192
3fda8c4c
ML
2193 return ( $filepart, $dirpart );
2194}
2195
2196sub filecleanup
2197{
2198 my $filename = shift;
2199
2200 return undef unless(defined($filename));
2201 if ( $filename =~ /^\// )
2202 {
2203 print "E absolute filenames '$filename' not supported by server\n";
2204 return undef;
2205 }
2206
2207 $filename =~ s/^\.\///g;
82000d74 2208 $filename = $state->{prependdir} . $filename;
3fda8c4c
ML
2209 return $filename;
2210}
2211
044182ef
MO
2212sub validateGitDir
2213{
2214 if( !defined($state->{CVSROOT}) )
2215 {
2216 print "error 1 CVSROOT not specified\n";
2217 cleanupWorkTree();
2218 exit;
2219 }
2220 if( $ENV{GIT_DIR} ne ($state->{CVSROOT} . '/') )
2221 {
2222 print "error 1 Internally inconsistent CVSROOT\n";
2223 cleanupWorkTree();
2224 exit;
2225 }
2226}
2227
2228# Setup working directory in a work tree with the requested version
2229# loaded in the index.
2230sub setupWorkTree
2231{
2232 my ($ver) = @_;
2233
2234 validateGitDir();
2235
2236 if( ( defined($work->{state}) && $work->{state} != 1 ) ||
2237 defined($work->{tmpDir}) )
2238 {
2239 $log->warn("Bad work tree state management");
2240 print "error 1 Internal setup multiple work trees without cleanup\n";
2241 cleanupWorkTree();
2242 exit;
2243 }
2244
2245 $work->{workDir} = tempdir ( DIR => $TEMP_DIR );
2246
2247 if( !defined($work->{index}) )
2248 {
2249 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2250 }
2251
2252 chdir $work->{workDir} or
2253 die "Unable to chdir to $work->{workDir}\n";
2254
2255 $log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");
2256
2257 $ENV{GIT_WORK_TREE} = ".";
2258 $ENV{GIT_INDEX_FILE} = $work->{index};
2259 $work->{state} = 2;
2260
2261 if($ver)
2262 {
2263 system("git","read-tree",$ver);
2264 unless ($? == 0)
2265 {
2266 $log->warn("Error running git-read-tree");
2267 die "Error running git-read-tree $ver in $work->{workDir} $!\n";
2268 }
2269 }
2270 # else # req_annotate reads tree for each file
2271}
2272
2273# Ensure current directory is in some kind of working directory,
2274# with a recent version loaded in the index.
2275sub ensureWorkTree
2276{
2277 if( defined($work->{tmpDir}) )
2278 {
2279 $log->warn("Bad work tree state management [ensureWorkTree()]");
2280 print "error 1 Internal setup multiple dirs without cleanup\n";
2281 cleanupWorkTree();
2282 exit;
2283 }
2284 if( $work->{state} )
2285 {
2286 return;
2287 }
2288
2289 validateGitDir();
2290
2291 if( !defined($work->{emptyDir}) )
2292 {
2293 $work->{emptyDir} = tempdir ( DIR => $TEMP_DIR, OPEN => 0);
2294 }
2295 chdir $work->{emptyDir} or
2296 die "Unable to chdir to $work->{emptyDir}\n";
2297
2298 my $ver = `git show-ref -s refs/heads/$state->{module}`;
2299 chomp $ver;
2300 if ($ver !~ /^[0-9a-f]{40}$/)
2301 {
2302 $log->warn("Error from git show-ref -s refs/head$state->{module}");
2303 print "error 1 cannot find the current HEAD of module";
2304 cleanupWorkTree();
2305 exit;
2306 }
2307
2308 if( !defined($work->{index}) )
2309 {
2310 (undef, $work->{index}) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 );
2311 }
2312
2313 $ENV{GIT_WORK_TREE} = ".";
2314 $ENV{GIT_INDEX_FILE} = $work->{index};
2315 $work->{state} = 1;
2316
2317 system("git","read-tree",$ver);
2318 unless ($? == 0)
2319 {
2320 die "Error running git-read-tree $ver $!\n";
2321 }
2322}
2323
2324# Cleanup working directory that is not needed any longer.
2325sub cleanupWorkTree
2326{
2327 if( ! $work->{state} )
2328 {
2329 return;
2330 }
2331
2332 chdir "/" or die "Unable to chdir '/'\n";
2333
2334 if( defined($work->{workDir}) )
2335 {
2336 rmtree( $work->{workDir} );
2337 undef $work->{workDir};
2338 }
2339 undef $work->{state};
2340}
2341
2342# Setup a temporary directory (not a working tree), typically for
2343# merging dirty state as in req_update.
2344sub setupTmpDir
2345{
2346 $work->{tmpDir} = tempdir ( DIR => $TEMP_DIR );
2347 chdir $work->{tmpDir} or die "Unable to chdir $work->{tmpDir}\n";
2348
2349 return $work->{tmpDir};
2350}
2351
2352# Clean up a previously setupTmpDir. Restore previous work tree if
2353# appropriate.
2354sub cleanupTmpDir
2355{
2356 if ( !defined($work->{tmpDir}) )
2357 {
2358 $log->warn("cleanup tmpdir that has not been setup");
2359 die "Cleanup tmpDir that has not been setup\n";
2360 }
2361 if( defined($work->{state}) )
2362 {
2363 if( $work->{state} == 1 )
2364 {
2365 chdir $work->{emptyDir} or
2366 die "Unable to chdir to $work->{emptyDir}\n";
2367 }
2368 elsif( $work->{state} == 2 )
2369 {
2370 chdir $work->{workDir} or
2371 die "Unable to chdir to $work->{emptyDir}\n";
2372 }
2373 else
2374 {
2375 $log->warn("Inconsistent work dir state");
2376 die "Inconsistent work dir state\n";
2377 }
2378 }
2379 else
2380 {
2381 chdir "/" or die "Unable to chdir '/'\n";
2382 }
2383}
2384
8538e876
AP
2385# Given a path, this function returns a string containing the kopts
2386# that should go into that path's Entries line. For example, a binary
2387# file should get -kb.
2388sub kopts_from_path
2389{
90948a42 2390 my ($path, $srcType, $name) = @_;
8538e876 2391
8a06a632
MO
2392 if ( defined ( $cfg->{gitcvs}{usecrlfattr} ) and
2393 $cfg->{gitcvs}{usecrlfattr} =~ /\s*(1|true|yes)\s*$/i )
2394 {
2395 my ($val) = check_attr( "crlf", $path );
2396 if ( $val eq "set" )
2397 {
2398 return "";
2399 }
2400 elsif ( $val eq "unset" )
2401 {
2402 return "-kb"
2403 }
2404 else
2405 {
2406 $log->info("Unrecognized check_attr crlf $path : $val");
2407 }
2408 }
8538e876 2409
90948a42 2410 if ( defined ( $cfg->{gitcvs}{allbinary} ) )
8538e876 2411 {
90948a42
MO
2412 if( ($cfg->{gitcvs}{allbinary} =~ /^\s*(1|true|yes)\s*$/i) )
2413 {
2414 return "-kb";
2415 }
2416 elsif( ($cfg->{gitcvs}{allbinary} =~ /^\s*guess\s*$/i) )
2417 {
2418 if( $srcType eq "sha1Or-k" &&
2419 !defined($name) )
2420 {
2421 my ($ret)=$state->{entries}{$path}{options};
2422 if( !defined($ret) )
2423 {
2424 $ret=$state->{opt}{k};
2425 if(defined($ret))
2426 {
2427 $ret="-k$ret";
2428 }
2429 else
2430 {
2431 $ret="";
2432 }
2433 }
2434 if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) )
2435 {
2436 print "E Bad -k option\n";
2437 $log->warn("Bad -k option: $ret");
2438 die "Error: Bad -k option: $ret\n";
2439 }
2440
2441 return $ret;
2442 }
2443 else
2444 {
2445 if( is_binary($srcType,$name) )
2446 {
2447 $log->debug("... as binary");
2448 return "-kb";
2449 }
2450 else
2451 {
2452 $log->debug("... as text");
2453 }
2454 }
2455 }
8538e876 2456 }
90948a42
MO
2457 # Return "" to give no special treatment to any path
2458 return "";
8538e876
AP
2459}
2460
8a06a632
MO
2461sub check_attr
2462{
2463 my ($attr,$path) = @_;
2464 ensureWorkTree();
2465 if ( open my $fh, '-|', "git", "check-attr", $attr, "--", $path )
2466 {
2467 my $val = <$fh>;
2468 close $fh;
2469 $val =~ s/.*: ([^:\r\n]*)\s*$/$1/;
2470 return $val;
2471 }
2472 else
2473 {
2474 return undef;
2475 }
2476}
2477
90948a42
MO
2478# This should have the same heuristics as convert.c:is_binary() and related.
2479# Note that the bare CR test is done by callers in convert.c.
2480sub is_binary
2481{
2482 my ($srcType,$name) = @_;
2483 $log->debug("is_binary($srcType,$name)");
2484
2485 # Minimize amount of interpreted code run in the inner per-character
2486 # loop for large files, by totalling each character value and
2487 # then analyzing the totals.
2488 my @counts;
2489 my $i;
2490 for($i=0;$i<256;$i++)
2491 {
2492 $counts[$i]=0;
2493 }
2494
2495 my $fh = open_blob_or_die($srcType,$name);
2496 my $line;
2497 while( defined($line=<$fh>) )
2498 {
2499 # Any '\0' and bare CR are considered binary.
2500 if( $line =~ /\0|(\r[^\n])/ )
2501 {
2502 close($fh);
2503 return 1;
2504 }
2505
2506 # Count up each character in the line:
2507 my $len=length($line);
2508 for($i=0;$i<$len;$i++)
2509 {
2510 $counts[ord(substr($line,$i,1))]++;
2511 }
2512 }
2513 close $fh;
2514
2515 # Don't count CR and LF as either printable/nonprintable
2516 $counts[ord("\n")]=0;
2517 $counts[ord("\r")]=0;
2518
2519 # Categorize individual character count into printable and nonprintable:
2520 my $printable=0;
2521 my $nonprintable=0;
2522 for($i=0;$i<256;$i++)
2523 {
2524 if( $i < 32 &&
2525 $i != ord("\b") &&
2526 $i != ord("\t") &&
2527 $i != 033 && # ESC
2528 $i != 014 ) # FF
2529 {
2530 $nonprintable+=$counts[$i];
2531 }
2532 elsif( $i==127 ) # DEL
2533 {
2534 $nonprintable+=$counts[$i];
2535 }
2536 else
2537 {
2538 $printable+=$counts[$i];
2539 }
2540 }
2541
2542 return ($printable >> 7) < $nonprintable;
2543}
2544
2545# Returns open file handle. Possible invocations:
2546# - open_blob_or_die("file",$filename);
2547# - open_blob_or_die("sha1",$filehash);
2548sub open_blob_or_die
2549{
2550 my ($srcType,$name) = @_;
2551 my ($fh);
2552 if( $srcType eq "file" )
2553 {
2554 if( !open $fh,"<",$name )
2555 {
2556 $log->warn("Unable to open file $name: $!");
2557 die "Unable to open file $name: $!\n";
2558 }
2559 }
2560 elsif( $srcType eq "sha1" || $srcType eq "sha1Or-k" )
2561 {
2562 unless ( defined ( $name ) and $name =~ /^[a-zA-Z0-9]{40}$/ )
2563 {
2564 $log->warn("Need filehash");
2565 die "Need filehash\n";
2566 }
2567
2568 my $type = `git cat-file -t $name`;
2569 chomp $type;
2570
2571 unless ( defined ( $type ) and $type eq "blob" )
2572 {
2573 $log->warn("Invalid type '$type' for '$name'");
2574 die ( "Invalid type '$type' (expected 'blob')" )
2575 }
2576
2577 my $size = `git cat-file -s $name`;
2578 chomp $size;
2579
2580 $log->debug("open_blob_or_die($name) size=$size, type=$type");
2581
2582 unless( open $fh, '-|', "git", "cat-file", "blob", $name )
2583 {
2584 $log->warn("Unable to open sha1 $name");
2585 die "Unable to open sha1 $name\n";
2586 }
2587 }
2588 else
2589 {
2590 $log->warn("Unknown type of blob source: $srcType");
2591 die "Unknown type of blob source: $srcType\n";
2592 }
2593 return $fh;
2594}
2595
d500a1ee
FE
2596# Generate a CVS author name from Git author information, by taking the local
2597# part of the email address and replacing characters not in the Portable
2598# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS
2599# Login names are Unix login names, which should be restricted to this
2600# character set.
c1bc3061
DD
2601sub cvs_author
2602{
2603 my $author_line = shift;
d500a1ee
FE
2604 (my $author) = $author_line =~ /<([^@>]*)/;
2605
2606 $author =~ s/[^-a-zA-Z0-9_.]/_/g;
2607 $author =~ s/^-/_/;
c1bc3061
DD
2608
2609 $author;
2610}
2611
031a027a
ÆAB
2612
2613sub descramble
2614{
2615 # This table is from src/scramble.c in the CVS source
2616 my @SHIFTS = (
2617 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
2618 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2619 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87,
2620 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105,
2621 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35,
2622 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56,
2623 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48,
2624 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223,
2625 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,
2626 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,
2627 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,
2628 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,
2629 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,
2630 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,
2631 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,
2632 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152
2633 );
2634 my ($str) = @_;
2635
2636 # This should never happen, the same password format (A) bas been
2637 # used by CVS since the beginning of time
2638 die "invalid password format $1" unless substr($str, 0, 1) eq 'A';
2639
2640 my @str = unpack "C*", substr($str, 1);
2641 my $ret = join '', map { chr $SHIFTS[$_] } @str;
2642 return $ret;
2643}
2644
2645
3fda8c4c
ML
2646package GITCVS::log;
2647
2648####
2649#### Copyright The Open University UK - 2006.
2650####
2651#### Authors: Martyn Smith <martyn@catalyst.net.nz>
2652#### Martin Langhoff <martin@catalyst.net.nz>
2653####
2654####
2655
2656use strict;
2657use warnings;
2658
2659=head1 NAME
2660
2661GITCVS::log
2662
2663=head1 DESCRIPTION
2664
2665This module provides very crude logging with a similar interface to
2666Log::Log4perl
2667
2668=head1 METHODS
2669
2670=cut
2671
2672=head2 new
2673
2674Creates a new log object, optionally you can specify a filename here to
5348b6e7 2675indicate the file to log to. If no log file is specified, you can specify one
3fda8c4c
ML
2676later with method setfile, or indicate you no longer want logging with method
2677nofile.
2678
2679Until one of these methods is called, all log calls will buffer messages ready
2680to write out.
2681
2682=cut
2683sub new
2684{
2685 my $class = shift;
2686 my $filename = shift;
2687
2688 my $self = {};
2689
2690 bless $self, $class;
2691
2692 if ( defined ( $filename ) )
2693 {
2694 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2695 }
2696
2697 return $self;
2698}
2699
2700=head2 setfile
2701
2702This methods takes a filename, and attempts to open that file as the log file.
2703If successful, all buffered data is written out to the file, and any further
2704logging is written directly to the file.
2705
2706=cut
2707sub setfile
2708{
2709 my $self = shift;
2710 my $filename = shift;
2711
2712 if ( defined ( $filename ) )
2713 {
2714 open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!");
2715 }
2716
2717 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2718
2719 while ( my $line = shift @{$self->{buffer}} )
2720 {
2721 print {$self->{fh}} $line;
2722 }
2723}
2724
2725=head2 nofile
2726
2727This method indicates no logging is going to be used. It flushes any entries in
2728the internal buffer, and sets a flag to ensure no further data is put there.
2729
2730=cut
2731sub nofile
2732{
2733 my $self = shift;
2734
2735 $self->{nolog} = 1;
2736
2737 return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" );
2738
2739 $self->{buffer} = [];
2740}
2741
2742=head2 _logopen
2743
2744Internal method. Returns true if the log file is open, false otherwise.
2745
2746=cut
2747sub _logopen
2748{
2749 my $self = shift;
2750
2751 return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" );
2752 return 0;
2753}
2754
2755=head2 debug info warn fatal
2756
2757These four methods are wrappers to _log. They provide the actual interface for
2758logging data.
2759
2760=cut
2761sub debug { my $self = shift; $self->_log("debug", @_); }
2762sub info { my $self = shift; $self->_log("info" , @_); }
2763sub warn { my $self = shift; $self->_log("warn" , @_); }
2764sub fatal { my $self = shift; $self->_log("fatal", @_); }
2765
2766=head2 _log
2767
2768This is an internal method called by the logging functions. It generates a
2769timestamp and pushes the logged line either to file, or internal buffer.
2770
2771=cut
2772sub _log
2773{
2774 my $self = shift;
2775 my $level = shift;
2776
2777 return if ( $self->{nolog} );
2778
2779 my @time = localtime;
2780 my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s",
2781 $time[5] + 1900,
2782 $time[4] + 1,
2783 $time[3],
2784 $time[2],
2785 $time[1],
2786 $time[0],
2787 uc $level,
2788 );
2789
2790 if ( $self->_logopen )
2791 {
2792 print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n";
2793 } else {
2794 push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n";
2795 }
2796}
2797
2798=head2 DESTROY
2799
2800This method simply closes the file handle if one is open
2801
2802=cut
2803sub DESTROY
2804{
2805 my $self = shift;
2806
2807 if ( $self->_logopen )
2808 {
2809 close $self->{fh};
2810 }
2811}
2812
2813package GITCVS::updater;
2814
2815####
2816#### Copyright The Open University UK - 2006.
2817####
2818#### Authors: Martyn Smith <martyn@catalyst.net.nz>
2819#### Martin Langhoff <martin@catalyst.net.nz>
2820####
2821####
2822
2823use strict;
2824use warnings;
2825use DBI;
2826
2827=head1 METHODS
2828
2829=cut
2830
2831=head2 new
2832
2833=cut
2834sub new
2835{
2836 my $class = shift;
2837 my $config = shift;
2838 my $module = shift;
2839 my $log = shift;
2840
2841 die "Need to specify a git repository" unless ( defined($config) and -d $config );
2842 die "Need to specify a module" unless ( defined($module) );
2843
2844 $class = ref($class) || $class;
2845
2846 my $self = {};
2847
2848 bless $self, $class;
2849
6aeeffd1
JE
2850 $self->{valid_tables} = {'revision' => 1,
2851 'revision_ix1' => 1,
2852 'revision_ix2' => 1,
2853 'head' => 1,
2854 'head_ix1' => 1,
2855 'properties' => 1,
2856 'commitmsgs' => 1};
2857
3fda8c4c 2858 $self->{module} = $module;
3fda8c4c
ML
2859 $self->{git_path} = $config . "/";
2860
2861 $self->{log} = $log;
2862
2863 die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
2864
eb1780d4 2865 $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
473937ed 2866 $cfg->{gitcvs}{dbdriver} || "SQLite";
eb1780d4
FL
2867 $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
2868 $cfg->{gitcvs}{dbname} || "%Ggitcvs.%m.sqlite";
2869 $self->{dbuser} = $cfg->{gitcvs}{$state->{method}}{dbuser} ||
2870 $cfg->{gitcvs}{dbuser} || "";
2871 $self->{dbpass} = $cfg->{gitcvs}{$state->{method}}{dbpass} ||
2872 $cfg->{gitcvs}{dbpass} || "";
6aeeffd1
JE
2873 $self->{dbtablenameprefix} = $cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||
2874 $cfg->{gitcvs}{dbtablenameprefix} || "";
eb1780d4
FL
2875 my %mapping = ( m => $module,
2876 a => $state->{method},
2877 u => getlogin || getpwuid($<) || $<,
2878 G => $self->{git_path},
2879 g => mangle_dirname($self->{git_path}),
2880 );
2881 $self->{dbname} =~ s/%([mauGg])/$mapping{$1}/eg;
2882 $self->{dbuser} =~ s/%([mauGg])/$mapping{$1}/eg;
6aeeffd1
JE
2883 $self->{dbtablenameprefix} =~ s/%([mauGg])/$mapping{$1}/eg;
2884 $self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});
eb1780d4 2885
473937ed
FL
2886 die "Invalid char ':' in dbdriver" if $self->{dbdriver} =~ /:/;
2887 die "Invalid char ';' in dbname" if $self->{dbname} =~ /;/;
2888 $self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",
eb1780d4
FL
2889 $self->{dbuser},
2890 $self->{dbpass});
920a449a 2891 die "Error connecting to database\n" unless defined $self->{dbh};
3fda8c4c
ML
2892
2893 $self->{tables} = {};
0cf611a3 2894 foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )
3fda8c4c 2895 {
3fda8c4c
ML
2896 $self->{tables}{$table} = 1;
2897 }
2898
2899 # Construct the revision table if required
6aeeffd1 2900 unless ( $self->{tables}{$self->tablename("revision")} )
3fda8c4c 2901 {
6aeeffd1
JE
2902 my $tablename = $self->tablename("revision");
2903 my $ix1name = $self->tablename("revision_ix1");
2904 my $ix2name = $self->tablename("revision_ix2");
3fda8c4c 2905 $self->{dbh}->do("
6aeeffd1 2906 CREATE TABLE $tablename (
3fda8c4c
ML
2907 name TEXT NOT NULL,
2908 revision INTEGER NOT NULL,
2909 filehash TEXT NOT NULL,
2910 commithash TEXT NOT NULL,
2911 author TEXT NOT NULL,
2912 modified TEXT NOT NULL,
2913 mode TEXT NOT NULL
2914 )
2915 ");
178e015c 2916 $self->{dbh}->do("
6aeeffd1
JE
2917 CREATE INDEX $ix1name
2918 ON $tablename (name,revision)
178e015c
SP
2919 ");
2920 $self->{dbh}->do("
6aeeffd1
JE
2921 CREATE INDEX $ix2name
2922 ON $tablename (name,commithash)
178e015c 2923 ");
3fda8c4c
ML
2924 }
2925
178e015c 2926 # Construct the head table if required
6aeeffd1 2927 unless ( $self->{tables}{$self->tablename("head")} )
3fda8c4c 2928 {
6aeeffd1
JE
2929 my $tablename = $self->tablename("head");
2930 my $ix1name = $self->tablename("head_ix1");
3fda8c4c 2931 $self->{dbh}->do("
6aeeffd1 2932 CREATE TABLE $tablename (
3fda8c4c
ML
2933 name TEXT NOT NULL,
2934 revision INTEGER NOT NULL,
2935 filehash TEXT NOT NULL,
2936 commithash TEXT NOT NULL,
2937 author TEXT NOT NULL,
2938 modified TEXT NOT NULL,
2939 mode TEXT NOT NULL
2940 )
2941 ");
178e015c 2942 $self->{dbh}->do("
6aeeffd1
JE
2943 CREATE INDEX $ix1name
2944 ON $tablename (name)
178e015c 2945 ");
3fda8c4c
ML
2946 }
2947
2948 # Construct the properties table if required
6aeeffd1 2949 unless ( $self->{tables}{$self->tablename("properties")} )
3fda8c4c 2950 {
6aeeffd1 2951 my $tablename = $self->tablename("properties");
3fda8c4c 2952 $self->{dbh}->do("
6aeeffd1 2953 CREATE TABLE $tablename (
3fda8c4c
ML
2954 key TEXT NOT NULL PRIMARY KEY,
2955 value TEXT
2956 )
2957 ");
2958 }
2959
2960 # Construct the commitmsgs table if required
6aeeffd1 2961 unless ( $self->{tables}{$self->tablename("commitmsgs")} )
3fda8c4c 2962 {
6aeeffd1 2963 my $tablename = $self->tablename("commitmsgs");
3fda8c4c 2964 $self->{dbh}->do("
6aeeffd1 2965 CREATE TABLE $tablename (
3fda8c4c
ML
2966 key TEXT NOT NULL PRIMARY KEY,
2967 value TEXT
2968 )
2969 ");
2970 }
2971
2972 return $self;
2973}
2974
6aeeffd1
JE
2975=head2 tablename
2976
2977=cut
2978sub tablename
2979{
2980 my $self = shift;
2981 my $name = shift;
2982
2983 if (exists $self->{valid_tables}{$name}) {
2984 return $self->{dbtablenameprefix} . $name;
2985 } else {
2986 return undef;
2987 }
2988}
2989
3fda8c4c
ML
2990=head2 update
2991
2992=cut
2993sub update
2994{
2995 my $self = shift;
2996
2997 # first lets get the commit list
2998 $ENV{GIT_DIR} = $self->{git_path};
2999
49fb940e
ML
3000 my $commitsha1 = `git rev-parse $self->{module}`;
3001 chomp $commitsha1;
3002
3003 my $commitinfo = `git cat-file commit $self->{module} 2>&1`;
3fda8c4c
ML
3004 unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ )
3005 {
3006 die("Invalid module '$self->{module}'");
3007 }
3008
3009
3010 my $git_log;
3011 my $lastcommit = $self->_get_prop("last_commit");
3012
49fb940e
ML
3013 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
3014 return 1;
3015 }
3016
3fda8c4c
ML
3017 # Start exclusive lock here...
3018 $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";
3019
3020 # TODO: log processing is memory bound
3021 # if we can parse into a 2nd file that is in reverse order
3022 # we can probably do something really efficient
a248c961 3023 my @git_log_params = ('--pretty', '--parents', '--topo-order');
3fda8c4c
ML
3024
3025 if (defined $lastcommit) {
3026 push @git_log_params, "$lastcommit..$self->{module}";
3027 } else {
3028 push @git_log_params, $self->{module};
3029 }
a248c961 3030 # git-rev-list is the backend / plumbing version of git-log
d2feb01a 3031 open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
3fda8c4c
ML
3032
3033 my @commits;
3034
3035 my %commit = ();
3036
3037 while ( <GITLOG> )
3038 {
3039 chomp;
3040 if (m/^commit\s+(.*)$/) {
3041 # on ^commit lines put the just seen commit in the stack
3042 # and prime things for the next one
3043 if (keys %commit) {
3044 my %copy = %commit;
3045 unshift @commits, \%copy;
3046 %commit = ();
3047 }
3048 my @parents = split(m/\s+/, $1);
3049 $commit{hash} = shift @parents;
3050 $commit{parents} = \@parents;
3051 } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
3052 # on rfc822-like lines seen before we see any message,
3053 # lowercase the entry and put it in the hash as key-value
3054 $commit{lc($1)} = $2;
3055 } else {
3056 # message lines - skip initial empty line
3057 # and trim whitespace
3058 if (!exists($commit{message}) && m/^\s*$/) {
3059 # define it to mark the end of headers
3060 $commit{message} = '';
3061 next;
3062 }
3063 s/^\s+//; s/\s+$//; # trim ws
3064 $commit{message} .= $_ . "\n";
3065 }
3066 }
3067 close GITLOG;
3068
3069 unshift @commits, \%commit if ( keys %commit );
3070
3071 # Now all the commits are in the @commits bucket
3072 # ordered by time DESC. for each commit that needs processing,
3073 # determine whether it's following the last head we've seen or if
3074 # it's on its own branch, grab a file list, and add whatever's changed
3075 # NOTE: $lastcommit refers to the last commit from previous run
3076 # $lastpicked is the last commit we picked in this run
3077 my $lastpicked;
3078 my $head = {};
3079 if (defined $lastcommit) {
3080 $lastpicked = $lastcommit;
3081 }
3082
3083 my $committotal = scalar(@commits);
3084 my $commitcount = 0;
3085
3086 # Load the head table into $head (for cached lookups during the update process)
3087 foreach my $file ( @{$self->gethead()} )
3088 {
3089 $head->{$file->{name}} = $file;
3090 }
3091
3092 foreach my $commit ( @commits )
3093 {
3094 $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
3095 if (defined $lastpicked)
3096 {
3097 if (!in_array($lastpicked, @{$commit->{parents}}))
3098 {
3099 # skip, we'll see this delta
3100 # as part of a merge later
3101 # warn "skipping off-track $commit->{hash}\n";
3102 next;
3103 } elsif (@{$commit->{parents}} > 1) {
3104 # it is a merge commit, for each parent that is
3105 # not $lastpicked, see if we can get a log
3106 # from the merge-base to that parent to put it
3107 # in the message as a merge summary.
3108 my @parents = @{$commit->{parents}};
3109 foreach my $parent (@parents) {
3110 # git-merge-base can potentially (but rarely) throw
3111 # several candidate merge bases. let's assume
3112 # that the first one is the best one.
3113 if ($parent eq $lastpicked) {
3114 next;
3115 }
e509db99 3116 my $base = eval {
d2feb01a 3117 safe_pipe_capture('git', 'merge-base',
a5e40798 3118 $lastpicked, $parent);
e509db99
SP
3119 };
3120 # The two branches may not be related at all,
3121 # in which case merge base simply fails to find
3122 # any, but that's Ok.
3123 next if ($@);
3124
3fda8c4c
ML
3125 chomp $base;
3126 if ($base) {
3127 my @merged;
3128 # print "want to log between $base $parent \n";
d2feb01a 3129 open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
a5e40798 3130 or die "Cannot call git-log: $!";
3fda8c4c
ML
3131 my $mergedhash;
3132 while (<GITLOG>) {
3133 chomp;
3134 if (!defined $mergedhash) {
3135 if (m/^commit\s+(.+)$/) {
3136 $mergedhash = $1;
3137 } else {
3138 next;
3139 }
3140 } else {
3141 # grab the first line that looks non-rfc822
3142 # aka has content after leading space
3143 if (m/^\s+(\S.*)$/) {
3144 my $title = $1;
3145 $title = substr($title,0,100); # truncate
3146 unshift @merged, "$mergedhash $title";
3147 undef $mergedhash;
3148 }
3149 }
3150 }
3151 close GITLOG;
3152 if (@merged) {
3153 $commit->{mergemsg} = $commit->{message};
3154 $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
3155 foreach my $summary (@merged) {
3156 $commit->{mergemsg} .= "\t$summary\n";
3157 }
3158 $commit->{mergemsg} .= "\n\n";
3159 # print "Message for $commit->{hash} \n$commit->{mergemsg}";
3160 }
3161 }
3162 }
3163 }
3164 }
3165
3166 # convert the date to CVS-happy format
3167 $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
3168
3169 if ( defined ( $lastpicked ) )
3170 {
d2feb01a 3171 my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
e02cd638 3172 local ($/) = "\0";
3fda8c4c
ML
3173 while ( <FILELIST> )
3174 {
e02cd638
JH
3175 chomp;
3176 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
3177 {
3178 die("Couldn't process git-diff-tree line : $_");
3179 }
e02cd638
JH
3180 my ($mode, $hash, $change) = ($1, $2, $3);
3181 my $name = <FILELIST>;
3182 chomp($name);
3fda8c4c 3183
e02cd638 3184 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
3fda8c4c
ML
3185
3186 my $git_perms = "";
e02cd638
JH
3187 $git_perms .= "r" if ( $mode & 4 );
3188 $git_perms .= "w" if ( $mode & 2 );
3189 $git_perms .= "x" if ( $mode & 1 );
3fda8c4c
ML
3190 $git_perms = "rw" if ( $git_perms eq "" );
3191
e02cd638 3192 if ( $change eq "D" )
3fda8c4c 3193 {
e02cd638
JH
3194 #$log->debug("DELETE $name");
3195 $head->{$name} = {
3196 name => $name,
3197 revision => $head->{$name}{revision} + 1,
3fda8c4c
ML
3198 filehash => "deleted",
3199 commithash => $commit->{hash},
3200 modified => $commit->{date},
3201 author => $commit->{author},
3202 mode => $git_perms,
3203 };
e02cd638 3204 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c 3205 }
9027efed 3206 elsif ( $change eq "M" || $change eq "T" )
3fda8c4c 3207 {
e02cd638
JH
3208 #$log->debug("MODIFIED $name");
3209 $head->{$name} = {
3210 name => $name,
3211 revision => $head->{$name}{revision} + 1,
3212 filehash => $hash,
3fda8c4c
ML
3213 commithash => $commit->{hash},
3214 modified => $commit->{date},
3215 author => $commit->{author},
3216 mode => $git_perms,
3217 };
e02cd638 3218 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c 3219 }
e02cd638 3220 elsif ( $change eq "A" )
3fda8c4c 3221 {
e02cd638
JH
3222 #$log->debug("ADDED $name");
3223 $head->{$name} = {
3224 name => $name,
a7da9adb 3225 revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
e02cd638 3226 filehash => $hash,
3fda8c4c
ML
3227 commithash => $commit->{hash},
3228 modified => $commit->{date},
3229 author => $commit->{author},
3230 mode => $git_perms,
3231 };
e02cd638 3232 $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c
ML
3233 }
3234 else
3235 {
e02cd638 3236 $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
3fda8c4c
ML
3237 die;
3238 }
3239 }
3240 close FILELIST;
3241 } else {
3242 # this is used to detect files removed from the repo
3243 my $seen_files = {};
3244
d2feb01a 3245 my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
e02cd638 3246 local $/ = "\0";
3fda8c4c
ML
3247 while ( <FILELIST> )
3248 {
e02cd638
JH
3249 chomp;
3250 unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
3fda8c4c
ML
3251 {
3252 die("Couldn't process git-ls-tree line : $_");
3253 }
3254
3255 my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
3256
3257 $seen_files->{$git_filename} = 1;
3258
3259 my ( $oldhash, $oldrevision, $oldmode ) = (
3260 $head->{$git_filename}{filehash},
3261 $head->{$git_filename}{revision},
3262 $head->{$git_filename}{mode}
3263 );
3264
3265 if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
3266 {
3267 $git_perms = "";
3268 $git_perms .= "r" if ( $1 & 4 );
3269 $git_perms .= "w" if ( $1 & 2 );
3270 $git_perms .= "x" if ( $1 & 1 );
3271 } else {
3272 $git_perms = "rw";
3273 }
3274
3275 # unless the file exists with the same hash, we need to update it ...
3276 unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
3277 {
3278 my $newrevision = ( $oldrevision or 0 ) + 1;
3279
3280 $head->{$git_filename} = {
3281 name => $git_filename,
3282 revision => $newrevision,
3283 filehash => $git_hash,
3284 commithash => $commit->{hash},
3285 modified => $commit->{date},
3286 author => $commit->{author},
3287 mode => $git_perms,
3288 };
3289
3290
96256bba 3291 $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
3fda8c4c
ML
3292 }
3293 }
3294 close FILELIST;
3295
3296 # Detect deleted files
3297 foreach my $file ( keys %$head )
3298 {
3299 unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
3300 {
3301 $head->{$file}{revision}++;
3302 $head->{$file}{filehash} = "deleted";
3303 $head->{$file}{commithash} = $commit->{hash};
3304 $head->{$file}{modified} = $commit->{date};
3305 $head->{$file}{author} = $commit->{author};
3306
96256bba 3307 $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
3fda8c4c
ML
3308 }
3309 }
3310 # END : "Detect deleted files"
3311 }
3312
3313
3314 if (exists $commit->{mergemsg})
3315 {
96256bba 3316 $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
3fda8c4c
ML
3317 }
3318
3319 $lastpicked = $commit->{hash};
3320
3321 $self->_set_prop("last_commit", $commit->{hash});
3322 }
3323
96256bba 3324 $self->delete_head();
3fda8c4c
ML
3325 foreach my $file ( keys %$head )
3326 {
96256bba 3327 $self->insert_head(
3fda8c4c
ML
3328 $file,
3329 $head->{$file}{revision},
3330 $head->{$file}{filehash},
3331 $head->{$file}{commithash},
3332 $head->{$file}{modified},
3333 $head->{$file}{author},
3334 $head->{$file}{mode},
3335 );
3336 }
3337 # invalidate the gethead cache
3338 $self->{gethead_cache} = undef;
3339
3340
3341 # Ending exclusive lock here
3342 $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
3343}
3344
96256bba
JS
3345sub insert_rev
3346{
3347 my $self = shift;
3348 my $name = shift;
3349 my $revision = shift;
3350 my $filehash = shift;
3351 my $commithash = shift;
3352 my $modified = shift;
3353 my $author = shift;
3354 my $mode = shift;
6aeeffd1 3355 my $tablename = $self->tablename("revision");
96256bba 3356
6aeeffd1 3357 my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
96256bba
JS
3358 $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3359}
3360
3361sub insert_mergelog
3362{
3363 my $self = shift;
3364 my $key = shift;
3365 my $value = shift;
6aeeffd1 3366 my $tablename = $self->tablename("commitmsgs");
96256bba 3367
6aeeffd1 3368 my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
96256bba
JS
3369 $insert_mergelog->execute($key, $value);
3370}
3371
3372sub delete_head
3373{
3374 my $self = shift;
6aeeffd1 3375 my $tablename = $self->tablename("head");
96256bba 3376
6aeeffd1 3377 my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM $tablename",{},1);
96256bba
JS
3378 $delete_head->execute();
3379}
3380
3381sub insert_head
3382{
3383 my $self = shift;
3384 my $name = shift;
3385 my $revision = shift;
3386 my $filehash = shift;
3387 my $commithash = shift;
3388 my $modified = shift;
3389 my $author = shift;
3390 my $mode = shift;
6aeeffd1 3391 my $tablename = $self->tablename("head");
96256bba 3392
6aeeffd1 3393 my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO $tablename (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);
96256bba
JS
3394 $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
3395}
3396
3fda8c4c
ML
3397sub _headrev
3398{
3399 my $self = shift;
3400 my $filename = shift;
6aeeffd1 3401 my $tablename = $self->tablename("head");
3fda8c4c 3402
6aeeffd1 3403 my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1);
3fda8c4c
ML
3404 $db_query->execute($filename);
3405 my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
3406
3407 return ( $hash, $revision, $mode );
3408}
3409
3410sub _get_prop
3411{
3412 my $self = shift;
3413 my $key = shift;
6aeeffd1 3414 my $tablename = $self->tablename("properties");
3fda8c4c 3415
6aeeffd1 3416 my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3fda8c4c
ML
3417 $db_query->execute($key);
3418 my ( $value ) = $db_query->fetchrow_array;
3419
3420 return $value;
3421}
3422
3423sub _set_prop
3424{
3425 my $self = shift;
3426 my $key = shift;
3427 my $value = shift;
6aeeffd1 3428 my $tablename = $self->tablename("properties");
3fda8c4c 3429
6aeeffd1 3430 my $db_query = $self->{dbh}->prepare_cached("UPDATE $tablename SET value=? WHERE key=?",{},1);
3fda8c4c
ML
3431 $db_query->execute($value, $key);
3432
3433 unless ( $db_query->rows )
3434 {
6aeeffd1 3435 $db_query = $self->{dbh}->prepare_cached("INSERT INTO $tablename (key, value) VALUES (?,?)",{},1);
3fda8c4c
ML
3436 $db_query->execute($key, $value);
3437 }
3438
3439 return $value;
3440}
3441
3442=head2 gethead
3443
3444=cut
3445
3446sub gethead
3447{
3448 my $self = shift;
6aeeffd1 3449 my $tablename = $self->tablename("head");
3fda8c4c
ML
3450
3451 return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
3452
6aeeffd1 3453 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM $tablename ORDER BY name ASC",{},1);
3fda8c4c
ML
3454 $db_query->execute();
3455
3456 my $tree = [];
3457 while ( my $file = $db_query->fetchrow_hashref )
3458 {
3459 push @$tree, $file;
3460 }
3461
3462 $self->{gethead_cache} = $tree;
3463
3464 return $tree;
3465}
3466
3467=head2 getlog
3468
3469=cut
3470
3471sub getlog
3472{
3473 my $self = shift;
3474 my $filename = shift;
6aeeffd1 3475 my $tablename = $self->tablename("revision");
3fda8c4c 3476
6aeeffd1 3477 my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3fda8c4c
ML
3478 $db_query->execute($filename);
3479
3480 my $tree = [];
3481 while ( my $file = $db_query->fetchrow_hashref )
3482 {
3483 push @$tree, $file;
3484 }
3485
3486 return $tree;
3487}
3488
3489=head2 getmeta
3490
3491This function takes a filename (with path) argument and returns a hashref of
3492metadata for that file.
3493
3494=cut
3495
3496sub getmeta
3497{
3498 my $self = shift;
3499 my $filename = shift;
3500 my $revision = shift;
6aeeffd1
JE
3501 my $tablename_rev = $self->tablename("revision");
3502 my $tablename_head = $self->tablename("head");
3fda8c4c
ML
3503
3504 my $db_query;
3505 if ( defined($revision) and $revision =~ /^\d+$/ )
3506 {
6aeeffd1 3507 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
3fda8c4c
ML
3508 $db_query->execute($filename, $revision);
3509 }
3510 elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
3511 {
6aeeffd1 3512 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
3fda8c4c
ML
3513 $db_query->execute($filename, $revision);
3514 } else {
6aeeffd1 3515 $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
3fda8c4c
ML
3516 $db_query->execute($filename);
3517 }
3518
3519 return $db_query->fetchrow_hashref;
3520}
3521
3522=head2 commitmessage
3523
3524this function takes a commithash and returns the commit message for that commit
3525
3526=cut
3527sub commitmessage
3528{
3529 my $self = shift;
3530 my $commithash = shift;
6aeeffd1 3531 my $tablename = $self->tablename("commitmsgs");
3fda8c4c
ML
3532
3533 die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ );
3534
3535 my $db_query;
6aeeffd1 3536 $db_query = $self->{dbh}->prepare_cached("SELECT value FROM $tablename WHERE key=?",{},1);
3fda8c4c
ML
3537 $db_query->execute($commithash);
3538
3539 my ( $message ) = $db_query->fetchrow_array;
3540
3541 if ( defined ( $message ) )
3542 {
3543 $message .= " " if ( $message =~ /\n$/ );
3544 return $message;
3545 }
3546
d2feb01a 3547 my @lines = safe_pipe_capture("git", "cat-file", "commit", $commithash);
3fda8c4c
ML
3548 shift @lines while ( $lines[0] =~ /\S/ );
3549 $message = join("",@lines);
3550 $message .= " " if ( $message =~ /\n$/ );
3551 return $message;
3552}
3553
3554=head2 gethistory
3555
3556This function takes a filename (with path) argument and returns an arrayofarrays
3557containing revision,filehash,commithash ordered by revision descending
3558
3559=cut
3560sub gethistory
3561{
3562 my $self = shift;
3563 my $filename = shift;
6aeeffd1 3564 my $tablename = $self->tablename("revision");
3fda8c4c
ML
3565
3566 my $db_query;
6aeeffd1 3567 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
3fda8c4c
ML
3568 $db_query->execute($filename);
3569
3570 return $db_query->fetchall_arrayref;
3571}
3572
3573=head2 gethistorydense
3574
3575This function takes a filename (with path) argument and returns an arrayofarrays
3576containing revision,filehash,commithash ordered by revision descending.
3577
3578This version of gethistory skips deleted entries -- so it is useful for annotate.
3579The 'dense' part is a reference to a '--dense' option available for git-rev-list
3580and other git tools that depend on it.
3581
3582=cut
3583sub gethistorydense
3584{
3585 my $self = shift;
3586 my $filename = shift;
6aeeffd1 3587 my $tablename = $self->tablename("revision");
3fda8c4c
ML
3588
3589 my $db_query;
6aeeffd1 3590 $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
3fda8c4c
ML
3591 $db_query->execute($filename);
3592
3593 return $db_query->fetchall_arrayref;
3594}
3595
3596=head2 in_array()
3597
3598from Array::PAT - mimics the in_array() function
3599found in PHP. Yuck but works for small arrays.
3600
3601=cut
3602sub in_array
3603{
3604 my ($check, @array) = @_;
3605 my $retval = 0;
3606 foreach my $test (@array){
3607 if($check eq $test){
3608 $retval = 1;
3609 }
3610 }
3611 return $retval;
3612}
3613
3614=head2 safe_pipe_capture
3615
5348b6e7 3616an alternative to `command` that allows input to be passed as an array
3fda8c4c
ML
3617to work around shell problems with weird characters in arguments
3618
3619=cut
3620sub safe_pipe_capture {
3621
3622 my @output;
3623
3624 if (my $pid = open my $child, '-|') {
3625 @output = (<$child>);
3626 close $child or die join(' ',@_).": $! $?";
3627 } else {
3628 exec(@_) or die "$! $?"; # exec() can fail the executable can't be found
3629 }
3630 return wantarray ? @output : join('',@output);
3631}
3632
eb1780d4
FL
3633=head2 mangle_dirname
3634
3635create a string from a directory name that is suitable to use as
3636part of a filename, mainly by converting all chars except \w.- to _
3637
3638=cut
3639sub mangle_dirname {
3640 my $dirname = shift;
3641 return unless defined $dirname;
3642
3643 $dirname =~ s/[^\w.-]/_/g;
3644
3645 return $dirname;
3646}
3fda8c4c 3647
6aeeffd1
JE
3648=head2 mangle_tablename
3649
3650create a string from a that is suitable to use as part of an SQL table
3651name, mainly by converting all chars except \w to _
3652
3653=cut
3654sub mangle_tablename {
3655 my $tablename = shift;
3656 return unless defined $tablename;
3657
3658 $tablename =~ s/[^\w_]/_/g;
3659
3660 return $tablename;
3661}
3662
3fda8c4c 36631;