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; 25use Getopt::Long qw(:config require_order no_ignore_case); 26 27my$VERSION='@@GIT_VERSION@@'; 28 29my$log= GITCVS::log->new(); 30my$cfg; 31 32my$DATE_LIST= { 33 Jan =>"01", 34 Feb =>"02", 35 Mar =>"03", 36 Apr =>"04", 37 May =>"05", 38 Jun =>"06", 39 Jul =>"07", 40 Aug =>"08", 41 Sep =>"09", 42 Oct =>"10", 43 Nov =>"11", 44 Dec =>"12", 45}; 46 47# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 48$| =1; 49 50#### Definition and mappings of functions #### 51 52my$methods= { 53'Root'=> \&req_Root, 54'Valid-responses'=> \&req_Validresponses, 55'valid-requests'=> \&req_validrequests, 56'Directory'=> \&req_Directory, 57'Entry'=> \&req_Entry, 58'Modified'=> \&req_Modified, 59'Unchanged'=> \&req_Unchanged, 60'Questionable'=> \&req_Questionable, 61'Argument'=> \&req_Argument, 62'Argumentx'=> \&req_Argument, 63'expand-modules'=> \&req_expandmodules, 64'add'=> \&req_add, 65'remove'=> \&req_remove, 66'co'=> \&req_co, 67'update'=> \&req_update, 68'ci'=> \&req_ci, 69'diff'=> \&req_diff, 70'log'=> \&req_log, 71'rlog'=> \&req_log, 72'tag'=> \&req_CATCHALL, 73'status'=> \&req_status, 74'admin'=> \&req_CATCHALL, 75'history'=> \&req_CATCHALL, 76'watchers'=> \&req_CATCHALL, 77'editors'=> \&req_CATCHALL, 78'annotate'=> \&req_annotate, 79'Global_option'=> \&req_Globaloption, 80#'annotate' => \&req_CATCHALL, 81}; 82 83############################################## 84 85 86# $state holds all the bits of information the clients sends us that could 87# potentially be useful when it comes to actually _doing_ something. 88my$state= { prependdir =>''}; 89$log->info("--------------- STARTING -----------------"); 90 91my$usage= 92"Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n". 93" --base-path <path> : Prepend to requested CVSROOT\n". 94" --strict-paths : Don't allow recursing into subdirectories\n". 95" --export-all : Don't check for gitcvs.enabled in config\n". 96" --version, -V : Print version information and exit\n". 97" --help, -h, -H : Print usage information and exit\n". 98"\n". 99"<directory> ... is a list of allowed directories. If no directories\n". 100"are given, all are allowed. This is an additional restriction, gitcvs\n". 101"access still needs to be enabled by the gitcvs.enabled config option.\n"; 102 103my@opts= ('help|h|H','version|V', 104'base-path=s','strict-paths','export-all'); 105GetOptions($state,@opts) 106or die$usage; 107 108if($state->{version}) { 109print"git-cvsserver version$VERSION\n"; 110exit; 111} 112if($state->{help}) { 113print$usage; 114exit; 115} 116 117my$TEMP_DIR= tempdir( CLEANUP =>1); 118$log->debug("Temporary directory is '$TEMP_DIR'"); 119 120$state->{method} ='ext'; 121if(@ARGV) { 122if($ARGV[0]eq'pserver') { 123$state->{method} ='pserver'; 124shift@ARGV; 125}elsif($ARGV[0]eq'server') { 126shift@ARGV; 127} 128} 129 130# everything else is a directory 131$state->{allowed_roots} = [@ARGV]; 132 133# don't export the whole system unless the users requests it 134if($state->{'export-all'} && !@{$state->{allowed_roots}}) { 135die"--export-all can only be used together with an explicit whitelist\n"; 136} 137 138# if we are called with a pserver argument, 139# deal with the authentication cat before entering the 140# main loop 141if($state->{method}eq'pserver') { 142my$line= <STDIN>;chomp$line; 143unless($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 144die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 145} 146my$request=$1; 147$line= <STDIN>;chomp$line; 148 req_Root('root',$line)# reuse Root 149or die"E Invalid root$line\n"; 150$line= <STDIN>;chomp$line; 151unless($lineeq'anonymous') { 152print"E Only anonymous user allowed via pserver\n"; 153print"I HATE YOU\n"; 154exit1; 155} 156$line= <STDIN>;chomp$line;# validate the password? 157$line= <STDIN>;chomp$line; 158unless($lineeq"END$requestREQUEST") { 159die"E Do not understand$line-- expecting END$requestREQUEST\n"; 160} 161print"I LOVE YOU\n"; 162exit if$requesteq'VERIFICATION';# cvs login 163# and now back to our regular programme... 164} 165 166# Keep going until the client closes the connection 167while(<STDIN>) 168{ 169chomp; 170 171# Check to see if we've seen this method, and call appropriate function. 172if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 173{ 174# use the $methods hash to call the appropriate sub for this command 175#$log->info("Method : $1"); 176&{$methods->{$1}}($1,$2); 177}else{ 178# log fatal because we don't understand this function. If this happens 179# we're fairly screwed because we don't know if the client is expecting 180# a response. If it is, the client will hang, we'll hang, and the whole 181# thing will be custard. 182$log->fatal("Don't understand command$_\n"); 183die("Unknown command$_"); 184} 185} 186 187$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 188$log->info("--------------- FINISH -----------------"); 189 190# Magic catchall method. 191# This is the method that will handle all commands we haven't yet 192# implemented. It simply sends a warning to the log file indicating a 193# command that hasn't been implemented has been invoked. 194sub req_CATCHALL 195{ 196my($cmd,$data) =@_; 197$log->warn("Unhandled command : req_$cmd:$data"); 198} 199 200 201# Root pathname \n 202# Response expected: no. Tell the server which CVSROOT to use. Note that 203# pathname is a local directory and not a fully qualified CVSROOT variable. 204# pathname must already exist; if creating a new root, use the init 205# request, not Root. pathname does not include the hostname of the server, 206# how to access the server, etc.; by the time the CVS protocol is in use, 207# connection, authentication, etc., are already taken care of. The Root 208# request must be sent only once, and it must be sent before any requests 209# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 210sub req_Root 211{ 212my($cmd,$data) =@_; 213$log->debug("req_Root :$data"); 214 215unless($data=~ m#^/#) { 216print"error 1 Root must be an absolute pathname\n"; 217return0; 218} 219 220my$cvsroot=$state->{'base-path'} ||''; 221$cvsroot=~ s#/+$##; 222$cvsroot.=$data; 223 224if($state->{CVSROOT} 225&& ($state->{CVSROOT}ne$cvsroot)) { 226print"error 1 Conflicting roots specified\n"; 227return0; 228} 229 230$state->{CVSROOT} =$cvsroot; 231 232$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 233 234if(@{$state->{allowed_roots}}) { 235my$allowed=0; 236foreachmy$dir(@{$state->{allowed_roots}}) { 237next unless$dir=~ m#^/#; 238$dir=~ s#/+$##; 239if($state->{'strict-paths'}) { 240if($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { 241$allowed=1; 242last; 243} 244}elsif($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { 245$allowed=1; 246last; 247} 248} 249 250unless($allowed) { 251print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 252print"E\n"; 253print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 254return0; 255} 256} 257 258unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 259print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 260print"E\n"; 261print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 262return0; 263} 264 265my@gitvars=`git-config -l`; 266if($?) { 267print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 268print"E\n"; 269print"error 1 - problem executing git-config\n"; 270return0; 271} 272foreachmy$line(@gitvars) 273{ 274next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 275unless($2) { 276$cfg->{$1}{$3} =$4; 277}else{ 278$cfg->{$1}{$2}{$3} =$4; 279} 280} 281 282my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 283||$cfg->{gitcvs}{enabled}); 284unless($state->{'export-all'} || 285($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i)) { 286print"E GITCVS emulation needs to be enabled on this repo\n"; 287print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 288print"E\n"; 289print"error 1 GITCVS emulation disabled\n"; 290return0; 291} 292 293my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 294if($logfile) 295{ 296$log->setfile($logfile); 297}else{ 298$log->nofile(); 299} 300 301return1; 302} 303 304# Global_option option \n 305# Response expected: no. Transmit one of the global options `-q', `-Q', 306# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 307# variations (such as combining of options) are allowed. For graceful 308# handling of valid-requests, it is probably better to make new global 309# options separate requests, rather than trying to add them to this 310# request. 311sub req_Globaloption 312{ 313my($cmd,$data) =@_; 314$log->debug("req_Globaloption :$data"); 315$state->{globaloptions}{$data} =1; 316} 317 318# Valid-responses request-list \n 319# Response expected: no. Tell the server what responses the client will 320# accept. request-list is a space separated list of tokens. 321sub req_Validresponses 322{ 323my($cmd,$data) =@_; 324$log->debug("req_Validresponses :$data"); 325 326# TODO : re-enable this, currently it's not particularly useful 327#$state->{validresponses} = [ split /\s+/, $data ]; 328} 329 330# valid-requests \n 331# Response expected: yes. Ask the server to send back a Valid-requests 332# response. 333sub req_validrequests 334{ 335my($cmd,$data) =@_; 336 337$log->debug("req_validrequests"); 338 339$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 340$log->debug("SEND : ok"); 341 342print"Valid-requests ".join(" ",keys%$methods) ."\n"; 343print"ok\n"; 344} 345 346# Directory local-directory \n 347# Additional data: repository \n. Response expected: no. Tell the server 348# what directory to use. The repository should be a directory name from a 349# previous server response. Note that this both gives a default for Entry 350# and Modified and also for ci and the other commands; normal usage is to 351# send Directory for each directory in which there will be an Entry or 352# Modified, and then a final Directory for the original directory, then the 353# command. The local-directory is relative to the top level at which the 354# command is occurring (i.e. the last Directory which is sent before the 355# command); to indicate that top level, `.' should be sent for 356# local-directory. 357sub req_Directory 358{ 359my($cmd,$data) =@_; 360 361my$repository= <STDIN>; 362chomp$repository; 363 364 365$state->{localdir} =$data; 366$state->{repository} =$repository; 367$state->{path} =$repository; 368$state->{path} =~s/^$state->{CVSROOT}\///; 369$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 370$state->{path} .="/"if($state->{path} =~ /\S/ ); 371 372$state->{directory} =$state->{localdir}; 373$state->{directory} =""if($state->{directory}eq"."); 374$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 375 376if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 377{ 378$log->info("Setting prepend to '$state->{path}'"); 379$state->{prependdir} =$state->{path}; 380foreachmy$entry(keys%{$state->{entries}} ) 381{ 382$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 383delete$state->{entries}{$entry}; 384} 385} 386 387if(defined($state->{prependdir} ) ) 388{ 389$log->debug("Prepending '$state->{prependdir}' to state|directory"); 390$state->{directory} =$state->{prependdir} .$state->{directory} 391} 392$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 393} 394 395# Entry entry-line \n 396# Response expected: no. Tell the server what version of a file is on the 397# local machine. The name in entry-line is a name relative to the directory 398# most recently specified with Directory. If the user is operating on only 399# some files in a directory, Entry requests for only those files need be 400# included. If an Entry request is sent without Modified, Is-modified, or 401# Unchanged, it means the file is lost (does not exist in the working 402# directory). If both Entry and one of Modified, Is-modified, or Unchanged 403# are sent for the same file, Entry must be sent first. For a given file, 404# one can send Modified, Is-modified, or Unchanged, but not more than one 405# of these three. 406sub req_Entry 407{ 408my($cmd,$data) =@_; 409 410#$log->debug("req_Entry : $data"); 411 412my@data=split(/\//,$data); 413 414$state->{entries}{$state->{directory}.$data[1]} = { 415 revision =>$data[2], 416 conflict =>$data[3], 417 options =>$data[4], 418 tag_or_date =>$data[5], 419}; 420 421$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 422} 423 424# Questionable filename \n 425# Response expected: no. Additional data: no. Tell the server to check 426# whether filename should be ignored, and if not, next time the server 427# sends responses, send (in a M response) `?' followed by the directory and 428# filename. filename must not contain `/'; it needs to be a file in the 429# directory named by the most recent Directory request. 430sub req_Questionable 431{ 432my($cmd,$data) =@_; 433 434$log->debug("req_Questionable :$data"); 435$state->{entries}{$state->{directory}.$data}{questionable} =1; 436} 437 438# add \n 439# Response expected: yes. Add a file or directory. This uses any previous 440# Argument, Directory, Entry, or Modified requests, if they have been sent. 441# The last Directory sent specifies the working directory at the time of 442# the operation. To add a directory, send the directory to be added using 443# Directory and Argument requests. 444sub req_add 445{ 446my($cmd,$data) =@_; 447 448 argsplit("add"); 449 450my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 451$updater->update(); 452 453 argsfromdir($updater); 454 455my$addcount=0; 456 457foreachmy$filename( @{$state->{args}} ) 458{ 459$filename= filecleanup($filename); 460 461my$meta=$updater->getmeta($filename); 462my$wrev= revparse($filename); 463 464if($wrev&&$meta&& ($wrev<0)) 465{ 466# previously removed file, add back 467$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 468 469print"MT +updated\n"; 470print"MT text U\n"; 471print"MT fname$filename\n"; 472print"MT newline\n"; 473print"MT -updated\n"; 474 475unless($state->{globaloptions}{-n} ) 476{ 477my($filepart,$dirpart) = filenamesplit($filename,1); 478 479print"Created$dirpart\n"; 480print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 481 482# this is an "entries" line 483my$kopts= kopts_from_path($filepart); 484$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 485print"/$filepart/1.$meta->{revision}//$kopts/\n"; 486# permissions 487$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 488print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 489# transmit file 490 transmitfile($meta->{filehash}); 491} 492 493next; 494} 495 496unless(defined($state->{entries}{$filename}{modified_filename} ) ) 497{ 498print"E cvs add: nothing known about `$filename'\n"; 499next; 500} 501# TODO : check we're not squashing an already existing file 502if(defined($state->{entries}{$filename}{revision} ) ) 503{ 504print"E cvs add: `$filename' has already been entered\n"; 505next; 506} 507 508my($filepart,$dirpart) = filenamesplit($filename,1); 509 510print"E cvs add: scheduling file `$filename' for addition\n"; 511 512print"Checked-in$dirpart\n"; 513print"$filename\n"; 514my$kopts= kopts_from_path($filepart); 515print"/$filepart/0//$kopts/\n"; 516 517$addcount++; 518} 519 520if($addcount==1) 521{ 522print"E cvs add: use `cvs commit' to add this file permanently\n"; 523} 524elsif($addcount>1) 525{ 526print"E cvs add: use `cvs commit' to add these files permanently\n"; 527} 528 529print"ok\n"; 530} 531 532# remove \n 533# Response expected: yes. Remove a file. This uses any previous Argument, 534# Directory, Entry, or Modified requests, if they have been sent. The last 535# Directory sent specifies the working directory at the time of the 536# operation. Note that this request does not actually do anything to the 537# repository; the only effect of a successful remove request is to supply 538# the client with a new entries line containing `-' to indicate a removed 539# file. In fact, the client probably could perform this operation without 540# contacting the server, although using remove may cause the server to 541# perform a few more checks. The client sends a subsequent ci request to 542# actually record the removal in the repository. 543sub req_remove 544{ 545my($cmd,$data) =@_; 546 547 argsplit("remove"); 548 549# Grab a handle to the SQLite db and do any necessary updates 550my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 551$updater->update(); 552 553#$log->debug("add state : " . Dumper($state)); 554 555my$rmcount=0; 556 557foreachmy$filename( @{$state->{args}} ) 558{ 559$filename= filecleanup($filename); 560 561if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 562{ 563print"E cvs remove: file `$filename' still in working directory\n"; 564next; 565} 566 567my$meta=$updater->getmeta($filename); 568my$wrev= revparse($filename); 569 570unless(defined($wrev) ) 571{ 572print"E cvs remove: nothing known about `$filename'\n"; 573next; 574} 575 576if(defined($wrev)and$wrev<0) 577{ 578print"E cvs remove: file `$filename' already scheduled for removal\n"; 579next; 580} 581 582unless($wrev==$meta->{revision} ) 583{ 584# TODO : not sure if the format of this message is quite correct. 585print"E cvs remove: Up to date check failed for `$filename'\n"; 586next; 587} 588 589 590my($filepart,$dirpart) = filenamesplit($filename,1); 591 592print"E cvs remove: scheduling `$filename' for removal\n"; 593 594print"Checked-in$dirpart\n"; 595print"$filename\n"; 596my$kopts= kopts_from_path($filepart); 597print"/$filepart/-1.$wrev//$kopts/\n"; 598 599$rmcount++; 600} 601 602if($rmcount==1) 603{ 604print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 605} 606elsif($rmcount>1) 607{ 608print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 609} 610 611print"ok\n"; 612} 613 614# Modified filename \n 615# Response expected: no. Additional data: mode, \n, file transmission. Send 616# the server a copy of one locally modified file. filename is a file within 617# the most recent directory sent with Directory; it must not contain `/'. 618# If the user is operating on only some files in a directory, only those 619# files need to be included. This can also be sent without Entry, if there 620# is no entry for the file. 621sub req_Modified 622{ 623my($cmd,$data) =@_; 624 625my$mode= <STDIN>; 626defined$mode 627or(print"E end of file reading mode for$data\n"),return; 628chomp$mode; 629my$size= <STDIN>; 630defined$size 631or(print"E end of file reading size of$data\n"),return; 632chomp$size; 633 634# Grab config information 635my$blocksize=8192; 636my$bytesleft=$size; 637my$tmp; 638 639# Get a filehandle/name to write it to 640my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 641 642# Loop over file data writing out to temporary file. 643while($bytesleft) 644{ 645$blocksize=$bytesleftif($bytesleft<$blocksize); 646read STDIN,$tmp,$blocksize; 647print$fh $tmp; 648$bytesleft-=$blocksize; 649} 650 651close$fh 652or(print"E failed to write temporary,$filename:$!\n"),return; 653 654# Ensure we have something sensible for the file mode 655if($mode=~/u=(\w+)/) 656{ 657$mode=$1; 658}else{ 659$mode="rw"; 660} 661 662# Save the file data in $state 663$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 664$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 665$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 666$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 667 668 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 669} 670 671# Unchanged filename\n 672# Response expected: no. Tell the server that filename has not been 673# modified in the checked out directory. The filename is a file within the 674# most recent directory sent with Directory; it must not contain `/'. 675sub req_Unchanged 676{ 677 my ($cmd,$data) =@_; 678 679$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 680 681 #$log->debug("req_Unchanged :$data"); 682} 683 684# Argument text\n 685# Response expected: no. Save argument for use in a subsequent command. 686# Arguments accumulate until an argument-using command is given, at which 687# point they are forgotten. 688# Argumentx text\n 689# Response expected: no. Append\nfollowed by text to the current argument 690# being saved. 691sub req_Argument 692{ 693 my ($cmd,$data) =@_; 694 695 # Argumentx means: append to last Argument (with a newline in front) 696 697$log->debug("$cmd:$data"); 698 699 if ($cmdeq 'Argumentx') { 700 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 701 } else { 702 push @{$state->{arguments}},$data; 703 } 704} 705 706# expand-modules\n 707# Response expected: yes. Expand the modules which are specified in the 708# arguments. Returns the data in Module-expansion responses. Note that the 709# server can assume that this is checkout or export, not rtag or rdiff; the 710# latter do not access the working directory and thus have no need to 711# expand modules on the client side. Expand may not be the best word for 712# what this request does. It does not necessarily tell you all the files 713# contained in a module, for example. Basically it is a way of telling you 714# which working directories the server needs to know about in order to 715# handle a checkout of the specified modules. For example, suppose that the 716# server has a module defined by 717# aliasmodule -a 1dir 718# That is, one can check out aliasmodule and it will take 1dir in the 719# repository and check it out to 1dir in the working directory. Now suppose 720# the client already has this module checked out and is planning on using 721# the co request to update it. Without using expand-modules, the client 722# would have two bad choices: it could either send information about all 723# working directories under the current directory, which could be 724# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 725# stands for 1dir, and neglect to send information for 1dir, which would 726# lead to incorrect operation. With expand-modules, the client would first 727# ask for the module to be expanded: 728sub req_expandmodules 729{ 730 my ($cmd,$data) =@_; 731 732 argsplit(); 733 734$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 735 736 unless ( ref$state->{arguments} eq "ARRAY" ) 737 { 738 print "ok\n"; 739 return; 740 } 741 742 foreach my$module( @{$state->{arguments}} ) 743 { 744$log->debug("SEND : Module-expansion$module"); 745 print "Module-expansion$module\n"; 746 } 747 748 print "ok\n"; 749 statecleanup(); 750} 751 752# co\n 753# Response expected: yes. Get files from the repository. This uses any 754# previous Argument, Directory, Entry, or Modified requests, if they have 755# been sent. Arguments to this command are module names; the client cannot 756# know what directories they correspond to except by (1) just sending the 757# co request, and then seeing what directory names the server sends back in 758# its responses, and (2) the expand-modules request. 759sub req_co 760{ 761 my ($cmd,$data) =@_; 762 763 argsplit("co"); 764 765 my$module=$state->{args}[0]; 766 my$checkout_path=$module; 767 768 # use the user specified directory if we're given it 769$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 770 771$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 772 773$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 774 775$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 776 777# Grab a handle to the SQLite db and do any necessary updates 778my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 779$updater->update(); 780 781$checkout_path=~ s|/$||;# get rid of trailing slashes 782 783# Eclipse seems to need the Clear-sticky command 784# to prepare the 'Entries' file for the new directory. 785print"Clear-sticky$checkout_path/\n"; 786print$state->{CVSROOT} ."/$module/\n"; 787print"Clear-static-directory$checkout_path/\n"; 788print$state->{CVSROOT} ."/$module/\n"; 789print"Clear-sticky$checkout_path/\n";# yes, twice 790print$state->{CVSROOT} ."/$module/\n"; 791print"Template$checkout_path/\n"; 792print$state->{CVSROOT} ."/$module/\n"; 793print"0\n"; 794 795# instruct the client that we're checking out to $checkout_path 796print"E cvs checkout: Updating$checkout_path\n"; 797 798my%seendirs= (); 799my$lastdir=''; 800 801# recursive 802sub prepdir { 803my($dir,$repodir,$remotedir,$seendirs) =@_; 804my$parent= dirname($dir); 805$dir=~ s|/+$||; 806$repodir=~ s|/+$||; 807$remotedir=~ s|/+$||; 808$parent=~ s|/+$||; 809$log->debug("announcedir$dir,$repodir,$remotedir"); 810 811if($parenteq'.'||$parenteq'./') { 812$parent=''; 813} 814# recurse to announce unseen parents first 815if(length($parent) && !exists($seendirs->{$parent})) { 816 prepdir($parent,$repodir,$remotedir,$seendirs); 817} 818# Announce that we are going to modify at the parent level 819if($parent) { 820print"E cvs checkout: Updating$remotedir/$parent\n"; 821}else{ 822print"E cvs checkout: Updating$remotedir\n"; 823} 824print"Clear-sticky$remotedir/$parent/\n"; 825print"$repodir/$parent/\n"; 826 827print"Clear-static-directory$remotedir/$dir/\n"; 828print"$repodir/$dir/\n"; 829print"Clear-sticky$remotedir/$parent/\n";# yes, twice 830print"$repodir/$parent/\n"; 831print"Template$remotedir/$dir/\n"; 832print"$repodir/$dir/\n"; 833print"0\n"; 834 835$seendirs->{$dir} =1; 836} 837 838foreachmy$git( @{$updater->gethead} ) 839{ 840# Don't want to check out deleted files 841next if($git->{filehash}eq"deleted"); 842 843($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 844 845if(length($git->{dir}) &&$git->{dir}ne'./' 846&&$git->{dir}ne$lastdir) { 847unless(exists($seendirs{$git->{dir}})) { 848 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 849$checkout_path, \%seendirs); 850$lastdir=$git->{dir}; 851$seendirs{$git->{dir}} =1; 852} 853print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 854} 855 856# modification time of this file 857print"Mod-time$git->{modified}\n"; 858 859# print some information to the client 860if(defined($git->{dir} )and$git->{dir}ne"./") 861{ 862print"M U$checkout_path/$git->{dir}$git->{name}\n"; 863}else{ 864print"M U$checkout_path/$git->{name}\n"; 865} 866 867# instruct client we're sending a file to put in this path 868print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 869 870print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 871 872# this is an "entries" line 873my$kopts= kopts_from_path($git->{name}); 874print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 875# permissions 876print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 877 878# transmit file 879 transmitfile($git->{filehash}); 880} 881 882print"ok\n"; 883 884 statecleanup(); 885} 886 887# update \n 888# Response expected: yes. Actually do a cvs update command. This uses any 889# previous Argument, Directory, Entry, or Modified requests, if they have 890# been sent. The last Directory sent specifies the working directory at the 891# time of the operation. The -I option is not used--files which the client 892# can decide whether to ignore are not mentioned and the client sends the 893# Questionable request for others. 894sub req_update 895{ 896my($cmd,$data) =@_; 897 898$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 899 900 argsplit("update"); 901 902# 903# It may just be a client exploring the available heads/modules 904# in that case, list them as top level directories and leave it 905# at that. Eclipse uses this technique to offer you a list of 906# projects (heads in this case) to checkout. 907# 908if($state->{module}eq'') { 909my$heads_dir=$state->{CVSROOT} .'/refs/heads'; 910if(!opendir HEADS,$heads_dir) { 911print"E [server aborted]: Failed to open directory, " 912."$heads_dir:$!\nerror\n"; 913return0; 914} 915print"E cvs update: Updating .\n"; 916while(my$head=readdir(HEADS)) { 917if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 918print"E cvs update: New directory `$head'\n"; 919} 920} 921closedir HEADS; 922print"ok\n"; 923return1; 924} 925 926 927# Grab a handle to the SQLite db and do any necessary updates 928my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 929 930$updater->update(); 931 932 argsfromdir($updater); 933 934#$log->debug("update state : " . Dumper($state)); 935 936# foreach file specified on the command line ... 937foreachmy$filename( @{$state->{args}} ) 938{ 939$filename= filecleanup($filename); 940 941$log->debug("Processing file$filename"); 942 943# if we have a -C we should pretend we never saw modified stuff 944if(exists($state->{opt}{C} ) ) 945{ 946delete$state->{entries}{$filename}{modified_hash}; 947delete$state->{entries}{$filename}{modified_filename}; 948$state->{entries}{$filename}{unchanged} =1; 949} 950 951my$meta; 952if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 953{ 954$meta=$updater->getmeta($filename,$1); 955}else{ 956$meta=$updater->getmeta($filename); 957} 958 959if( !defined$meta) 960{ 961$meta= { 962 name =>$filename, 963 revision =>0, 964 filehash =>'added' 965}; 966} 967 968my$oldmeta=$meta; 969 970my$wrev= revparse($filename); 971 972# If the working copy is an old revision, lets get that version too for comparison. 973if(defined($wrev)and$wrev!=$meta->{revision} ) 974{ 975$oldmeta=$updater->getmeta($filename,$wrev); 976} 977 978#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 979 980# Files are up to date if the working copy and repo copy have the same revision, 981# and the working copy is unmodified _and_ the user hasn't specified -C 982next if(defined($wrev) 983and defined($meta->{revision}) 984and$wrev==$meta->{revision} 985and$state->{entries}{$filename}{unchanged} 986and not exists($state->{opt}{C} ) ); 987 988# If the working copy and repo copy have the same revision, 989# but the working copy is modified, tell the client it's modified 990if(defined($wrev) 991and defined($meta->{revision}) 992and$wrev==$meta->{revision} 993and defined($state->{entries}{$filename}{modified_hash}) 994and not exists($state->{opt}{C} ) ) 995{ 996$log->info("Tell the client the file is modified"); 997print"MT text M\n"; 998print"MT fname$filename\n"; 999print"MT newline\n";1000next;1001}10021003if($meta->{filehash}eq"deleted")1004{1005my($filepart,$dirpart) = filenamesplit($filename,1);10061007$log->info("Removing '$filename' from working copy (no longer in the repo)");10081009print"E cvs update: `$filename' is no longer in the repository\n";1010# Don't want to actually _DO_ the update if -n specified1011unless($state->{globaloptions}{-n} ) {1012print"Removed$dirpart\n";1013print"$filepart\n";1014}1015}1016elsif(not defined($state->{entries}{$filename}{modified_hash} )1017or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1018or$meta->{filehash}eq'added')1019{1020# normal update, just send the new revision (either U=Update,1021# or A=Add, or R=Remove)1022if(defined($wrev) &&$wrev<0)1023{1024$log->info("Tell the client the file is scheduled for removal");1025print"MT text R\n";1026print"MT fname$filename\n";1027print"MT newline\n";1028next;1029}1030elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) )1031{1032$log->info("Tell the client the file is scheduled for addition");1033print"MT text A\n";1034print"MT fname$filename\n";1035print"MT newline\n";1036next;10371038}1039else{1040$log->info("Updating '$filename' to ".$meta->{revision});1041print"MT +updated\n";1042print"MT text U\n";1043print"MT fname$filename\n";1044print"MT newline\n";1045print"MT -updated\n";1046}10471048my($filepart,$dirpart) = filenamesplit($filename,1);10491050# Don't want to actually _DO_ the update if -n specified1051unless($state->{globaloptions}{-n} )1052{1053if(defined($wrev) )1054{1055# instruct client we're sending a file to put in this path as a replacement1056print"Update-existing$dirpart\n";1057$log->debug("Updating existing file 'Update-existing$dirpart'");1058}else{1059# instruct client we're sending a file to put in this path as a new file1060print"Clear-static-directory$dirpart\n";1061print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1062print"Clear-sticky$dirpart\n";1063print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";10641065$log->debug("Creating new file 'Created$dirpart'");1066print"Created$dirpart\n";1067}1068print$state->{CVSROOT} ."/$state->{module}/$filename\n";10691070# this is an "entries" line1071my$kopts= kopts_from_path($filepart);1072$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1073print"/$filepart/1.$meta->{revision}//$kopts/\n";10741075# permissions1076$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1077print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10781079# transmit file1080 transmitfile($meta->{filehash});1081}1082}else{1083$log->info("Updating '$filename'");1084my($filepart,$dirpart) = filenamesplit($meta->{name},1);10851086my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/";10871088chdir$dir;1089my$file_local=$filepart.".mine";1090system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1091my$file_old=$filepart.".".$oldmeta->{revision};1092 transmitfile($oldmeta->{filehash},$file_old);1093my$file_new=$filepart.".".$meta->{revision};1094 transmitfile($meta->{filehash},$file_new);10951096# we need to merge with the local changes ( M=successful merge, C=conflict merge )1097$log->info("Merging$file_local,$file_old,$file_new");1098print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10991100$log->debug("Temporary directory for merge is$dir");11011102my$return=system("git","merge-file",$file_local,$file_old,$file_new);1103$return>>=8;11041105if($return==0)1106{1107$log->info("Merged successfully");1108print"M M$filename\n";1109$log->debug("Merged$dirpart");11101111# Don't want to actually _DO_ the update if -n specified1112unless($state->{globaloptions}{-n} )1113{1114print"Merged$dirpart\n";1115$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1116print$state->{CVSROOT} ."/$state->{module}/$filename\n";1117my$kopts= kopts_from_path($filepart);1118$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1119print"/$filepart/1.$meta->{revision}//$kopts/\n";1120}1121}1122elsif($return==1)1123{1124$log->info("Merged with conflicts");1125print"E cvs update: conflicts found in$filename\n";1126print"M C$filename\n";11271128# Don't want to actually _DO_ the update if -n specified1129unless($state->{globaloptions}{-n} )1130{1131print"Merged$dirpart\n";1132print$state->{CVSROOT} ."/$state->{module}/$filename\n";1133my$kopts= kopts_from_path($filepart);1134print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1135}1136}1137else1138{1139$log->warn("Merge failed");1140next;1141}11421143# Don't want to actually _DO_ the update if -n specified1144unless($state->{globaloptions}{-n} )1145{1146# permissions1147$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1148print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";11491150# transmit file, format is single integer on a line by itself (file1151# size) followed by the file contents1152# TODO : we should copy files in blocks1153my$data=`cat$file_local`;1154$log->debug("File size : " . length($data));1155 print length($data) . "\n";1156 print$data;1157 }11581159 chdir "/";1160 }11611162 }11631164 print "ok\n";1165}11661167sub req_ci1168{1169 my ($cmd,$data) =@_;11701171 argsplit("ci");11721173 #$log->debug("State : " . Dumper($state));11741175$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));11761177 if ($state->{method} eq 'pserver')1178 {1179 print "error 1 pserver access cannot commit\n";1180 exit;1181 }11821183 if ( -e$state->{CVSROOT} . "/index" )1184 {1185$log->warn("file 'index' already exists in the git repository");1186 print "error 1 Index already exists in git repo\n";1187 exit;1188 }11891190 # Grab a handle to the SQLite db and do any necessary updates1191 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1192$updater->update();11931194 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1195 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1196$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");11971198$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1199$ENV{GIT_INDEX_FILE} =$file_index;12001201 # Remember where the head was at the beginning.1202 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1203 chomp$parenthash;1204 if ($parenthash!~ /^[0-9a-f]{40}$/) {1205 print "error 1 pserver cannot find the current HEAD of module";1206 exit;1207 }12081209 chdir$tmpdir;12101211 # populate the temporary index based1212 system("git-read-tree",$parenthash);1213 unless ($?== 0)1214 {1215 die "Error running git-read-tree$state->{module}$file_index$!";1216 }1217$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");12181219 my@committedfiles= ();1220 my%oldmeta;12211222 # foreach file specified on the command line ...1223 foreach my$filename( @{$state->{args}} )1224 {1225 my$committedfile=$filename;1226$filename= filecleanup($filename);12271228 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );12291230 my$meta=$updater->getmeta($filename);1231$oldmeta{$filename} =$meta;12321233 my$wrev= revparse($filename);12341235 my ($filepart,$dirpart) = filenamesplit($filename);12361237 # do a checkout of the file if it part of this tree1238 if ($wrev) {1239 system('git-checkout-index', '-f', '-u',$filename);1240 unless ($?== 0) {1241 die "Error running git-checkout-index -f -u$filename:$!";1242 }1243 }12441245 my$addflag= 0;1246 my$rmflag= 0;1247$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1248$addflag= 1 unless ( -e$filename);12491250 # Do up to date checking1251 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1252 {1253 # fail everything if an up to date check fails1254 print "error 1 Up to date check failed for$filename\n";1255 chdir "/";1256 exit;1257 }12581259 push@committedfiles,$committedfile;1260$log->info("Committing$filename");12611262 system("mkdir","-p",$dirpart) unless ( -d$dirpart);12631264 unless ($rmflag)1265 {1266$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1267 rename$state->{entries}{$filename}{modified_filename},$filename;12681269 # Calculate modes to remove1270 my$invmode= "";1271 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }12721273$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1274 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1275 }12761277 if ($rmflag)1278 {1279$log->info("Removing file '$filename'");1280 unlink($filename);1281 system("git-update-index", "--remove",$filename);1282 }1283 elsif ($addflag)1284 {1285$log->info("Adding file '$filename'");1286 system("git-update-index", "--add",$filename);1287 } else {1288$log->info("Updating file '$filename'");1289 system("git-update-index",$filename);1290 }1291 }12921293 unless ( scalar(@committedfiles) > 0 )1294 {1295 print "E No files to commit\n";1296 print "ok\n";1297 chdir "/";1298 return;1299 }13001301 my$treehash= `git-write-tree`;1302 chomp$treehash;13031304$log->debug("Treehash :$treehash, Parenthash :$parenthash");13051306 # write our commit message out if we have one ...1307 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1308 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1309 print$msg_fh"\n\nvia git-CVS emulator\n";1310 close$msg_fh;13111312 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1313chomp($commithash);1314$log->info("Commit hash :$commithash");13151316unless($commithash=~/[a-zA-Z0-9]{40}/)1317{1318$log->warn("Commit failed (Invalid commit hash)");1319print"error 1 Commit failed (unknown reason)\n";1320chdir"/";1321exit;1322}13231324# Check that this is allowed, just as we would with a receive-pack1325my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1326$parenthash,$commithash);1327if( -x $cmd[0] ) {1328unless(system(@cmd) ==0)1329{1330$log->warn("Commit failed (update hook declined to update ref)");1331print"error 1 Commit failed (update hook declined)\n";1332chdir"/";1333exit;1334}1335}13361337if(system(qw(git update-ref -m),"cvsserver ci",1338"refs/heads/$state->{module}",$commithash,$parenthash)) {1339$log->warn("update-ref for$state->{module} failed.");1340print"error 1 Cannot commit -- update first\n";1341exit;1342}13431344$updater->update();13451346# foreach file specified on the command line ...1347foreachmy$filename(@committedfiles)1348{1349$filename= filecleanup($filename);13501351my$meta=$updater->getmeta($filename);1352unless(defined$meta->{revision}) {1353$meta->{revision} =1;1354}13551356my($filepart,$dirpart) = filenamesplit($filename,1);13571358$log->debug("Checked-in$dirpart:$filename");13591360print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1361if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1362{1363print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1364print"Remove-entry$dirpart\n";1365print"$filename\n";1366}else{1367if($meta->{revision} ==1) {1368print"M initial revision: 1.1\n";1369}else{1370print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1371}1372print"Checked-in$dirpart\n";1373print"$filename\n";1374my$kopts= kopts_from_path($filepart);1375print"/$filepart/1.$meta->{revision}//$kopts/\n";1376}1377}13781379chdir"/";1380print"ok\n";1381}13821383sub req_status1384{1385my($cmd,$data) =@_;13861387 argsplit("status");13881389$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1390#$log->debug("status state : " . Dumper($state));13911392# Grab a handle to the SQLite db and do any necessary updates1393my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1394$updater->update();13951396# if no files were specified, we need to work out what files we should be providing status on ...1397 argsfromdir($updater);13981399# foreach file specified on the command line ...1400foreachmy$filename( @{$state->{args}} )1401{1402$filename= filecleanup($filename);14031404my$meta=$updater->getmeta($filename);1405my$oldmeta=$meta;14061407my$wrev= revparse($filename);14081409# If the working copy is an old revision, lets get that version too for comparison.1410if(defined($wrev)and$wrev!=$meta->{revision} )1411{1412$oldmeta=$updater->getmeta($filename,$wrev);1413}14141415# TODO : All possible statuses aren't yet implemented1416my$status;1417# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1418$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1419and1420( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1421or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1422);14231424# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1425$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1426and1427($state->{entries}{$filename}{unchanged}1428or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1429);14301431# Need checkout if it exists in the repo but doesn't have a working copy1432$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );14331434# Locally modified if working copy and repo copy have the same revision but there are local changes1435$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );14361437# Needs Merge if working copy revision is less than repo copy and there are local changes1438$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );14391440$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1441$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1442$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1443$status||="File had conflicts on merge"if(0);14441445$status||="Unknown";14461447print"M ===================================================================\n";1448print"M File:$filename\tStatus:$status\n";1449if(defined($state->{entries}{$filename}{revision}) )1450{1451print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1452}else{1453print"M Working revision:\tNo entry for$filename\n";1454}1455if(defined($meta->{revision}) )1456{1457print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1458print"M Sticky Tag:\t\t(none)\n";1459print"M Sticky Date:\t\t(none)\n";1460print"M Sticky Options:\t\t(none)\n";1461}else{1462print"M Repository revision:\tNo revision control file\n";1463}1464print"M\n";1465}14661467print"ok\n";1468}14691470sub req_diff1471{1472my($cmd,$data) =@_;14731474 argsplit("diff");14751476$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1477#$log->debug("status state : " . Dumper($state));14781479my($revision1,$revision2);1480if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1481{1482$revision1=$state->{opt}{r}[0];1483$revision2=$state->{opt}{r}[1];1484}else{1485$revision1=$state->{opt}{r};1486}14871488$revision1=~s/^1\.//if(defined($revision1) );1489$revision2=~s/^1\.//if(defined($revision2) );14901491$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );14921493# Grab a handle to the SQLite db and do any necessary updates1494my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1495$updater->update();14961497# if no files were specified, we need to work out what files we should be providing status on ...1498 argsfromdir($updater);14991500# foreach file specified on the command line ...1501foreachmy$filename( @{$state->{args}} )1502{1503$filename= filecleanup($filename);15041505my($fh,$file1,$file2,$meta1,$meta2,$filediff);15061507my$wrev= revparse($filename);15081509# We need _something_ to diff against1510next unless(defined($wrev) );15111512# if we have a -r switch, use it1513if(defined($revision1) )1514{1515(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1516$meta1=$updater->getmeta($filename,$revision1);1517unless(defined($meta1)and$meta1->{filehash}ne"deleted")1518{1519print"E File$filenameat revision 1.$revision1doesn't exist\n";1520next;1521}1522 transmitfile($meta1->{filehash},$file1);1523}1524# otherwise we just use the working copy revision1525else1526{1527(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1528$meta1=$updater->getmeta($filename,$wrev);1529 transmitfile($meta1->{filehash},$file1);1530}15311532# if we have a second -r switch, use it too1533if(defined($revision2) )1534{1535(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1536$meta2=$updater->getmeta($filename,$revision2);15371538unless(defined($meta2)and$meta2->{filehash}ne"deleted")1539{1540print"E File$filenameat revision 1.$revision2doesn't exist\n";1541next;1542}15431544 transmitfile($meta2->{filehash},$file2);1545}1546# otherwise we just use the working copy1547else1548{1549$file2=$state->{entries}{$filename}{modified_filename};1550}15511552# if we have been given -r, and we don't have a $file2 yet, lets get one1553if(defined($revision1)and not defined($file2) )1554{1555(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1556$meta2=$updater->getmeta($filename,$wrev);1557 transmitfile($meta2->{filehash},$file2);1558}15591560# We need to have retrieved something useful1561next unless(defined($meta1) );15621563# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1564next if(not defined($meta2)and$wrev==$meta1->{revision}1565and1566( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1567or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1568);15691570# Apparently we only show diffs for locally modified files1571next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );15721573print"M Index:$filename\n";1574print"M ===================================================================\n";1575print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1576print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1577print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1578print"M diff ";1579foreachmy$opt(keys%{$state->{opt}} )1580{1581if(ref$state->{opt}{$opt}eq"ARRAY")1582{1583foreachmy$value( @{$state->{opt}{$opt}} )1584{1585print"-$opt$value";1586}1587}else{1588print"-$opt";1589print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1590}1591}1592print"$filename\n";15931594$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));15951596($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15971598if(exists$state->{opt}{u} )1599{1600system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1601}else{1602system("diff$file1$file2>$filediff");1603}16041605while( <$fh> )1606{1607print"M$_";1608}1609close$fh;1610}16111612print"ok\n";1613}16141615sub req_log1616{1617my($cmd,$data) =@_;16181619 argsplit("log");16201621$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1622#$log->debug("log state : " . Dumper($state));16231624my($minrev,$maxrev);1625if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1626{1627my$control=$2;1628$minrev=$1;1629$maxrev=$3;1630$minrev=~s/^1\.//if(defined($minrev) );1631$maxrev=~s/^1\.//if(defined($maxrev) );1632$minrev++if(defined($minrev)and$controleq"::");1633}16341635# Grab a handle to the SQLite db and do any necessary updates1636my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1637$updater->update();16381639# if no files were specified, we need to work out what files we should be providing status on ...1640 argsfromdir($updater);16411642# foreach file specified on the command line ...1643foreachmy$filename( @{$state->{args}} )1644{1645$filename= filecleanup($filename);16461647my$headmeta=$updater->getmeta($filename);16481649my$revisions=$updater->getlog($filename);1650my$totalrevisions=scalar(@$revisions);16511652if(defined($minrev) )1653{1654$log->debug("Removing revisions less than$minrev");1655while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1656{1657pop@$revisions;1658}1659}1660if(defined($maxrev) )1661{1662$log->debug("Removing revisions greater than$maxrev");1663while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1664{1665shift@$revisions;1666}1667}16681669next unless(scalar(@$revisions) );16701671print"M\n";1672print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1673print"M Working file:$filename\n";1674print"M head: 1.$headmeta->{revision}\n";1675print"M branch:\n";1676print"M locks: strict\n";1677print"M access list:\n";1678print"M symbolic names:\n";1679print"M keyword substitution: kv\n";1680print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1681print"M description:\n";16821683foreachmy$revision(@$revisions)1684{1685print"M ----------------------------\n";1686print"M revision 1.$revision->{revision}\n";1687# reformat the date for log output1688$revision->{modified} =sprintf('%04d/%02d/%02d%s',$3,$DATE_LIST->{$2},$1,$4)if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and defined($DATE_LIST->{$2}) );1689$revision->{author} =~s/\s+.*//;1690$revision->{author} =~s/^(.{8}).*/$1/;1691print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1692my$commitmessage=$updater->commitmessage($revision->{commithash});1693$commitmessage=~s/^/M /mg;1694print$commitmessage."\n";1695}1696print"M =============================================================================\n";1697}16981699print"ok\n";1700}17011702sub req_annotate1703{1704my($cmd,$data) =@_;17051706 argsplit("annotate");17071708$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1709#$log->debug("status state : " . Dumper($state));17101711# Grab a handle to the SQLite db and do any necessary updates1712my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1713$updater->update();17141715# if no files were specified, we need to work out what files we should be providing annotate on ...1716 argsfromdir($updater);17171718# we'll need a temporary checkout dir1719my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1720my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1721$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");17221723$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1724$ENV{GIT_INDEX_FILE} =$file_index;17251726chdir$tmpdir;17271728# foreach file specified on the command line ...1729foreachmy$filename( @{$state->{args}} )1730{1731$filename= filecleanup($filename);17321733my$meta=$updater->getmeta($filename);17341735next unless($meta->{revision} );17361737# get all the commits that this file was in1738# in dense format -- aka skip dead revisions1739my$revisions=$updater->gethistorydense($filename);1740my$lastseenin=$revisions->[0][2];17411742# populate the temporary index based on the latest commit were we saw1743# the file -- but do it cheaply without checking out any files1744# TODO: if we got a revision from the client, use that instead1745# to look up the commithash in sqlite (still good to default to1746# the current head as we do now)1747system("git-read-tree",$lastseenin);1748unless($?==0)1749{1750print"E error running git-read-tree$lastseenin$file_index$!\n";1751return;1752}1753$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");17541755# do a checkout of the file1756system('git-checkout-index','-f','-u',$filename);1757unless($?==0) {1758print"E error running git-checkout-index -f -u$filename:$!\n";1759return;1760}17611762$log->info("Annotate$filename");17631764# Prepare a file with the commits from the linearized1765# history that annotate should know about. This prevents1766# git-jsannotate telling us about commits we are hiding1767# from the client.17681769my$a_hints="$tmpdir/.annotate_hints";1770if(!open(ANNOTATEHINTS,'>',$a_hints)) {1771print"E failed to open '$a_hints' for writing:$!\n";1772return;1773}1774for(my$i=0;$i<@$revisions;$i++)1775{1776print ANNOTATEHINTS $revisions->[$i][2];1777if($i+1<@$revisions) {# have we got a parent?1778print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1779}1780print ANNOTATEHINTS "\n";1781}17821783print ANNOTATEHINTS "\n";1784close ANNOTATEHINTS1785or(print"E failed to write$a_hints:$!\n"),return;17861787my@cmd= (qw(git-annotate -l -S),$a_hints,$filename);1788if(!open(ANNOTATE,"-|",@cmd)) {1789print"E error invoking ".join(' ',@cmd) .":$!\n";1790return;1791}1792my$metadata= {};1793print"E Annotations for$filename\n";1794print"E ***************\n";1795while( <ANNOTATE> )1796{1797if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1798{1799my$commithash=$1;1800my$data=$2;1801unless(defined($metadata->{$commithash} ) )1802{1803$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1804$metadata->{$commithash}{author} =~s/\s+.*//;1805$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1806$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1807}1808printf("M 1.%-5d (%-8s%10s):%s\n",1809$metadata->{$commithash}{revision},1810$metadata->{$commithash}{author},1811$metadata->{$commithash}{modified},1812$data1813);1814}else{1815$log->warn("Error in annotate output! LINE:$_");1816print"E Annotate error\n";1817next;1818}1819}1820close ANNOTATE;1821}18221823# done; get out of the tempdir1824chdir"/";18251826print"ok\n";18271828}18291830# This method takes the state->{arguments} array and produces two new arrays.1831# The first is $state->{args} which is everything before the '--' argument, and1832# the second is $state->{files} which is everything after it.1833sub argsplit1834{1835$state->{args} = [];1836$state->{files} = [];1837$state->{opt} = {};18381839return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");18401841my$type=shift;18421843if(defined($type) )1844{1845my$opt= {};1846$opt= { A =>0, N =>0, P =>0, R =>0, c =>0, f =>0, l =>0, n =>0, p =>0, s =>0, r =>1, D =>1, d =>1, k =>1, j =>1, }if($typeeq"co");1847$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1848$opt= { A =>0, P =>0, C =>0, d =>0, f =>0, l =>0, R =>0, p =>0, k =>1, r =>1, D =>1, j =>1, I =>1, W =>1}if($typeeq"update");1849$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1850$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1851$opt= { k =>1, m =>1}if($typeeq"add");1852$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1853$opt= { l =>0, b =>0, h =>0, R =>0, t =>0, N =>0, S =>0, r =>1, d =>1, s =>1, w =>1}if($typeeq"log");185418551856while(scalar( @{$state->{arguments}} ) >0)1857{1858my$arg=shift@{$state->{arguments}};18591860next if($argeq"--");1861next unless($arg=~/\S/);18621863# if the argument looks like a switch1864if($arg=~/^-(\w)(.*)/)1865{1866# if it's a switch that takes an argument1867if($opt->{$1} )1868{1869# If this switch has already been provided1870if($opt->{$1} >1and exists($state->{opt}{$1} ) )1871{1872$state->{opt}{$1} = [$state->{opt}{$1} ];1873if(length($2) >0)1874{1875push@{$state->{opt}{$1}},$2;1876}else{1877push@{$state->{opt}{$1}},shift@{$state->{arguments}};1878}1879}else{1880# if there's extra data in the arg, use that as the argument for the switch1881if(length($2) >0)1882{1883$state->{opt}{$1} =$2;1884}else{1885$state->{opt}{$1} =shift@{$state->{arguments}};1886}1887}1888}else{1889$state->{opt}{$1} =undef;1890}1891}1892else1893{1894push@{$state->{args}},$arg;1895}1896}1897}1898else1899{1900my$mode=0;19011902foreachmy$value( @{$state->{arguments}} )1903{1904if($valueeq"--")1905{1906$mode++;1907next;1908}1909push@{$state->{args}},$valueif($mode==0);1910push@{$state->{files}},$valueif($mode==1);1911}1912}1913}19141915# This method uses $state->{directory} to populate $state->{args} with a list of filenames1916sub argsfromdir1917{1918my$updater=shift;19191920$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");19211922return if(scalar( @{$state->{args}} ) >1);19231924my@gethead= @{$updater->gethead};19251926# push added files1927foreachmy$file(keys%{$state->{entries}}) {1928if(exists$state->{entries}{$file}{revision} &&1929$state->{entries}{$file}{revision} ==0)1930{1931push@gethead, { name =>$file, filehash =>'added'};1932}1933}19341935if(scalar(@{$state->{args}}) ==1)1936{1937my$arg=$state->{args}[0];1938$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );19391940$log->info("Only one arg specified, checking for directory expansion on '$arg'");19411942foreachmy$file(@gethead)1943{1944next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1945next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1946push@{$state->{args}},$file->{name};1947}19481949shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1950}else{1951$log->info("Only one arg specified, populating file list automatically");19521953$state->{args} = [];19541955foreachmy$file(@gethead)1956{1957next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1958next unless($file->{name} =~s/^$state->{prependdir}//);1959push@{$state->{args}},$file->{name};1960}1961}1962}19631964# This method cleans up the $state variable after a command that uses arguments has run1965sub statecleanup1966{1967$state->{files} = [];1968$state->{args} = [];1969$state->{arguments} = [];1970$state->{entries} = {};1971}19721973sub revparse1974{1975my$filename=shift;19761977returnundefunless(defined($state->{entries}{$filename}{revision} ) );19781979return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1980return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);19811982returnundef;1983}19841985# This method takes a file hash and does a CVS "file transfer" which transmits the1986# size of the file, and then the file contents.1987# If a second argument $targetfile is given, the file is instead written out to1988# a file by the name of $targetfile1989sub transmitfile1990{1991my$filehash=shift;1992my$targetfile=shift;19931994if(defined($filehash)and$filehasheq"deleted")1995{1996$log->warn("filehash is 'deleted'");1997return;1998}19992000die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);20012002my$type=`git-cat-file -t$filehash`;2003 chomp$type;20042005 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );20062007 my$size= `git-cat-file -s $filehash`;2008chomp$size;20092010$log->debug("transmitfile($filehash) size=$size, type=$type");20112012if(open my$fh,'-|',"git-cat-file","blob",$filehash)2013{2014if(defined($targetfile) )2015{2016open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2017print NEWFILE $_while( <$fh> );2018close NEWFILE or die("Failed to write '$targetfile':$!");2019}else{2020print"$size\n";2021printwhile( <$fh> );2022}2023close$fhor die("Couldn't close filehandle for transmitfile():$!");2024}else{2025die("Couldn't execute git-cat-file");2026}2027}20282029# This method takes a file name, and returns ( $dirpart, $filepart ) which2030# refers to the directory portion and the file portion of the filename2031# respectively2032sub filenamesplit2033{2034my$filename=shift;2035my$fixforlocaldir=shift;20362037my($filepart,$dirpart) = ($filename,".");2038($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2039$dirpart.="/";20402041if($fixforlocaldir)2042{2043$dirpart=~s/^$state->{prependdir}//;2044}20452046return($filepart,$dirpart);2047}20482049sub filecleanup2050{2051my$filename=shift;20522053returnundefunless(defined($filename));2054if($filename=~/^\// )2055{2056print"E absolute filenames '$filename' not supported by server\n";2057returnundef;2058}20592060$filename=~s/^\.\///g;2061$filename=$state->{prependdir} .$filename;2062return$filename;2063}20642065# Given a path, this function returns a string containing the kopts2066# that should go into that path's Entries line. For example, a binary2067# file should get -kb.2068sub kopts_from_path2069{2070my($path) =@_;20712072# Once it exists, the git attributes system should be used to look up2073# what attributes apply to this path.20742075# Until then, take the setting from the config file2076unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)2077{2078# Return "" to give no special treatment to any path2079return"";2080}else{2081# Alternatively, to have all files treated as if they are binary (which2082# is more like git itself), always return the "-kb" option2083return"-kb";2084}2085}20862087package GITCVS::log;20882089####2090#### Copyright The Open University UK - 2006.2091####2092#### Authors: Martyn Smith <martyn@catalyst.net.nz>2093#### Martin Langhoff <martin@catalyst.net.nz>2094####2095####20962097use strict;2098use warnings;20992100=head1 NAME21012102GITCVS::log21032104=head1 DESCRIPTION21052106This module provides very crude logging with a similar interface to2107Log::Log4perl21082109=head1 METHODS21102111=cut21122113=head2 new21142115Creates a new log object, optionally you can specify a filename here to2116indicate the file to log to. If no log file is specified, you can specify one2117later with method setfile, or indicate you no longer want logging with method2118nofile.21192120Until one of these methods is called, all log calls will buffer messages ready2121to write out.21222123=cut2124sub new2125{2126my$class=shift;2127my$filename=shift;21282129my$self= {};21302131bless$self,$class;21322133if(defined($filename) )2134{2135open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2136}21372138return$self;2139}21402141=head2 setfile21422143This methods takes a filename, and attempts to open that file as the log file.2144If successful, all buffered data is written out to the file, and any further2145logging is written directly to the file.21462147=cut2148sub setfile2149{2150my$self=shift;2151my$filename=shift;21522153if(defined($filename) )2154{2155open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2156}21572158return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21592160while(my$line=shift@{$self->{buffer}} )2161{2162print{$self->{fh}}$line;2163}2164}21652166=head2 nofile21672168This method indicates no logging is going to be used. It flushes any entries in2169the internal buffer, and sets a flag to ensure no further data is put there.21702171=cut2172sub nofile2173{2174my$self=shift;21752176$self->{nolog} =1;21772178return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21792180$self->{buffer} = [];2181}21822183=head2 _logopen21842185Internal method. Returns true if the log file is open, false otherwise.21862187=cut2188sub _logopen2189{2190my$self=shift;21912192return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2193return0;2194}21952196=head2 debug info warn fatal21972198These four methods are wrappers to _log. They provide the actual interface for2199logging data.22002201=cut2202sub debug {my$self=shift;$self->_log("debug",@_); }2203sub info {my$self=shift;$self->_log("info",@_); }2204subwarn{my$self=shift;$self->_log("warn",@_); }2205sub fatal {my$self=shift;$self->_log("fatal",@_); }22062207=head2 _log22082209This is an internal method called by the logging functions. It generates a2210timestamp and pushes the logged line either to file, or internal buffer.22112212=cut2213sub _log2214{2215my$self=shift;2216my$level=shift;22172218return if($self->{nolog} );22192220my@time=localtime;2221my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2222$time[5] +1900,2223$time[4] +1,2224$time[3],2225$time[2],2226$time[1],2227$time[0],2228uc$level,2229);22302231if($self->_logopen)2232{2233print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2234}else{2235push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2236}2237}22382239=head2 DESTROY22402241This method simply closes the file handle if one is open22422243=cut2244sub DESTROY2245{2246my$self=shift;22472248if($self->_logopen)2249{2250close$self->{fh};2251}2252}22532254package GITCVS::updater;22552256####2257#### Copyright The Open University UK - 2006.2258####2259#### Authors: Martyn Smith <martyn@catalyst.net.nz>2260#### Martin Langhoff <martin@catalyst.net.nz>2261####2262####22632264use strict;2265use warnings;2266use DBI;22672268=head1 METHODS22692270=cut22712272=head2 new22732274=cut2275sub new2276{2277my$class=shift;2278my$config=shift;2279my$module=shift;2280my$log=shift;22812282die"Need to specify a git repository"unless(defined($config)and-d $config);2283die"Need to specify a module"unless(defined($module) );22842285$class=ref($class) ||$class;22862287my$self= {};22882289bless$self,$class;22902291$self->{module} =$module;2292$self->{git_path} =$config."/";22932294$self->{log} =$log;22952296die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );22972298$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2299$cfg->{gitcvs}{dbdriver} ||"SQLite";2300$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2301$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2302$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2303$cfg->{gitcvs}{dbuser} ||"";2304$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2305$cfg->{gitcvs}{dbpass} ||"";2306my%mapping= ( m =>$module,2307 a =>$state->{method},2308 u =>getlogin||getpwuid($<) || $<,2309 G =>$self->{git_path},2310 g => mangle_dirname($self->{git_path}),2311);2312$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2313$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;23142315die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2316die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2317$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2318$self->{dbuser},2319$self->{dbpass});2320die"Error connecting to database\n"unlessdefined$self->{dbh};23212322$self->{tables} = {};2323foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2324{2325$self->{tables}{$table} =1;2326}23272328# Construct the revision table if required2329unless($self->{tables}{revision} )2330{2331$self->{dbh}->do("2332 CREATE TABLE revision (2333 name TEXT NOT NULL,2334 revision INTEGER NOT NULL,2335 filehash TEXT NOT NULL,2336 commithash TEXT NOT NULL,2337 author TEXT NOT NULL,2338 modified TEXT NOT NULL,2339 mode TEXT NOT NULL2340 )2341 ");2342$self->{dbh}->do("2343 CREATE INDEX revision_ix12344 ON revision (name,revision)2345 ");2346$self->{dbh}->do("2347 CREATE INDEX revision_ix22348 ON revision (name,commithash)2349 ");2350}23512352# Construct the head table if required2353unless($self->{tables}{head} )2354{2355$self->{dbh}->do("2356 CREATE TABLE head (2357 name TEXT NOT NULL,2358 revision INTEGER NOT NULL,2359 filehash TEXT NOT NULL,2360 commithash TEXT NOT NULL,2361 author TEXT NOT NULL,2362 modified TEXT NOT NULL,2363 mode TEXT NOT NULL2364 )2365 ");2366$self->{dbh}->do("2367 CREATE INDEX head_ix12368 ON head (name)2369 ");2370}23712372# Construct the properties table if required2373unless($self->{tables}{properties} )2374{2375$self->{dbh}->do("2376 CREATE TABLE properties (2377 key TEXT NOT NULL PRIMARY KEY,2378 value TEXT2379 )2380 ");2381}23822383# Construct the commitmsgs table if required2384unless($self->{tables}{commitmsgs} )2385{2386$self->{dbh}->do("2387 CREATE TABLE commitmsgs (2388 key TEXT NOT NULL PRIMARY KEY,2389 value TEXT2390 )2391 ");2392}23932394return$self;2395}23962397=head2 update23982399=cut2400sub update2401{2402my$self=shift;24032404# first lets get the commit list2405$ENV{GIT_DIR} =$self->{git_path};24062407my$commitsha1=`git rev-parse$self->{module}`;2408chomp$commitsha1;24092410my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2411unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2412{2413die("Invalid module '$self->{module}'");2414}241524162417my$git_log;2418my$lastcommit=$self->_get_prop("last_commit");24192420if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2421return1;2422}24232424# Start exclusive lock here...2425$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";24262427# TODO: log processing is memory bound2428# if we can parse into a 2nd file that is in reverse order2429# we can probably do something really efficient2430my@git_log_params= ('--pretty','--parents','--topo-order');24312432if(defined$lastcommit) {2433push@git_log_params,"$lastcommit..$self->{module}";2434}else{2435push@git_log_params,$self->{module};2436}2437# git-rev-list is the backend / plumbing version of git-log2438open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";24392440my@commits;24412442my%commit= ();24432444while( <GITLOG> )2445{2446chomp;2447if(m/^commit\s+(.*)$/) {2448# on ^commit lines put the just seen commit in the stack2449# and prime things for the next one2450if(keys%commit) {2451my%copy=%commit;2452unshift@commits, \%copy;2453%commit= ();2454}2455my@parents=split(m/\s+/,$1);2456$commit{hash} =shift@parents;2457$commit{parents} = \@parents;2458}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2459# on rfc822-like lines seen before we see any message,2460# lowercase the entry and put it in the hash as key-value2461$commit{lc($1)} =$2;2462}else{2463# message lines - skip initial empty line2464# and trim whitespace2465if(!exists($commit{message}) &&m/^\s*$/) {2466# define it to mark the end of headers2467$commit{message} ='';2468next;2469}2470s/^\s+//;s/\s+$//;# trim ws2471$commit{message} .=$_."\n";2472}2473}2474close GITLOG;24752476unshift@commits, \%commitif(keys%commit);24772478# Now all the commits are in the @commits bucket2479# ordered by time DESC. for each commit that needs processing,2480# determine whether it's following the last head we've seen or if2481# it's on its own branch, grab a file list, and add whatever's changed2482# NOTE: $lastcommit refers to the last commit from previous run2483# $lastpicked is the last commit we picked in this run2484my$lastpicked;2485my$head= {};2486if(defined$lastcommit) {2487$lastpicked=$lastcommit;2488}24892490my$committotal=scalar(@commits);2491my$commitcount=0;24922493# Load the head table into $head (for cached lookups during the update process)2494foreachmy$file( @{$self->gethead()} )2495{2496$head->{$file->{name}} =$file;2497}24982499foreachmy$commit(@commits)2500{2501$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2502if(defined$lastpicked)2503{2504if(!in_array($lastpicked, @{$commit->{parents}}))2505{2506# skip, we'll see this delta2507# as part of a merge later2508# warn "skipping off-track $commit->{hash}\n";2509next;2510}elsif(@{$commit->{parents}} >1) {2511# it is a merge commit, for each parent that is2512# not $lastpicked, see if we can get a log2513# from the merge-base to that parent to put it2514# in the message as a merge summary.2515my@parents= @{$commit->{parents}};2516foreachmy$parent(@parents) {2517# git-merge-base can potentially (but rarely) throw2518# several candidate merge bases. let's assume2519# that the first one is the best one.2520if($parenteq$lastpicked) {2521next;2522}2523my$base= safe_pipe_capture('git-merge-base',2524$lastpicked,$parent);2525chomp$base;2526if($base) {2527my@merged;2528# print "want to log between $base $parent \n";2529open(GITLOG,'-|','git-log',"$base..$parent")2530or die"Cannot call git-log:$!";2531my$mergedhash;2532while(<GITLOG>) {2533chomp;2534if(!defined$mergedhash) {2535if(m/^commit\s+(.+)$/) {2536$mergedhash=$1;2537}else{2538next;2539}2540}else{2541# grab the first line that looks non-rfc8222542# aka has content after leading space2543if(m/^\s+(\S.*)$/) {2544my$title=$1;2545$title=substr($title,0,100);# truncate2546unshift@merged,"$mergedhash$title";2547undef$mergedhash;2548}2549}2550}2551close GITLOG;2552if(@merged) {2553$commit->{mergemsg} =$commit->{message};2554$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2555foreachmy$summary(@merged) {2556$commit->{mergemsg} .="\t$summary\n";2557}2558$commit->{mergemsg} .="\n\n";2559# print "Message for $commit->{hash} \n$commit->{mergemsg}";2560}2561}2562}2563}2564}25652566# convert the date to CVS-happy format2567$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);25682569if(defined($lastpicked) )2570{2571my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2572local($/) ="\0";2573while( <FILELIST> )2574{2575chomp;2576unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2577{2578die("Couldn't process git-diff-tree line :$_");2579}2580my($mode,$hash,$change) = ($1,$2,$3);2581my$name= <FILELIST>;2582chomp($name);25832584# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");25852586my$git_perms="";2587$git_perms.="r"if($mode&4);2588$git_perms.="w"if($mode&2);2589$git_perms.="x"if($mode&1);2590$git_perms="rw"if($git_permseq"");25912592if($changeeq"D")2593{2594#$log->debug("DELETE $name");2595$head->{$name} = {2596 name =>$name,2597 revision =>$head->{$name}{revision} +1,2598 filehash =>"deleted",2599 commithash =>$commit->{hash},2600 modified =>$commit->{date},2601 author =>$commit->{author},2602 mode =>$git_perms,2603};2604$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2605}2606elsif($changeeq"M")2607{2608#$log->debug("MODIFIED $name");2609$head->{$name} = {2610 name =>$name,2611 revision =>$head->{$name}{revision} +1,2612 filehash =>$hash,2613 commithash =>$commit->{hash},2614 modified =>$commit->{date},2615 author =>$commit->{author},2616 mode =>$git_perms,2617};2618$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2619}2620elsif($changeeq"A")2621{2622#$log->debug("ADDED $name");2623$head->{$name} = {2624 name =>$name,2625 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2626 filehash =>$hash,2627 commithash =>$commit->{hash},2628 modified =>$commit->{date},2629 author =>$commit->{author},2630 mode =>$git_perms,2631};2632$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2633}2634else2635{2636$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2637die;2638}2639}2640close FILELIST;2641}else{2642# this is used to detect files removed from the repo2643my$seen_files= {};26442645my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2646local$/="\0";2647while( <FILELIST> )2648{2649chomp;2650unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2651{2652die("Couldn't process git-ls-tree line :$_");2653}26542655my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);26562657$seen_files->{$git_filename} =1;26582659my($oldhash,$oldrevision,$oldmode) = (2660$head->{$git_filename}{filehash},2661$head->{$git_filename}{revision},2662$head->{$git_filename}{mode}2663);26642665if($git_perms=~/^\d\d\d(\d)\d\d/o)2666{2667$git_perms="";2668$git_perms.="r"if($1&4);2669$git_perms.="w"if($1&2);2670$git_perms.="x"if($1&1);2671}else{2672$git_perms="rw";2673}26742675# unless the file exists with the same hash, we need to update it ...2676unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2677{2678my$newrevision= ($oldrevisionor0) +1;26792680$head->{$git_filename} = {2681 name =>$git_filename,2682 revision =>$newrevision,2683 filehash =>$git_hash,2684 commithash =>$commit->{hash},2685 modified =>$commit->{date},2686 author =>$commit->{author},2687 mode =>$git_perms,2688};268926902691$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2692}2693}2694close FILELIST;26952696# Detect deleted files2697foreachmy$file(keys%$head)2698{2699unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2700{2701$head->{$file}{revision}++;2702$head->{$file}{filehash} ="deleted";2703$head->{$file}{commithash} =$commit->{hash};2704$head->{$file}{modified} =$commit->{date};2705$head->{$file}{author} =$commit->{author};27062707$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2708}2709}2710# END : "Detect deleted files"2711}271227132714if(exists$commit->{mergemsg})2715{2716$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2717}27182719$lastpicked=$commit->{hash};27202721$self->_set_prop("last_commit",$commit->{hash});2722}27232724$self->delete_head();2725foreachmy$file(keys%$head)2726{2727$self->insert_head(2728$file,2729$head->{$file}{revision},2730$head->{$file}{filehash},2731$head->{$file}{commithash},2732$head->{$file}{modified},2733$head->{$file}{author},2734$head->{$file}{mode},2735);2736}2737# invalidate the gethead cache2738$self->{gethead_cache} =undef;273927402741# Ending exclusive lock here2742$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2743}27442745sub insert_rev2746{2747my$self=shift;2748my$name=shift;2749my$revision=shift;2750my$filehash=shift;2751my$commithash=shift;2752my$modified=shift;2753my$author=shift;2754my$mode=shift;27552756my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2757$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2758}27592760sub insert_mergelog2761{2762my$self=shift;2763my$key=shift;2764my$value=shift;27652766my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2767$insert_mergelog->execute($key,$value);2768}27692770sub delete_head2771{2772my$self=shift;27732774my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2775$delete_head->execute();2776}27772778sub insert_head2779{2780my$self=shift;2781my$name=shift;2782my$revision=shift;2783my$filehash=shift;2784my$commithash=shift;2785my$modified=shift;2786my$author=shift;2787my$mode=shift;27882789my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2790$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2791}27922793sub _headrev2794{2795my$self=shift;2796my$filename=shift;27972798my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2799$db_query->execute($filename);2800my($hash,$revision,$mode) =$db_query->fetchrow_array;28012802return($hash,$revision,$mode);2803}28042805sub _get_prop2806{2807my$self=shift;2808my$key=shift;28092810my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2811$db_query->execute($key);2812my($value) =$db_query->fetchrow_array;28132814return$value;2815}28162817sub _set_prop2818{2819my$self=shift;2820my$key=shift;2821my$value=shift;28222823my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2824$db_query->execute($value,$key);28252826unless($db_query->rows)2827{2828$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2829$db_query->execute($key,$value);2830}28312832return$value;2833}28342835=head2 gethead28362837=cut28382839sub gethead2840{2841my$self=shift;28422843return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );28442845my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2846$db_query->execute();28472848my$tree= [];2849while(my$file=$db_query->fetchrow_hashref)2850{2851push@$tree,$file;2852}28532854$self->{gethead_cache} =$tree;28552856return$tree;2857}28582859=head2 getlog28602861=cut28622863sub getlog2864{2865my$self=shift;2866my$filename=shift;28672868my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2869$db_query->execute($filename);28702871my$tree= [];2872while(my$file=$db_query->fetchrow_hashref)2873{2874push@$tree,$file;2875}28762877return$tree;2878}28792880=head2 getmeta28812882This function takes a filename (with path) argument and returns a hashref of2883metadata for that file.28842885=cut28862887sub getmeta2888{2889my$self=shift;2890my$filename=shift;2891my$revision=shift;28922893my$db_query;2894if(defined($revision)and$revision=~/^\d+$/)2895{2896$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2897$db_query->execute($filename,$revision);2898}2899elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2900{2901$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2902$db_query->execute($filename,$revision);2903}else{2904$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2905$db_query->execute($filename);2906}29072908return$db_query->fetchrow_hashref;2909}29102911=head2 commitmessage29122913this function takes a commithash and returns the commit message for that commit29142915=cut2916sub commitmessage2917{2918my$self=shift;2919my$commithash=shift;29202921die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);29222923my$db_query;2924$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2925$db_query->execute($commithash);29262927my($message) =$db_query->fetchrow_array;29282929if(defined($message) )2930{2931$message.=" "if($message=~/\n$/);2932return$message;2933}29342935my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2936shift@lineswhile($lines[0] =~/\S/);2937$message=join("",@lines);2938$message.=" "if($message=~/\n$/);2939return$message;2940}29412942=head2 gethistory29432944This function takes a filename (with path) argument and returns an arrayofarrays2945containing revision,filehash,commithash ordered by revision descending29462947=cut2948sub gethistory2949{2950my$self=shift;2951my$filename=shift;29522953my$db_query;2954$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2955$db_query->execute($filename);29562957return$db_query->fetchall_arrayref;2958}29592960=head2 gethistorydense29612962This function takes a filename (with path) argument and returns an arrayofarrays2963containing revision,filehash,commithash ordered by revision descending.29642965This version of gethistory skips deleted entries -- so it is useful for annotate.2966The 'dense' part is a reference to a '--dense' option available for git-rev-list2967and other git tools that depend on it.29682969=cut2970sub gethistorydense2971{2972my$self=shift;2973my$filename=shift;29742975my$db_query;2976$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2977$db_query->execute($filename);29782979return$db_query->fetchall_arrayref;2980}29812982=head2 in_array()29832984from Array::PAT - mimics the in_array() function2985found in PHP. Yuck but works for small arrays.29862987=cut2988sub in_array2989{2990my($check,@array) =@_;2991my$retval=0;2992foreachmy$test(@array){2993if($checkeq$test){2994$retval=1;2995}2996}2997return$retval;2998}29993000=head2 safe_pipe_capture30013002an alternative to `command` that allows input to be passed as an array3003to work around shell problems with weird characters in arguments30043005=cut3006sub safe_pipe_capture {30073008my@output;30093010if(my$pid=open my$child,'-|') {3011@output= (<$child>);3012close$childor die join(' ',@_).":$!$?";3013}else{3014exec(@_)or die"$!$?";# exec() can fail the executable can't be found3015}3016returnwantarray?@output:join('',@output);3017}30183019=head2 mangle_dirname30203021create a string from a directory name that is suitable to use as3022part of a filename, mainly by converting all chars except \w.- to _30233024=cut3025sub mangle_dirname {3026my$dirname=shift;3027return unlessdefined$dirname;30283029$dirname=~s/[^\w.-]/_/g;30303031return$dirname;3032}303330341;