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