1#!/usr/bin/perl 2 3#### 4#### This application is a CVS emulation layer for git. 5#### It is intended for clients to connect over SSH. 6#### See the documentation for more details. 7#### 8#### Copyright The Open University UK - 2006. 9#### 10#### Authors: Martyn Smith <martyn@catalyst.net.nz> 11#### Martin Langhoff <martin@catalyst.net.nz> 12#### 13#### 14#### Released under the GNU Public License, version 2. 15#### 16#### 17 18use strict; 19use warnings; 20use bytes; 21 22use Fcntl; 23use File::Temp qw/tempdir tempfile/; 24use File::Basename; 25 26my$log= GITCVS::log->new(); 27my$cfg; 28 29my$DATE_LIST= { 30 Jan =>"01", 31 Feb =>"02", 32 Mar =>"03", 33 Apr =>"04", 34 May =>"05", 35 Jun =>"06", 36 Jul =>"07", 37 Aug =>"08", 38 Sep =>"09", 39 Oct =>"10", 40 Nov =>"11", 41 Dec =>"12", 42}; 43 44# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 45$| =1; 46 47#### Definition and mappings of functions #### 48 49my$methods= { 50'Root'=> \&req_Root, 51'Valid-responses'=> \&req_Validresponses, 52'valid-requests'=> \&req_validrequests, 53'Directory'=> \&req_Directory, 54'Entry'=> \&req_Entry, 55'Modified'=> \&req_Modified, 56'Unchanged'=> \&req_Unchanged, 57'Questionable'=> \&req_Questionable, 58'Argument'=> \&req_Argument, 59'Argumentx'=> \&req_Argument, 60'expand-modules'=> \&req_expandmodules, 61'add'=> \&req_add, 62'remove'=> \&req_remove, 63'co'=> \&req_co, 64'update'=> \&req_update, 65'ci'=> \&req_ci, 66'diff'=> \&req_diff, 67'log'=> \&req_log, 68'rlog'=> \&req_log, 69'tag'=> \&req_CATCHALL, 70'status'=> \&req_status, 71'admin'=> \&req_CATCHALL, 72'history'=> \&req_CATCHALL, 73'watchers'=> \&req_CATCHALL, 74'editors'=> \&req_CATCHALL, 75'annotate'=> \&req_annotate, 76'Global_option'=> \&req_Globaloption, 77#'annotate' => \&req_CATCHALL, 78}; 79 80############################################## 81 82 83# $state holds all the bits of information the clients sends us that could 84# potentially be useful when it comes to actually _doing_ something. 85my$state= { prependdir =>''}; 86$log->info("--------------- STARTING -----------------"); 87 88my$TEMP_DIR= tempdir( CLEANUP =>1); 89$log->debug("Temporary directory is '$TEMP_DIR'"); 90 91# if we are called with a pserver argument, 92# deal with the authentication cat before entering the 93# main loop 94$state->{method} ='ext'; 95if(@ARGV&&$ARGV[0]eq'pserver') { 96$state->{method} ='pserver'; 97my$line= <STDIN>;chomp$line; 98unless($lineeq'BEGIN AUTH REQUEST') { 99die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 100} 101$line= <STDIN>;chomp$line; 102 req_Root('root',$line)# reuse Root 103or die"E Invalid root$line\n"; 104$line= <STDIN>;chomp$line; 105unless($lineeq'anonymous') { 106print"E Only anonymous user allowed via pserver\n"; 107print"I HATE YOU\n"; 108exit1; 109} 110$line= <STDIN>;chomp$line;# validate the password? 111$line= <STDIN>;chomp$line; 112unless($lineeq'END AUTH REQUEST') { 113die"E Do not understand$line-- expecting END AUTH REQUEST\n"; 114} 115print"I LOVE YOU\n"; 116# and now back to our regular programme... 117} 118 119# Keep going until the client closes the connection 120while(<STDIN>) 121{ 122chomp; 123 124# Check to see if we've seen this method, and call appropriate function. 125if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 126{ 127# use the $methods hash to call the appropriate sub for this command 128#$log->info("Method : $1"); 129&{$methods->{$1}}($1,$2); 130}else{ 131# log fatal because we don't understand this function. If this happens 132# we're fairly screwed because we don't know if the client is expecting 133# a response. If it is, the client will hang, we'll hang, and the whole 134# thing will be custard. 135$log->fatal("Don't understand command$_\n"); 136die("Unknown command$_"); 137} 138} 139 140$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 141$log->info("--------------- FINISH -----------------"); 142 143# Magic catchall method. 144# This is the method that will handle all commands we haven't yet 145# implemented. It simply sends a warning to the log file indicating a 146# command that hasn't been implemented has been invoked. 147sub req_CATCHALL 148{ 149my($cmd,$data) =@_; 150$log->warn("Unhandled command : req_$cmd:$data"); 151} 152 153 154# Root pathname \n 155# Response expected: no. Tell the server which CVSROOT to use. Note that 156# pathname is a local directory and not a fully qualified CVSROOT variable. 157# pathname must already exist; if creating a new root, use the init 158# request, not Root. pathname does not include the hostname of the server, 159# how to access the server, etc.; by the time the CVS protocol is in use, 160# connection, authentication, etc., are already taken care of. The Root 161# request must be sent only once, and it must be sent before any requests 162# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 163sub req_Root 164{ 165my($cmd,$data) =@_; 166$log->debug("req_Root :$data"); 167 168$state->{CVSROOT} =$data; 169 170$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 171unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 172print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 173print"E\n"; 174print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 175return0; 176} 177 178my@gitvars=`git-config -l`; 179if($?) { 180print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 181print"E\n"; 182print"error 1 - problem executing git-config\n"; 183return0; 184} 185foreachmy$line(@gitvars) 186{ 187next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 188unless($2) { 189$cfg->{$1}{$3} =$4; 190}else{ 191$cfg->{$1}{$2}{$3} =$4; 192} 193} 194 195my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 196||$cfg->{gitcvs}{enabled}); 197unless($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i) { 198print"E GITCVS emulation needs to be enabled on this repo\n"; 199print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 200print"E\n"; 201print"error 1 GITCVS emulation disabled\n"; 202return0; 203} 204 205my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 206if($logfile) 207{ 208$log->setfile($logfile); 209}else{ 210$log->nofile(); 211} 212 213return1; 214} 215 216# Global_option option \n 217# Response expected: no. Transmit one of the global options `-q', `-Q', 218# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 219# variations (such as combining of options) are allowed. For graceful 220# handling of valid-requests, it is probably better to make new global 221# options separate requests, rather than trying to add them to this 222# request. 223sub req_Globaloption 224{ 225my($cmd,$data) =@_; 226$log->debug("req_Globaloption :$data"); 227$state->{globaloptions}{$data} =1; 228} 229 230# Valid-responses request-list \n 231# Response expected: no. Tell the server what responses the client will 232# accept. request-list is a space separated list of tokens. 233sub req_Validresponses 234{ 235my($cmd,$data) =@_; 236$log->debug("req_Validresponses :$data"); 237 238# TODO : re-enable this, currently it's not particularly useful 239#$state->{validresponses} = [ split /\s+/, $data ]; 240} 241 242# valid-requests \n 243# Response expected: yes. Ask the server to send back a Valid-requests 244# response. 245sub req_validrequests 246{ 247my($cmd,$data) =@_; 248 249$log->debug("req_validrequests"); 250 251$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 252$log->debug("SEND : ok"); 253 254print"Valid-requests ".join(" ",keys%$methods) ."\n"; 255print"ok\n"; 256} 257 258# Directory local-directory \n 259# Additional data: repository \n. Response expected: no. Tell the server 260# what directory to use. The repository should be a directory name from a 261# previous server response. Note that this both gives a default for Entry 262# and Modified and also for ci and the other commands; normal usage is to 263# send Directory for each directory in which there will be an Entry or 264# Modified, and then a final Directory for the original directory, then the 265# command. The local-directory is relative to the top level at which the 266# command is occurring (i.e. the last Directory which is sent before the 267# command); to indicate that top level, `.' should be sent for 268# local-directory. 269sub req_Directory 270{ 271my($cmd,$data) =@_; 272 273my$repository= <STDIN>; 274chomp$repository; 275 276 277$state->{localdir} =$data; 278$state->{repository} =$repository; 279$state->{path} =$repository; 280$state->{path} =~s/^$state->{CVSROOT}\///; 281$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 282$state->{path} .="/"if($state->{path} =~ /\S/ ); 283 284$state->{directory} =$state->{localdir}; 285$state->{directory} =""if($state->{directory}eq"."); 286$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 287 288if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 289{ 290$log->info("Setting prepend to '$state->{path}'"); 291$state->{prependdir} =$state->{path}; 292foreachmy$entry(keys%{$state->{entries}} ) 293{ 294$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 295delete$state->{entries}{$entry}; 296} 297} 298 299if(defined($state->{prependdir} ) ) 300{ 301$log->debug("Prepending '$state->{prependdir}' to state|directory"); 302$state->{directory} =$state->{prependdir} .$state->{directory} 303} 304$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 305} 306 307# Entry entry-line \n 308# Response expected: no. Tell the server what version of a file is on the 309# local machine. The name in entry-line is a name relative to the directory 310# most recently specified with Directory. If the user is operating on only 311# some files in a directory, Entry requests for only those files need be 312# included. If an Entry request is sent without Modified, Is-modified, or 313# Unchanged, it means the file is lost (does not exist in the working 314# directory). If both Entry and one of Modified, Is-modified, or Unchanged 315# are sent for the same file, Entry must be sent first. For a given file, 316# one can send Modified, Is-modified, or Unchanged, but not more than one 317# of these three. 318sub req_Entry 319{ 320my($cmd,$data) =@_; 321 322#$log->debug("req_Entry : $data"); 323 324my@data=split(/\//,$data); 325 326$state->{entries}{$state->{directory}.$data[1]} = { 327 revision =>$data[2], 328 conflict =>$data[3], 329 options =>$data[4], 330 tag_or_date =>$data[5], 331}; 332 333$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 334} 335 336# Questionable filename \n 337# Response expected: no. Additional data: no. Tell the server to check 338# whether filename should be ignored, and if not, next time the server 339# sends responses, send (in a M response) `?' followed by the directory and 340# filename. filename must not contain `/'; it needs to be a file in the 341# directory named by the most recent Directory request. 342sub req_Questionable 343{ 344my($cmd,$data) =@_; 345 346$log->debug("req_Questionable :$data"); 347$state->{entries}{$state->{directory}.$data}{questionable} =1; 348} 349 350# add \n 351# Response expected: yes. Add a file or directory. This uses any previous 352# Argument, Directory, Entry, or Modified requests, if they have been sent. 353# The last Directory sent specifies the working directory at the time of 354# the operation. To add a directory, send the directory to be added using 355# Directory and Argument requests. 356sub req_add 357{ 358my($cmd,$data) =@_; 359 360 argsplit("add"); 361 362my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 363$updater->update(); 364 365 argsfromdir($updater); 366 367my$addcount=0; 368 369foreachmy$filename( @{$state->{args}} ) 370{ 371$filename= filecleanup($filename); 372 373my$meta=$updater->getmeta($filename); 374my$wrev= revparse($filename); 375 376if($wrev&&$meta&& ($wrev<0)) 377{ 378# previously removed file, add back 379$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 380 381print"MT +updated\n"; 382print"MT text U\n"; 383print"MT fname$filename\n"; 384print"MT newline\n"; 385print"MT -updated\n"; 386 387unless($state->{globaloptions}{-n} ) 388{ 389my($filepart,$dirpart) = filenamesplit($filename,1); 390 391print"Created$dirpart\n"; 392print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 393 394# this is an "entries" line 395my$kopts= kopts_from_path($filepart); 396$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 397print"/$filepart/1.$meta->{revision}//$kopts/\n"; 398# permissions 399$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 400print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 401# transmit file 402 transmitfile($meta->{filehash}); 403} 404 405next; 406} 407 408unless(defined($state->{entries}{$filename}{modified_filename} ) ) 409{ 410print"E cvs add: nothing known about `$filename'\n"; 411next; 412} 413# TODO : check we're not squashing an already existing file 414if(defined($state->{entries}{$filename}{revision} ) ) 415{ 416print"E cvs add: `$filename' has already been entered\n"; 417next; 418} 419 420my($filepart,$dirpart) = filenamesplit($filename,1); 421 422print"E cvs add: scheduling file `$filename' for addition\n"; 423 424print"Checked-in$dirpart\n"; 425print"$filename\n"; 426my$kopts= kopts_from_path($filepart); 427print"/$filepart/0//$kopts/\n"; 428 429$addcount++; 430} 431 432if($addcount==1) 433{ 434print"E cvs add: use `cvs commit' to add this file permanently\n"; 435} 436elsif($addcount>1) 437{ 438print"E cvs add: use `cvs commit' to add these files permanently\n"; 439} 440 441print"ok\n"; 442} 443 444# remove \n 445# Response expected: yes. Remove a file. This uses any previous Argument, 446# Directory, Entry, or Modified requests, if they have been sent. The last 447# Directory sent specifies the working directory at the time of the 448# operation. Note that this request does not actually do anything to the 449# repository; the only effect of a successful remove request is to supply 450# the client with a new entries line containing `-' to indicate a removed 451# file. In fact, the client probably could perform this operation without 452# contacting the server, although using remove may cause the server to 453# perform a few more checks. The client sends a subsequent ci request to 454# actually record the removal in the repository. 455sub req_remove 456{ 457my($cmd,$data) =@_; 458 459 argsplit("remove"); 460 461# Grab a handle to the SQLite db and do any necessary updates 462my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 463$updater->update(); 464 465#$log->debug("add state : " . Dumper($state)); 466 467my$rmcount=0; 468 469foreachmy$filename( @{$state->{args}} ) 470{ 471$filename= filecleanup($filename); 472 473if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 474{ 475print"E cvs remove: file `$filename' still in working directory\n"; 476next; 477} 478 479my$meta=$updater->getmeta($filename); 480my$wrev= revparse($filename); 481 482unless(defined($wrev) ) 483{ 484print"E cvs remove: nothing known about `$filename'\n"; 485next; 486} 487 488if(defined($wrev)and$wrev<0) 489{ 490print"E cvs remove: file `$filename' already scheduled for removal\n"; 491next; 492} 493 494unless($wrev==$meta->{revision} ) 495{ 496# TODO : not sure if the format of this message is quite correct. 497print"E cvs remove: Up to date check failed for `$filename'\n"; 498next; 499} 500 501 502my($filepart,$dirpart) = filenamesplit($filename,1); 503 504print"E cvs remove: scheduling `$filename' for removal\n"; 505 506print"Checked-in$dirpart\n"; 507print"$filename\n"; 508my$kopts= kopts_from_path($filepart); 509print"/$filepart/-1.$wrev//$kopts/\n"; 510 511$rmcount++; 512} 513 514if($rmcount==1) 515{ 516print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 517} 518elsif($rmcount>1) 519{ 520print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 521} 522 523print"ok\n"; 524} 525 526# Modified filename \n 527# Response expected: no. Additional data: mode, \n, file transmission. Send 528# the server a copy of one locally modified file. filename is a file within 529# the most recent directory sent with Directory; it must not contain `/'. 530# If the user is operating on only some files in a directory, only those 531# files need to be included. This can also be sent without Entry, if there 532# is no entry for the file. 533sub req_Modified 534{ 535my($cmd,$data) =@_; 536 537my$mode= <STDIN>; 538chomp$mode; 539my$size= <STDIN>; 540chomp$size; 541 542# Grab config information 543my$blocksize=8192; 544my$bytesleft=$size; 545my$tmp; 546 547# Get a filehandle/name to write it to 548my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 549 550# Loop over file data writing out to temporary file. 551while($bytesleft) 552{ 553$blocksize=$bytesleftif($bytesleft<$blocksize); 554read STDIN,$tmp,$blocksize; 555print$fh $tmp; 556$bytesleft-=$blocksize; 557} 558 559close$fh; 560 561# Ensure we have something sensible for the file mode 562if($mode=~/u=(\w+)/) 563{ 564$mode=$1; 565}else{ 566$mode="rw"; 567} 568 569# Save the file data in $state 570$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 571$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 572$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 573$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 574 575 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 576} 577 578# Unchanged filename\n 579# Response expected: no. Tell the server that filename has not been 580# modified in the checked out directory. The filename is a file within the 581# most recent directory sent with Directory; it must not contain `/'. 582sub req_Unchanged 583{ 584 my ($cmd,$data) =@_; 585 586$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 587 588 #$log->debug("req_Unchanged :$data"); 589} 590 591# Argument text\n 592# Response expected: no. Save argument for use in a subsequent command. 593# Arguments accumulate until an argument-using command is given, at which 594# point they are forgotten. 595# Argumentx text\n 596# Response expected: no. Append\nfollowed by text to the current argument 597# being saved. 598sub req_Argument 599{ 600 my ($cmd,$data) =@_; 601 602 # Argumentx means: append to last Argument (with a newline in front) 603 604$log->debug("$cmd:$data"); 605 606 if ($cmdeq 'Argumentx') { 607 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 608 } else { 609 push @{$state->{arguments}},$data; 610 } 611} 612 613# expand-modules\n 614# Response expected: yes. Expand the modules which are specified in the 615# arguments. Returns the data in Module-expansion responses. Note that the 616# server can assume that this is checkout or export, not rtag or rdiff; the 617# latter do not access the working directory and thus have no need to 618# expand modules on the client side. Expand may not be the best word for 619# what this request does. It does not necessarily tell you all the files 620# contained in a module, for example. Basically it is a way of telling you 621# which working directories the server needs to know about in order to 622# handle a checkout of the specified modules. For example, suppose that the 623# server has a module defined by 624# aliasmodule -a 1dir 625# That is, one can check out aliasmodule and it will take 1dir in the 626# repository and check it out to 1dir in the working directory. Now suppose 627# the client already has this module checked out and is planning on using 628# the co request to update it. Without using expand-modules, the client 629# would have two bad choices: it could either send information about all 630# working directories under the current directory, which could be 631# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 632# stands for 1dir, and neglect to send information for 1dir, which would 633# lead to incorrect operation. With expand-modules, the client would first 634# ask for the module to be expanded: 635sub req_expandmodules 636{ 637 my ($cmd,$data) =@_; 638 639 argsplit(); 640 641$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 642 643 unless ( ref$state->{arguments} eq "ARRAY" ) 644 { 645 print "ok\n"; 646 return; 647 } 648 649 foreach my$module( @{$state->{arguments}} ) 650 { 651$log->debug("SEND : Module-expansion$module"); 652 print "Module-expansion$module\n"; 653 } 654 655 print "ok\n"; 656 statecleanup(); 657} 658 659# co\n 660# Response expected: yes. Get files from the repository. This uses any 661# previous Argument, Directory, Entry, or Modified requests, if they have 662# been sent. Arguments to this command are module names; the client cannot 663# know what directories they correspond to except by (1) just sending the 664# co request, and then seeing what directory names the server sends back in 665# its responses, and (2) the expand-modules request. 666sub req_co 667{ 668 my ($cmd,$data) =@_; 669 670 argsplit("co"); 671 672 my$module=$state->{args}[0]; 673 my$checkout_path=$module; 674 675 # use the user specified directory if we're given it 676$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 677 678$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 679 680$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 681 682$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 683 684# Grab a handle to the SQLite db and do any necessary updates 685my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 686$updater->update(); 687 688$checkout_path=~ s|/$||;# get rid of trailing slashes 689 690# Eclipse seems to need the Clear-sticky command 691# to prepare the 'Entries' file for the new directory. 692print"Clear-sticky$checkout_path/\n"; 693print$state->{CVSROOT} ."/$module/\n"; 694print"Clear-static-directory$checkout_path/\n"; 695print$state->{CVSROOT} ."/$module/\n"; 696print"Clear-sticky$checkout_path/\n";# yes, twice 697print$state->{CVSROOT} ."/$module/\n"; 698print"Template$checkout_path/\n"; 699print$state->{CVSROOT} ."/$module/\n"; 700print"0\n"; 701 702# instruct the client that we're checking out to $checkout_path 703print"E cvs checkout: Updating$checkout_path\n"; 704 705my%seendirs= (); 706my$lastdir=''; 707 708# recursive 709sub prepdir { 710my($dir,$repodir,$remotedir,$seendirs) =@_; 711my$parent= dirname($dir); 712$dir=~ s|/+$||; 713$repodir=~ s|/+$||; 714$remotedir=~ s|/+$||; 715$parent=~ s|/+$||; 716$log->debug("announcedir$dir,$repodir,$remotedir"); 717 718if($parenteq'.'||$parenteq'./') { 719$parent=''; 720} 721# recurse to announce unseen parents first 722if(length($parent) && !exists($seendirs->{$parent})) { 723 prepdir($parent,$repodir,$remotedir,$seendirs); 724} 725# Announce that we are going to modify at the parent level 726if($parent) { 727print"E cvs checkout: Updating$remotedir/$parent\n"; 728}else{ 729print"E cvs checkout: Updating$remotedir\n"; 730} 731print"Clear-sticky$remotedir/$parent/\n"; 732print"$repodir/$parent/\n"; 733 734print"Clear-static-directory$remotedir/$dir/\n"; 735print"$repodir/$dir/\n"; 736print"Clear-sticky$remotedir/$parent/\n";# yes, twice 737print"$repodir/$parent/\n"; 738print"Template$remotedir/$dir/\n"; 739print"$repodir/$dir/\n"; 740print"0\n"; 741 742$seendirs->{$dir} =1; 743} 744 745foreachmy$git( @{$updater->gethead} ) 746{ 747# Don't want to check out deleted files 748next if($git->{filehash}eq"deleted"); 749 750($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 751 752if(length($git->{dir}) &&$git->{dir}ne'./' 753&&$git->{dir}ne$lastdir) { 754unless(exists($seendirs{$git->{dir}})) { 755 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 756$checkout_path, \%seendirs); 757$lastdir=$git->{dir}; 758$seendirs{$git->{dir}} =1; 759} 760print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 761} 762 763# modification time of this file 764print"Mod-time$git->{modified}\n"; 765 766# print some information to the client 767if(defined($git->{dir} )and$git->{dir}ne"./") 768{ 769print"M U$checkout_path/$git->{dir}$git->{name}\n"; 770}else{ 771print"M U$checkout_path/$git->{name}\n"; 772} 773 774# instruct client we're sending a file to put in this path 775print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 776 777print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 778 779# this is an "entries" line 780my$kopts= kopts_from_path($git->{name}); 781print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 782# permissions 783print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 784 785# transmit file 786 transmitfile($git->{filehash}); 787} 788 789print"ok\n"; 790 791 statecleanup(); 792} 793 794# update \n 795# Response expected: yes. Actually do a cvs update command. This uses any 796# previous Argument, Directory, Entry, or Modified requests, if they have 797# been sent. The last Directory sent specifies the working directory at the 798# time of the operation. The -I option is not used--files which the client 799# can decide whether to ignore are not mentioned and the client sends the 800# Questionable request for others. 801sub req_update 802{ 803my($cmd,$data) =@_; 804 805$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 806 807 argsplit("update"); 808 809# 810# It may just be a client exploring the available heads/modules 811# in that case, list them as top level directories and leave it 812# at that. Eclipse uses this technique to offer you a list of 813# projects (heads in this case) to checkout. 814# 815if($state->{module}eq'') { 816print"E cvs update: Updating .\n"; 817opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 818while(my$head=readdir(HEADS)) { 819if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 820print"E cvs update: New directory `$head'\n"; 821} 822} 823closedir HEADS; 824print"ok\n"; 825return1; 826} 827 828 829# Grab a handle to the SQLite db and do any necessary updates 830my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 831 832$updater->update(); 833 834 argsfromdir($updater); 835 836#$log->debug("update state : " . Dumper($state)); 837 838# foreach file specified on the command line ... 839foreachmy$filename( @{$state->{args}} ) 840{ 841$filename= filecleanup($filename); 842 843$log->debug("Processing file$filename"); 844 845# if we have a -C we should pretend we never saw modified stuff 846if(exists($state->{opt}{C} ) ) 847{ 848delete$state->{entries}{$filename}{modified_hash}; 849delete$state->{entries}{$filename}{modified_filename}; 850$state->{entries}{$filename}{unchanged} =1; 851} 852 853my$meta; 854if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 855{ 856$meta=$updater->getmeta($filename,$1); 857}else{ 858$meta=$updater->getmeta($filename); 859} 860 861if( !defined$meta) 862{ 863$meta= { 864 name =>$filename, 865 revision =>0, 866 filehash =>'added' 867}; 868} 869 870my$oldmeta=$meta; 871 872my$wrev= revparse($filename); 873 874# If the working copy is an old revision, lets get that version too for comparison. 875if(defined($wrev)and$wrev!=$meta->{revision} ) 876{ 877$oldmeta=$updater->getmeta($filename,$wrev); 878} 879 880#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 881 882# Files are up to date if the working copy and repo copy have the same revision, 883# and the working copy is unmodified _and_ the user hasn't specified -C 884next if(defined($wrev) 885and defined($meta->{revision}) 886and$wrev==$meta->{revision} 887and$state->{entries}{$filename}{unchanged} 888and not exists($state->{opt}{C} ) ); 889 890# If the working copy and repo copy have the same revision, 891# but the working copy is modified, tell the client it's modified 892if(defined($wrev) 893and defined($meta->{revision}) 894and$wrev==$meta->{revision} 895and defined($state->{entries}{$filename}{modified_hash}) 896and not exists($state->{opt}{C} ) ) 897{ 898$log->info("Tell the client the file is modified"); 899print"MT text M\n"; 900print"MT fname$filename\n"; 901print"MT newline\n"; 902next; 903} 904 905if($meta->{filehash}eq"deleted") 906{ 907my($filepart,$dirpart) = filenamesplit($filename,1); 908 909$log->info("Removing '$filename' from working copy (no longer in the repo)"); 910 911print"E cvs update: `$filename' is no longer in the repository\n"; 912# Don't want to actually _DO_ the update if -n specified 913unless($state->{globaloptions}{-n} ) { 914print"Removed$dirpart\n"; 915print"$filepart\n"; 916} 917} 918elsif(not defined($state->{entries}{$filename}{modified_hash} ) 919or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 920or$meta->{filehash}eq'added') 921{ 922# normal update, just send the new revision (either U=Update, 923# or A=Add, or R=Remove) 924if(defined($wrev) &&$wrev<0) 925{ 926$log->info("Tell the client the file is scheduled for removal"); 927print"MT text R\n"; 928print"MT fname$filename\n"; 929print"MT newline\n"; 930next; 931} 932elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 933{ 934$log->info("Tell the client the file is scheduled for addition"); 935print"MT text A\n"; 936print"MT fname$filename\n"; 937print"MT newline\n"; 938next; 939 940} 941else{ 942$log->info("Updating '$filename' to ".$meta->{revision}); 943print"MT +updated\n"; 944print"MT text U\n"; 945print"MT fname$filename\n"; 946print"MT newline\n"; 947print"MT -updated\n"; 948} 949 950my($filepart,$dirpart) = filenamesplit($filename,1); 951 952# Don't want to actually _DO_ the update if -n specified 953unless($state->{globaloptions}{-n} ) 954{ 955if(defined($wrev) ) 956{ 957# instruct client we're sending a file to put in this path as a replacement 958print"Update-existing$dirpart\n"; 959$log->debug("Updating existing file 'Update-existing$dirpart'"); 960}else{ 961# instruct client we're sending a file to put in this path as a new file 962print"Clear-static-directory$dirpart\n"; 963print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 964print"Clear-sticky$dirpart\n"; 965print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 966 967$log->debug("Creating new file 'Created$dirpart'"); 968print"Created$dirpart\n"; 969} 970print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 971 972# this is an "entries" line 973my$kopts= kopts_from_path($filepart); 974$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 975print"/$filepart/1.$meta->{revision}//$kopts/\n"; 976 977# permissions 978$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 979print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 980 981# transmit file 982 transmitfile($meta->{filehash}); 983} 984}else{ 985$log->info("Updating '$filename'"); 986my($filepart,$dirpart) = filenamesplit($meta->{name},1); 987 988my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 989 990chdir$dir; 991my$file_local=$filepart.".mine"; 992system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 993my$file_old=$filepart.".".$oldmeta->{revision}; 994 transmitfile($oldmeta->{filehash},$file_old); 995my$file_new=$filepart.".".$meta->{revision}; 996 transmitfile($meta->{filehash},$file_new); 997 998# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 999$log->info("Merging$file_local,$file_old,$file_new");1000print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10011002$log->debug("Temporary directory for merge is$dir");10031004my$return=system("git","merge-file",$file_local,$file_old,$file_new);1005$return>>=8;10061007if($return==0)1008{1009$log->info("Merged successfully");1010print"M M$filename\n";1011$log->debug("Merged$dirpart");10121013# Don't want to actually _DO_ the update if -n specified1014unless($state->{globaloptions}{-n} )1015{1016print"Merged$dirpart\n";1017$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1018print$state->{CVSROOT} ."/$state->{module}/$filename\n";1019my$kopts= kopts_from_path($filepart);1020$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1021print"/$filepart/1.$meta->{revision}//$kopts/\n";1022}1023}1024elsif($return==1)1025{1026$log->info("Merged with conflicts");1027print"E cvs update: conflicts found in$filename\n";1028print"M C$filename\n";10291030# Don't want to actually _DO_ the update if -n specified1031unless($state->{globaloptions}{-n} )1032{1033print"Merged$dirpart\n";1034print$state->{CVSROOT} ."/$state->{module}/$filename\n";1035my$kopts= kopts_from_path($filepart);1036print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1037}1038}1039else1040{1041$log->warn("Merge failed");1042next;1043}10441045# Don't want to actually _DO_ the update if -n specified1046unless($state->{globaloptions}{-n} )1047{1048# permissions1049$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1050print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10511052# transmit file, format is single integer on a line by itself (file1053# size) followed by the file contents1054# TODO : we should copy files in blocks1055my$data=`cat$file_local`;1056$log->debug("File size : " . length($data));1057 print length($data) . "\n";1058 print$data;1059 }10601061 chdir "/";1062 }10631064 }10651066 print "ok\n";1067}10681069sub req_ci1070{1071 my ($cmd,$data) =@_;10721073 argsplit("ci");10741075 #$log->debug("State : " . Dumper($state));10761077$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10781079 if ($state->{method} eq 'pserver')1080 {1081 print "error 1 pserver access cannot commit\n";1082 exit;1083 }10841085 if ( -e$state->{CVSROOT} . "/index" )1086 {1087$log->warn("file 'index' already exists in the git repository");1088 print "error 1 Index already exists in git repo\n";1089 exit;1090 }10911092 # Grab a handle to the SQLite db and do any necessary updates1093 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1094$updater->update();10951096 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1097 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1098$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");10991100$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1101$ENV{GIT_INDEX_FILE} =$file_index;11021103 # Remember where the head was at the beginning.1104 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1105 chomp$parenthash;1106 if ($parenthash!~ /^[0-9a-f]{40}$/) {1107 print "error 1 pserver cannot find the current HEAD of module";1108 exit;1109 }11101111 chdir$tmpdir;11121113 # populate the temporary index based1114 system("git-read-tree",$parenthash);1115 unless ($?== 0)1116 {1117 die "Error running git-read-tree$state->{module}$file_index$!";1118 }1119$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");11201121 my@committedfiles= ();1122 my%oldmeta;11231124 # foreach file specified on the command line ...1125 foreach my$filename( @{$state->{args}} )1126 {1127 my$committedfile=$filename;1128$filename= filecleanup($filename);11291130 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );11311132 my$meta=$updater->getmeta($filename);1133$oldmeta{$filename} =$meta;11341135 my$wrev= revparse($filename);11361137 my ($filepart,$dirpart) = filenamesplit($filename);11381139 # do a checkout of the file if it part of this tree1140 if ($wrev) {1141 system('git-checkout-index', '-f', '-u',$filename);1142 unless ($?== 0) {1143 die "Error running git-checkout-index -f -u$filename:$!";1144 }1145 }11461147 my$addflag= 0;1148 my$rmflag= 0;1149$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1150$addflag= 1 unless ( -e$filename);11511152 # Do up to date checking1153 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1154 {1155 # fail everything if an up to date check fails1156 print "error 1 Up to date check failed for$filename\n";1157 chdir "/";1158 exit;1159 }11601161 push@committedfiles,$committedfile;1162$log->info("Committing$filename");11631164 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11651166 unless ($rmflag)1167 {1168$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1169 rename$state->{entries}{$filename}{modified_filename},$filename;11701171 # Calculate modes to remove1172 my$invmode= "";1173 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11741175$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1176 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1177 }11781179 if ($rmflag)1180 {1181$log->info("Removing file '$filename'");1182 unlink($filename);1183 system("git-update-index", "--remove",$filename);1184 }1185 elsif ($addflag)1186 {1187$log->info("Adding file '$filename'");1188 system("git-update-index", "--add",$filename);1189 } else {1190$log->info("Updating file '$filename'");1191 system("git-update-index",$filename);1192 }1193 }11941195 unless ( scalar(@committedfiles) > 0 )1196 {1197 print "E No files to commit\n";1198 print "ok\n";1199 chdir "/";1200 return;1201 }12021203 my$treehash= `git-write-tree`;1204 chomp$treehash;12051206$log->debug("Treehash :$treehash, Parenthash :$parenthash");12071208 # write our commit message out if we have one ...1209 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1210 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1211 print$msg_fh"\n\nvia git-CVS emulator\n";1212 close$msg_fh;12131214 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1215chomp($commithash);1216$log->info("Commit hash :$commithash");12171218unless($commithash=~/[a-zA-Z0-9]{40}/)1219{1220$log->warn("Commit failed (Invalid commit hash)");1221print"error 1 Commit failed (unknown reason)\n";1222chdir"/";1223exit;1224}12251226# Check that this is allowed, just as we would with a receive-pack1227my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1228$parenthash,$commithash);1229if( -x $cmd[0] ) {1230unless(system(@cmd) ==0)1231{1232$log->warn("Commit failed (update hook declined to update ref)");1233print"error 1 Commit failed (update hook declined)\n";1234chdir"/";1235exit;1236}1237}12381239if(system(qw(git update-ref -m),"cvsserver ci",1240"refs/heads/$state->{module}",$commithash,$parenthash)) {1241$log->warn("update-ref for$state->{module} failed.");1242print"error 1 Cannot commit -- update first\n";1243exit;1244}12451246$updater->update();12471248# foreach file specified on the command line ...1249foreachmy$filename(@committedfiles)1250{1251$filename= filecleanup($filename);12521253my$meta=$updater->getmeta($filename);1254unless(defined$meta->{revision}) {1255$meta->{revision} =1;1256}12571258my($filepart,$dirpart) = filenamesplit($filename,1);12591260$log->debug("Checked-in$dirpart:$filename");12611262print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1263if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1264{1265print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1266print"Remove-entry$dirpart\n";1267print"$filename\n";1268}else{1269if($meta->{revision} ==1) {1270print"M initial revision: 1.1\n";1271}else{1272print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1273}1274print"Checked-in$dirpart\n";1275print"$filename\n";1276my$kopts= kopts_from_path($filepart);1277print"/$filepart/1.$meta->{revision}//$kopts/\n";1278}1279}12801281chdir"/";1282print"ok\n";1283}12841285sub req_status1286{1287my($cmd,$data) =@_;12881289 argsplit("status");12901291$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1292#$log->debug("status state : " . Dumper($state));12931294# Grab a handle to the SQLite db and do any necessary updates1295my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1296$updater->update();12971298# if no files were specified, we need to work out what files we should be providing status on ...1299 argsfromdir($updater);13001301# foreach file specified on the command line ...1302foreachmy$filename( @{$state->{args}} )1303{1304$filename= filecleanup($filename);13051306my$meta=$updater->getmeta($filename);1307my$oldmeta=$meta;13081309my$wrev= revparse($filename);13101311# If the working copy is an old revision, lets get that version too for comparison.1312if(defined($wrev)and$wrev!=$meta->{revision} )1313{1314$oldmeta=$updater->getmeta($filename,$wrev);1315}13161317# TODO : All possible statuses aren't yet implemented1318my$status;1319# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1320$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1321and1322( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1323or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1324);13251326# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1327$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1328and1329($state->{entries}{$filename}{unchanged}1330or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1331);13321333# Need checkout if it exists in the repo but doesn't have a working copy1334$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );13351336# Locally modified if working copy and repo copy have the same revision but there are local changes1337$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );13381339# Needs Merge if working copy revision is less than repo copy and there are local changes1340$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );13411342$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1343$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1344$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1345$status||="File had conflicts on merge"if(0);13461347$status||="Unknown";13481349print"M ===================================================================\n";1350print"M File:$filename\tStatus:$status\n";1351if(defined($state->{entries}{$filename}{revision}) )1352{1353print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1354}else{1355print"M Working revision:\tNo entry for$filename\n";1356}1357if(defined($meta->{revision}) )1358{1359print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1360print"M Sticky Tag:\t\t(none)\n";1361print"M Sticky Date:\t\t(none)\n";1362print"M Sticky Options:\t\t(none)\n";1363}else{1364print"M Repository revision:\tNo revision control file\n";1365}1366print"M\n";1367}13681369print"ok\n";1370}13711372sub req_diff1373{1374my($cmd,$data) =@_;13751376 argsplit("diff");13771378$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1379#$log->debug("status state : " . Dumper($state));13801381my($revision1,$revision2);1382if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1383{1384$revision1=$state->{opt}{r}[0];1385$revision2=$state->{opt}{r}[1];1386}else{1387$revision1=$state->{opt}{r};1388}13891390$revision1=~s/^1\.//if(defined($revision1) );1391$revision2=~s/^1\.//if(defined($revision2) );13921393$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );13941395# Grab a handle to the SQLite db and do any necessary updates1396my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1397$updater->update();13981399# if no files were specified, we need to work out what files we should be providing status on ...1400 argsfromdir($updater);14011402# foreach file specified on the command line ...1403foreachmy$filename( @{$state->{args}} )1404{1405$filename= filecleanup($filename);14061407my($fh,$file1,$file2,$meta1,$meta2,$filediff);14081409my$wrev= revparse($filename);14101411# We need _something_ to diff against1412next unless(defined($wrev) );14131414# if we have a -r switch, use it1415if(defined($revision1) )1416{1417(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1418$meta1=$updater->getmeta($filename,$revision1);1419unless(defined($meta1)and$meta1->{filehash}ne"deleted")1420{1421print"E File$filenameat revision 1.$revision1doesn't exist\n";1422next;1423}1424 transmitfile($meta1->{filehash},$file1);1425}1426# otherwise we just use the working copy revision1427else1428{1429(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1430$meta1=$updater->getmeta($filename,$wrev);1431 transmitfile($meta1->{filehash},$file1);1432}14331434# if we have a second -r switch, use it too1435if(defined($revision2) )1436{1437(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1438$meta2=$updater->getmeta($filename,$revision2);14391440unless(defined($meta2)and$meta2->{filehash}ne"deleted")1441{1442print"E File$filenameat revision 1.$revision2doesn't exist\n";1443next;1444}14451446 transmitfile($meta2->{filehash},$file2);1447}1448# otherwise we just use the working copy1449else1450{1451$file2=$state->{entries}{$filename}{modified_filename};1452}14531454# if we have been given -r, and we don't have a $file2 yet, lets get one1455if(defined($revision1)and not defined($file2) )1456{1457(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1458$meta2=$updater->getmeta($filename,$wrev);1459 transmitfile($meta2->{filehash},$file2);1460}14611462# We need to have retrieved something useful1463next unless(defined($meta1) );14641465# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1466next if(not defined($meta2)and$wrev==$meta1->{revision}1467and1468( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1469or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1470);14711472# Apparently we only show diffs for locally modified files1473next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14741475print"M Index:$filename\n";1476print"M ===================================================================\n";1477print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1478print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1479print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1480print"M diff ";1481foreachmy$opt(keys%{$state->{opt}} )1482{1483if(ref$state->{opt}{$opt}eq"ARRAY")1484{1485foreachmy$value( @{$state->{opt}{$opt}} )1486{1487print"-$opt$value";1488}1489}else{1490print"-$opt";1491print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1492}1493}1494print"$filename\n";14951496$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));14971498($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);14991500if(exists$state->{opt}{u} )1501{1502system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1503}else{1504system("diff$file1$file2>$filediff");1505}15061507while( <$fh> )1508{1509print"M$_";1510}1511close$fh;1512}15131514print"ok\n";1515}15161517sub req_log1518{1519my($cmd,$data) =@_;15201521 argsplit("log");15221523$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1524#$log->debug("log state : " . Dumper($state));15251526my($minrev,$maxrev);1527if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1528{1529my$control=$2;1530$minrev=$1;1531$maxrev=$3;1532$minrev=~s/^1\.//if(defined($minrev) );1533$maxrev=~s/^1\.//if(defined($maxrev) );1534$minrev++if(defined($minrev)and$controleq"::");1535}15361537# Grab a handle to the SQLite db and do any necessary updates1538my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1539$updater->update();15401541# if no files were specified, we need to work out what files we should be providing status on ...1542 argsfromdir($updater);15431544# foreach file specified on the command line ...1545foreachmy$filename( @{$state->{args}} )1546{1547$filename= filecleanup($filename);15481549my$headmeta=$updater->getmeta($filename);15501551my$revisions=$updater->getlog($filename);1552my$totalrevisions=scalar(@$revisions);15531554if(defined($minrev) )1555{1556$log->debug("Removing revisions less than$minrev");1557while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1558{1559pop@$revisions;1560}1561}1562if(defined($maxrev) )1563{1564$log->debug("Removing revisions greater than$maxrev");1565while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1566{1567shift@$revisions;1568}1569}15701571next unless(scalar(@$revisions) );15721573print"M\n";1574print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1575print"M Working file:$filename\n";1576print"M head: 1.$headmeta->{revision}\n";1577print"M branch:\n";1578print"M locks: strict\n";1579print"M access list:\n";1580print"M symbolic names:\n";1581print"M keyword substitution: kv\n";1582print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1583print"M description:\n";15841585foreachmy$revision(@$revisions)1586{1587print"M ----------------------------\n";1588print"M revision 1.$revision->{revision}\n";1589# reformat the date for log output1590$revision->{modified} =sprintf('%04d/%02d/%02d%s',$3,$DATE_LIST->{$2},$1,$4)if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and defined($DATE_LIST->{$2}) );1591$revision->{author} =~s/\s+.*//;1592$revision->{author} =~s/^(.{8}).*/$1/;1593print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1594my$commitmessage=$updater->commitmessage($revision->{commithash});1595$commitmessage=~s/^/M /mg;1596print$commitmessage."\n";1597}1598print"M =============================================================================\n";1599}16001601print"ok\n";1602}16031604sub req_annotate1605{1606my($cmd,$data) =@_;16071608 argsplit("annotate");16091610$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1611#$log->debug("status state : " . Dumper($state));16121613# Grab a handle to the SQLite db and do any necessary updates1614my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1615$updater->update();16161617# if no files were specified, we need to work out what files we should be providing annotate on ...1618 argsfromdir($updater);16191620# we'll need a temporary checkout dir1621my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1622my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1623$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");16241625$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1626$ENV{GIT_INDEX_FILE} =$file_index;16271628chdir$tmpdir;16291630# foreach file specified on the command line ...1631foreachmy$filename( @{$state->{args}} )1632{1633$filename= filecleanup($filename);16341635my$meta=$updater->getmeta($filename);16361637next unless($meta->{revision} );16381639# get all the commits that this file was in1640# in dense format -- aka skip dead revisions1641my$revisions=$updater->gethistorydense($filename);1642my$lastseenin=$revisions->[0][2];16431644# populate the temporary index based on the latest commit were we saw1645# the file -- but do it cheaply without checking out any files1646# TODO: if we got a revision from the client, use that instead1647# to look up the commithash in sqlite (still good to default to1648# the current head as we do now)1649system("git-read-tree",$lastseenin);1650unless($?==0)1651{1652die"Error running git-read-tree$lastseenin$file_index$!";1653}1654$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16551656# do a checkout of the file1657system('git-checkout-index','-f','-u',$filename);1658unless($?==0) {1659die"Error running git-checkout-index -f -u$filename:$!";1660}16611662$log->info("Annotate$filename");16631664# Prepare a file with the commits from the linearized1665# history that annotate should know about. This prevents1666# git-jsannotate telling us about commits we are hiding1667# from the client.16681669open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1670for(my$i=0;$i<@$revisions;$i++)1671{1672print ANNOTATEHINTS $revisions->[$i][2];1673if($i+1<@$revisions) {# have we got a parent?1674print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1675}1676print ANNOTATEHINTS "\n";1677}16781679print ANNOTATEHINTS "\n";1680close ANNOTATEHINTS;16811682my$annotatecmd='git-annotate';1683open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1684or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1685my$metadata= {};1686print"E Annotations for$filename\n";1687print"E ***************\n";1688while( <ANNOTATE> )1689{1690if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1691{1692my$commithash=$1;1693my$data=$2;1694unless(defined($metadata->{$commithash} ) )1695{1696$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1697$metadata->{$commithash}{author} =~s/\s+.*//;1698$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1699$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1700}1701printf("M 1.%-5d (%-8s%10s):%s\n",1702$metadata->{$commithash}{revision},1703$metadata->{$commithash}{author},1704$metadata->{$commithash}{modified},1705$data1706);1707}else{1708$log->warn("Error in annotate output! LINE:$_");1709print"E Annotate error\n";1710next;1711}1712}1713close ANNOTATE;1714}17151716# done; get out of the tempdir1717chdir"/";17181719print"ok\n";17201721}17221723# This method takes the state->{arguments} array and produces two new arrays.1724# The first is $state->{args} which is everything before the '--' argument, and1725# the second is $state->{files} which is everything after it.1726sub argsplit1727{1728return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");17291730my$type=shift;17311732$state->{args} = [];1733$state->{files} = [];1734$state->{opt} = {};17351736if(defined($type) )1737{1738my$opt= {};1739$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($typeeq"co");1740$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1741$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($typeeq"update");1742$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1743$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1744$opt= { k =>1, m =>1}if($typeeq"add");1745$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1746$opt= { l =>0, b =>0, h =>0, R =>0, t =>0, N =>0, S =>0, r =>1, d =>1, s =>1, w =>1}if($typeeq"log");174717481749while(scalar( @{$state->{arguments}} ) >0)1750{1751my$arg=shift@{$state->{arguments}};17521753next if($argeq"--");1754next unless($arg=~/\S/);17551756# if the argument looks like a switch1757if($arg=~/^-(\w)(.*)/)1758{1759# if it's a switch that takes an argument1760if($opt->{$1} )1761{1762# If this switch has already been provided1763if($opt->{$1} >1and exists($state->{opt}{$1} ) )1764{1765$state->{opt}{$1} = [$state->{opt}{$1} ];1766if(length($2) >0)1767{1768push@{$state->{opt}{$1}},$2;1769}else{1770push@{$state->{opt}{$1}},shift@{$state->{arguments}};1771}1772}else{1773# if there's extra data in the arg, use that as the argument for the switch1774if(length($2) >0)1775{1776$state->{opt}{$1} =$2;1777}else{1778$state->{opt}{$1} =shift@{$state->{arguments}};1779}1780}1781}else{1782$state->{opt}{$1} =undef;1783}1784}1785else1786{1787push@{$state->{args}},$arg;1788}1789}1790}1791else1792{1793my$mode=0;17941795foreachmy$value( @{$state->{arguments}} )1796{1797if($valueeq"--")1798{1799$mode++;1800next;1801}1802push@{$state->{args}},$valueif($mode==0);1803push@{$state->{files}},$valueif($mode==1);1804}1805}1806}18071808# This method uses $state->{directory} to populate $state->{args} with a list of filenames1809sub argsfromdir1810{1811my$updater=shift;18121813$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");18141815return if(scalar( @{$state->{args}} ) >1);18161817my@gethead= @{$updater->gethead};18181819# push added files1820foreachmy$file(keys%{$state->{entries}}) {1821if(exists$state->{entries}{$file}{revision} &&1822$state->{entries}{$file}{revision} ==0)1823{1824push@gethead, { name =>$file, filehash =>'added'};1825}1826}18271828if(scalar(@{$state->{args}}) ==1)1829{1830my$arg=$state->{args}[0];1831$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );18321833$log->info("Only one arg specified, checking for directory expansion on '$arg'");18341835foreachmy$file(@gethead)1836{1837next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1838next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1839push@{$state->{args}},$file->{name};1840}18411842shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1843}else{1844$log->info("Only one arg specified, populating file list automatically");18451846$state->{args} = [];18471848foreachmy$file(@gethead)1849{1850next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1851next unless($file->{name} =~s/^$state->{prependdir}//);1852push@{$state->{args}},$file->{name};1853}1854}1855}18561857# This method cleans up the $state variable after a command that uses arguments has run1858sub statecleanup1859{1860$state->{files} = [];1861$state->{args} = [];1862$state->{arguments} = [];1863$state->{entries} = {};1864}18651866sub revparse1867{1868my$filename=shift;18691870returnundefunless(defined($state->{entries}{$filename}{revision} ) );18711872return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1873return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18741875returnundef;1876}18771878# This method takes a file hash and does a CVS "file transfer" which transmits the1879# size of the file, and then the file contents.1880# If a second argument $targetfile is given, the file is instead written out to1881# a file by the name of $targetfile1882sub transmitfile1883{1884my$filehash=shift;1885my$targetfile=shift;18861887if(defined($filehash)and$filehasheq"deleted")1888{1889$log->warn("filehash is 'deleted'");1890return;1891}18921893die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);18941895my$type=`git-cat-file -t$filehash`;1896 chomp$type;18971898 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );18991900 my$size= `git-cat-file -s $filehash`;1901chomp$size;19021903$log->debug("transmitfile($filehash) size=$size, type=$type");19041905if(open my$fh,'-|',"git-cat-file","blob",$filehash)1906{1907if(defined($targetfile) )1908{1909open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1910print NEWFILE $_while( <$fh> );1911close NEWFILE;1912}else{1913print"$size\n";1914printwhile( <$fh> );1915}1916close$fhor die("Couldn't close filehandle for transmitfile()");1917}else{1918die("Couldn't execute git-cat-file");1919}1920}19211922# This method takes a file name, and returns ( $dirpart, $filepart ) which1923# refers to the directory portion and the file portion of the filename1924# respectively1925sub filenamesplit1926{1927my$filename=shift;1928my$fixforlocaldir=shift;19291930my($filepart,$dirpart) = ($filename,".");1931($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1932$dirpart.="/";19331934if($fixforlocaldir)1935{1936$dirpart=~s/^$state->{prependdir}//;1937}19381939return($filepart,$dirpart);1940}19411942sub filecleanup1943{1944my$filename=shift;19451946returnundefunless(defined($filename));1947if($filename=~/^\// )1948{1949print"E absolute filenames '$filename' not supported by server\n";1950returnundef;1951}19521953$filename=~s/^\.\///g;1954$filename=$state->{prependdir} .$filename;1955return$filename;1956}19571958# Given a path, this function returns a string containing the kopts1959# that should go into that path's Entries line. For example, a binary1960# file should get -kb.1961sub kopts_from_path1962{1963my($path) =@_;19641965# Once it exists, the git attributes system should be used to look up1966# what attributes apply to this path.19671968# Until then, take the setting from the config file1969unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1970{1971# Return "" to give no special treatment to any path1972return"";1973}else{1974# Alternatively, to have all files treated as if they are binary (which1975# is more like git itself), always return the "-kb" option1976return"-kb";1977}1978}19791980package GITCVS::log;19811982####1983#### Copyright The Open University UK - 2006.1984####1985#### Authors: Martyn Smith <martyn@catalyst.net.nz>1986#### Martin Langhoff <martin@catalyst.net.nz>1987####1988####19891990use strict;1991use warnings;19921993=head1 NAME19941995GITCVS::log19961997=head1 DESCRIPTION19981999This module provides very crude logging with a similar interface to2000Log::Log4perl20012002=head1 METHODS20032004=cut20052006=head2 new20072008Creates a new log object, optionally you can specify a filename here to2009indicate the file to log to. If no log file is specified, you can specify one2010later with method setfile, or indicate you no longer want logging with method2011nofile.20122013Until one of these methods is called, all log calls will buffer messages ready2014to write out.20152016=cut2017sub new2018{2019my$class=shift;2020my$filename=shift;20212022my$self= {};20232024bless$self,$class;20252026if(defined($filename) )2027{2028open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2029}20302031return$self;2032}20332034=head2 setfile20352036This methods takes a filename, and attempts to open that file as the log file.2037If successful, all buffered data is written out to the file, and any further2038logging is written directly to the file.20392040=cut2041sub setfile2042{2043my$self=shift;2044my$filename=shift;20452046if(defined($filename) )2047{2048open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2049}20502051return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20522053while(my$line=shift@{$self->{buffer}} )2054{2055print{$self->{fh}}$line;2056}2057}20582059=head2 nofile20602061This method indicates no logging is going to be used. It flushes any entries in2062the internal buffer, and sets a flag to ensure no further data is put there.20632064=cut2065sub nofile2066{2067my$self=shift;20682069$self->{nolog} =1;20702071return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20722073$self->{buffer} = [];2074}20752076=head2 _logopen20772078Internal method. Returns true if the log file is open, false otherwise.20792080=cut2081sub _logopen2082{2083my$self=shift;20842085return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2086return0;2087}20882089=head2 debug info warn fatal20902091These four methods are wrappers to _log. They provide the actual interface for2092logging data.20932094=cut2095sub debug {my$self=shift;$self->_log("debug",@_); }2096sub info {my$self=shift;$self->_log("info",@_); }2097subwarn{my$self=shift;$self->_log("warn",@_); }2098sub fatal {my$self=shift;$self->_log("fatal",@_); }20992100=head2 _log21012102This is an internal method called by the logging functions. It generates a2103timestamp and pushes the logged line either to file, or internal buffer.21042105=cut2106sub _log2107{2108my$self=shift;2109my$level=shift;21102111return if($self->{nolog} );21122113my@time=localtime;2114my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2115$time[5] +1900,2116$time[4] +1,2117$time[3],2118$time[2],2119$time[1],2120$time[0],2121uc$level,2122);21232124if($self->_logopen)2125{2126print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2127}else{2128push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2129}2130}21312132=head2 DESTROY21332134This method simply closes the file handle if one is open21352136=cut2137sub DESTROY2138{2139my$self=shift;21402141if($self->_logopen)2142{2143close$self->{fh};2144}2145}21462147package GITCVS::updater;21482149####2150#### Copyright The Open University UK - 2006.2151####2152#### Authors: Martyn Smith <martyn@catalyst.net.nz>2153#### Martin Langhoff <martin@catalyst.net.nz>2154####2155####21562157use strict;2158use warnings;2159use DBI;21602161=head1 METHODS21622163=cut21642165=head2 new21662167=cut2168sub new2169{2170my$class=shift;2171my$config=shift;2172my$module=shift;2173my$log=shift;21742175die"Need to specify a git repository"unless(defined($config)and-d $config);2176die"Need to specify a module"unless(defined($module) );21772178$class=ref($class) ||$class;21792180my$self= {};21812182bless$self,$class;21832184$self->{module} =$module;2185$self->{git_path} =$config."/";21862187$self->{log} =$log;21882189die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );21902191$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2192$cfg->{gitcvs}{dbdriver} ||"SQLite";2193$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2194$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2195$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2196$cfg->{gitcvs}{dbuser} ||"";2197$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2198$cfg->{gitcvs}{dbpass} ||"";2199my%mapping= ( m =>$module,2200 a =>$state->{method},2201 u =>getlogin||getpwuid($<) || $<,2202 G =>$self->{git_path},2203 g => mangle_dirname($self->{git_path}),2204);2205$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2206$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;22072208die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2209die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2210$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2211$self->{dbuser},2212$self->{dbpass});2213die"Error connecting to database\n"unlessdefined$self->{dbh};22142215$self->{tables} = {};2216foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2217{2218$self->{tables}{$table} =1;2219}22202221# Construct the revision table if required2222unless($self->{tables}{revision} )2223{2224$self->{dbh}->do("2225 CREATE TABLE revision (2226 name TEXT NOT NULL,2227 revision INTEGER NOT NULL,2228 filehash TEXT NOT NULL,2229 commithash TEXT NOT NULL,2230 author TEXT NOT NULL,2231 modified TEXT NOT NULL,2232 mode TEXT NOT NULL2233 )2234 ");2235$self->{dbh}->do("2236 CREATE INDEX revision_ix12237 ON revision (name,revision)2238 ");2239$self->{dbh}->do("2240 CREATE INDEX revision_ix22241 ON revision (name,commithash)2242 ");2243}22442245# Construct the head table if required2246unless($self->{tables}{head} )2247{2248$self->{dbh}->do("2249 CREATE TABLE head (2250 name TEXT NOT NULL,2251 revision INTEGER NOT NULL,2252 filehash TEXT NOT NULL,2253 commithash TEXT NOT NULL,2254 author TEXT NOT NULL,2255 modified TEXT NOT NULL,2256 mode TEXT NOT NULL2257 )2258 ");2259$self->{dbh}->do("2260 CREATE INDEX head_ix12261 ON head (name)2262 ");2263}22642265# Construct the properties table if required2266unless($self->{tables}{properties} )2267{2268$self->{dbh}->do("2269 CREATE TABLE properties (2270 key TEXT NOT NULL PRIMARY KEY,2271 value TEXT2272 )2273 ");2274}22752276# Construct the commitmsgs table if required2277unless($self->{tables}{commitmsgs} )2278{2279$self->{dbh}->do("2280 CREATE TABLE commitmsgs (2281 key TEXT NOT NULL PRIMARY KEY,2282 value TEXT2283 )2284 ");2285}22862287return$self;2288}22892290=head2 update22912292=cut2293sub update2294{2295my$self=shift;22962297# first lets get the commit list2298$ENV{GIT_DIR} =$self->{git_path};22992300my$commitsha1=`git rev-parse$self->{module}`;2301chomp$commitsha1;23022303my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2304unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2305{2306die("Invalid module '$self->{module}'");2307}230823092310my$git_log;2311my$lastcommit=$self->_get_prop("last_commit");23122313if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2314return1;2315}23162317# Start exclusive lock here...2318$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";23192320# TODO: log processing is memory bound2321# if we can parse into a 2nd file that is in reverse order2322# we can probably do something really efficient2323my@git_log_params= ('--pretty','--parents','--topo-order');23242325if(defined$lastcommit) {2326push@git_log_params,"$lastcommit..$self->{module}";2327}else{2328push@git_log_params,$self->{module};2329}2330# git-rev-list is the backend / plumbing version of git-log2331open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";23322333my@commits;23342335my%commit= ();23362337while( <GITLOG> )2338{2339chomp;2340if(m/^commit\s+(.*)$/) {2341# on ^commit lines put the just seen commit in the stack2342# and prime things for the next one2343if(keys%commit) {2344my%copy=%commit;2345unshift@commits, \%copy;2346%commit= ();2347}2348my@parents=split(m/\s+/,$1);2349$commit{hash} =shift@parents;2350$commit{parents} = \@parents;2351}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2352# on rfc822-like lines seen before we see any message,2353# lowercase the entry and put it in the hash as key-value2354$commit{lc($1)} =$2;2355}else{2356# message lines - skip initial empty line2357# and trim whitespace2358if(!exists($commit{message}) &&m/^\s*$/) {2359# define it to mark the end of headers2360$commit{message} ='';2361next;2362}2363s/^\s+//;s/\s+$//;# trim ws2364$commit{message} .=$_."\n";2365}2366}2367close GITLOG;23682369unshift@commits, \%commitif(keys%commit);23702371# Now all the commits are in the @commits bucket2372# ordered by time DESC. for each commit that needs processing,2373# determine whether it's following the last head we've seen or if2374# it's on its own branch, grab a file list, and add whatever's changed2375# NOTE: $lastcommit refers to the last commit from previous run2376# $lastpicked is the last commit we picked in this run2377my$lastpicked;2378my$head= {};2379if(defined$lastcommit) {2380$lastpicked=$lastcommit;2381}23822383my$committotal=scalar(@commits);2384my$commitcount=0;23852386# Load the head table into $head (for cached lookups during the update process)2387foreachmy$file( @{$self->gethead()} )2388{2389$head->{$file->{name}} =$file;2390}23912392foreachmy$commit(@commits)2393{2394$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2395if(defined$lastpicked)2396{2397if(!in_array($lastpicked, @{$commit->{parents}}))2398{2399# skip, we'll see this delta2400# as part of a merge later2401# warn "skipping off-track $commit->{hash}\n";2402next;2403}elsif(@{$commit->{parents}} >1) {2404# it is a merge commit, for each parent that is2405# not $lastpicked, see if we can get a log2406# from the merge-base to that parent to put it2407# in the message as a merge summary.2408my@parents= @{$commit->{parents}};2409foreachmy$parent(@parents) {2410# git-merge-base can potentially (but rarely) throw2411# several candidate merge bases. let's assume2412# that the first one is the best one.2413if($parenteq$lastpicked) {2414next;2415}2416open my$p,'git-merge-base '.$lastpicked.' '2417.$parent.'|';2418my@output= (<$p>);2419close$p;2420my$base=join('',@output);2421chomp$base;2422if($base) {2423my@merged;2424# print "want to log between $base $parent \n";2425open(GITLOG,'-|','git-log',"$base..$parent")2426or die"Cannot call git-log:$!";2427my$mergedhash;2428while(<GITLOG>) {2429chomp;2430if(!defined$mergedhash) {2431if(m/^commit\s+(.+)$/) {2432$mergedhash=$1;2433}else{2434next;2435}2436}else{2437# grab the first line that looks non-rfc8222438# aka has content after leading space2439if(m/^\s+(\S.*)$/) {2440my$title=$1;2441$title=substr($title,0,100);# truncate2442unshift@merged,"$mergedhash$title";2443undef$mergedhash;2444}2445}2446}2447close GITLOG;2448if(@merged) {2449$commit->{mergemsg} =$commit->{message};2450$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2451foreachmy$summary(@merged) {2452$commit->{mergemsg} .="\t$summary\n";2453}2454$commit->{mergemsg} .="\n\n";2455# print "Message for $commit->{hash} \n$commit->{mergemsg}";2456}2457}2458}2459}2460}24612462# convert the date to CVS-happy format2463$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24642465if(defined($lastpicked) )2466{2467my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2468local($/) ="\0";2469while( <FILELIST> )2470{2471chomp;2472unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2473{2474die("Couldn't process git-diff-tree line :$_");2475}2476my($mode,$hash,$change) = ($1,$2,$3);2477my$name= <FILELIST>;2478chomp($name);24792480# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24812482my$git_perms="";2483$git_perms.="r"if($mode&4);2484$git_perms.="w"if($mode&2);2485$git_perms.="x"if($mode&1);2486$git_perms="rw"if($git_permseq"");24872488if($changeeq"D")2489{2490#$log->debug("DELETE $name");2491$head->{$name} = {2492 name =>$name,2493 revision =>$head->{$name}{revision} +1,2494 filehash =>"deleted",2495 commithash =>$commit->{hash},2496 modified =>$commit->{date},2497 author =>$commit->{author},2498 mode =>$git_perms,2499};2500$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2501}2502elsif($changeeq"M")2503{2504#$log->debug("MODIFIED $name");2505$head->{$name} = {2506 name =>$name,2507 revision =>$head->{$name}{revision} +1,2508 filehash =>$hash,2509 commithash =>$commit->{hash},2510 modified =>$commit->{date},2511 author =>$commit->{author},2512 mode =>$git_perms,2513};2514$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2515}2516elsif($changeeq"A")2517{2518#$log->debug("ADDED $name");2519$head->{$name} = {2520 name =>$name,2521 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2522 filehash =>$hash,2523 commithash =>$commit->{hash},2524 modified =>$commit->{date},2525 author =>$commit->{author},2526 mode =>$git_perms,2527};2528$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2529}2530else2531{2532$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2533die;2534}2535}2536close FILELIST;2537}else{2538# this is used to detect files removed from the repo2539my$seen_files= {};25402541my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2542local$/="\0";2543while( <FILELIST> )2544{2545chomp;2546unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2547{2548die("Couldn't process git-ls-tree line :$_");2549}25502551my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);25522553$seen_files->{$git_filename} =1;25542555my($oldhash,$oldrevision,$oldmode) = (2556$head->{$git_filename}{filehash},2557$head->{$git_filename}{revision},2558$head->{$git_filename}{mode}2559);25602561if($git_perms=~/^\d\d\d(\d)\d\d/o)2562{2563$git_perms="";2564$git_perms.="r"if($1&4);2565$git_perms.="w"if($1&2);2566$git_perms.="x"if($1&1);2567}else{2568$git_perms="rw";2569}25702571# unless the file exists with the same hash, we need to update it ...2572unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2573{2574my$newrevision= ($oldrevisionor0) +1;25752576$head->{$git_filename} = {2577 name =>$git_filename,2578 revision =>$newrevision,2579 filehash =>$git_hash,2580 commithash =>$commit->{hash},2581 modified =>$commit->{date},2582 author =>$commit->{author},2583 mode =>$git_perms,2584};258525862587$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2588}2589}2590close FILELIST;25912592# Detect deleted files2593foreachmy$file(keys%$head)2594{2595unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2596{2597$head->{$file}{revision}++;2598$head->{$file}{filehash} ="deleted";2599$head->{$file}{commithash} =$commit->{hash};2600$head->{$file}{modified} =$commit->{date};2601$head->{$file}{author} =$commit->{author};26022603$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2604}2605}2606# END : "Detect deleted files"2607}260826092610if(exists$commit->{mergemsg})2611{2612$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2613}26142615$lastpicked=$commit->{hash};26162617$self->_set_prop("last_commit",$commit->{hash});2618}26192620$self->delete_head();2621foreachmy$file(keys%$head)2622{2623$self->insert_head(2624$file,2625$head->{$file}{revision},2626$head->{$file}{filehash},2627$head->{$file}{commithash},2628$head->{$file}{modified},2629$head->{$file}{author},2630$head->{$file}{mode},2631);2632}2633# invalidate the gethead cache2634$self->{gethead_cache} =undef;263526362637# Ending exclusive lock here2638$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2639}26402641sub insert_rev2642{2643my$self=shift;2644my$name=shift;2645my$revision=shift;2646my$filehash=shift;2647my$commithash=shift;2648my$modified=shift;2649my$author=shift;2650my$mode=shift;26512652my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2653$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2654}26552656sub insert_mergelog2657{2658my$self=shift;2659my$key=shift;2660my$value=shift;26612662my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2663$insert_mergelog->execute($key,$value);2664}26652666sub delete_head2667{2668my$self=shift;26692670my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2671$delete_head->execute();2672}26732674sub insert_head2675{2676my$self=shift;2677my$name=shift;2678my$revision=shift;2679my$filehash=shift;2680my$commithash=shift;2681my$modified=shift;2682my$author=shift;2683my$mode=shift;26842685my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2686$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2687}26882689sub _headrev2690{2691my$self=shift;2692my$filename=shift;26932694my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2695$db_query->execute($filename);2696my($hash,$revision,$mode) =$db_query->fetchrow_array;26972698return($hash,$revision,$mode);2699}27002701sub _get_prop2702{2703my$self=shift;2704my$key=shift;27052706my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2707$db_query->execute($key);2708my($value) =$db_query->fetchrow_array;27092710return$value;2711}27122713sub _set_prop2714{2715my$self=shift;2716my$key=shift;2717my$value=shift;27182719my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2720$db_query->execute($value,$key);27212722unless($db_query->rows)2723{2724$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2725$db_query->execute($key,$value);2726}27272728return$value;2729}27302731=head2 gethead27322733=cut27342735sub gethead2736{2737my$self=shift;27382739return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );27402741my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2742$db_query->execute();27432744my$tree= [];2745while(my$file=$db_query->fetchrow_hashref)2746{2747push@$tree,$file;2748}27492750$self->{gethead_cache} =$tree;27512752return$tree;2753}27542755=head2 getlog27562757=cut27582759sub getlog2760{2761my$self=shift;2762my$filename=shift;27632764my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2765$db_query->execute($filename);27662767my$tree= [];2768while(my$file=$db_query->fetchrow_hashref)2769{2770push@$tree,$file;2771}27722773return$tree;2774}27752776=head2 getmeta27772778This function takes a filename (with path) argument and returns a hashref of2779metadata for that file.27802781=cut27822783sub getmeta2784{2785my$self=shift;2786my$filename=shift;2787my$revision=shift;27882789my$db_query;2790if(defined($revision)and$revision=~/^\d+$/)2791{2792$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2793$db_query->execute($filename,$revision);2794}2795elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2796{2797$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2798$db_query->execute($filename,$revision);2799}else{2800$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2801$db_query->execute($filename);2802}28032804return$db_query->fetchrow_hashref;2805}28062807=head2 commitmessage28082809this function takes a commithash and returns the commit message for that commit28102811=cut2812sub commitmessage2813{2814my$self=shift;2815my$commithash=shift;28162817die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);28182819my$db_query;2820$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2821$db_query->execute($commithash);28222823my($message) =$db_query->fetchrow_array;28242825if(defined($message) )2826{2827$message.=" "if($message=~/\n$/);2828return$message;2829}28302831my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2832shift@lineswhile($lines[0] =~/\S/);2833$message=join("",@lines);2834$message.=" "if($message=~/\n$/);2835return$message;2836}28372838=head2 gethistory28392840This function takes a filename (with path) argument and returns an arrayofarrays2841containing revision,filehash,commithash ordered by revision descending28422843=cut2844sub gethistory2845{2846my$self=shift;2847my$filename=shift;28482849my$db_query;2850$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2851$db_query->execute($filename);28522853return$db_query->fetchall_arrayref;2854}28552856=head2 gethistorydense28572858This function takes a filename (with path) argument and returns an arrayofarrays2859containing revision,filehash,commithash ordered by revision descending.28602861This version of gethistory skips deleted entries -- so it is useful for annotate.2862The 'dense' part is a reference to a '--dense' option available for git-rev-list2863and other git tools that depend on it.28642865=cut2866sub gethistorydense2867{2868my$self=shift;2869my$filename=shift;28702871my$db_query;2872$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2873$db_query->execute($filename);28742875return$db_query->fetchall_arrayref;2876}28772878=head2 in_array()28792880from Array::PAT - mimics the in_array() function2881found in PHP. Yuck but works for small arrays.28822883=cut2884sub in_array2885{2886my($check,@array) =@_;2887my$retval=0;2888foreachmy$test(@array){2889if($checkeq$test){2890$retval=1;2891}2892}2893return$retval;2894}28952896=head2 safe_pipe_capture28972898an alternative to `command` that allows input to be passed as an array2899to work around shell problems with weird characters in arguments29002901=cut2902sub safe_pipe_capture {29032904my@output;29052906if(my$pid=open my$child,'-|') {2907@output= (<$child>);2908close$childor die join(' ',@_).":$!$?";2909}else{2910exec(@_)or die"$!$?";# exec() can fail the executable can't be found2911}2912returnwantarray?@output:join('',@output);2913}29142915=head2 mangle_dirname29162917create a string from a directory name that is suitable to use as2918part of a filename, mainly by converting all chars except \w.- to _29192920=cut2921sub mangle_dirname {2922my$dirname=shift;2923return unlessdefined$dirname;29242925$dirname=~s/[^\w.-]/_/g;29262927return$dirname;2928}292929301;