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