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_WORK_TREE} = ".";1200$ENV{GIT_INDEX_FILE} =$file_index;12011202 # Remember where the head was at the beginning.1203 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1204 chomp$parenthash;1205 if ($parenthash!~ /^[0-9a-f]{40}$/) {1206 print "error 1 pserver cannot find the current HEAD of module";1207 exit;1208 }12091210 chdir$tmpdir;12111212 # populate the temporary index based1213 system("git-read-tree",$parenthash);1214 unless ($?== 0)1215 {1216 die "Error running git-read-tree$state->{module}$file_index$!";1217 }1218$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");12191220 my@committedfiles= ();1221 my%oldmeta;12221223 # foreach file specified on the command line ...1224 foreach my$filename( @{$state->{args}} )1225 {1226 my$committedfile=$filename;1227$filename= filecleanup($filename);12281229 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );12301231 my$meta=$updater->getmeta($filename);1232$oldmeta{$filename} =$meta;12331234 my$wrev= revparse($filename);12351236 my ($filepart,$dirpart) = filenamesplit($filename);12371238 # do a checkout of the file if it part of this tree1239 if ($wrev) {1240 system('git-checkout-index', '-f', '-u',$filename);1241 unless ($?== 0) {1242 die "Error running git-checkout-index -f -u$filename:$!";1243 }1244 }12451246 my$addflag= 0;1247 my$rmflag= 0;1248$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1249$addflag= 1 unless ( -e$filename);12501251 # Do up to date checking1252 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1253 {1254 # fail everything if an up to date check fails1255 print "error 1 Up to date check failed for$filename\n";1256 chdir "/";1257 exit;1258 }12591260 push@committedfiles,$committedfile;1261$log->info("Committing$filename");12621263 system("mkdir","-p",$dirpart) unless ( -d$dirpart);12641265 unless ($rmflag)1266 {1267$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1268 rename$state->{entries}{$filename}{modified_filename},$filename;12691270 # Calculate modes to remove1271 my$invmode= "";1272 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }12731274$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1275 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1276 }12771278 if ($rmflag)1279 {1280$log->info("Removing file '$filename'");1281 unlink($filename);1282 system("git-update-index", "--remove",$filename);1283 }1284 elsif ($addflag)1285 {1286$log->info("Adding file '$filename'");1287 system("git-update-index", "--add",$filename);1288 } else {1289$log->info("Updating file '$filename'");1290 system("git-update-index",$filename);1291 }1292 }12931294 unless ( scalar(@committedfiles) > 0 )1295 {1296 print "E No files to commit\n";1297 print "ok\n";1298 chdir "/";1299 return;1300 }13011302 my$treehash= `git-write-tree`;1303 chomp$treehash;13041305$log->debug("Treehash :$treehash, Parenthash :$parenthash");13061307 # write our commit message out if we have one ...1308 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1309 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1310 print$msg_fh"\n\nvia git-CVS emulator\n";1311 close$msg_fh;13121313 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1314chomp($commithash);1315$log->info("Commit hash :$commithash");13161317unless($commithash=~/[a-zA-Z0-9]{40}/)1318{1319$log->warn("Commit failed (Invalid commit hash)");1320print"error 1 Commit failed (unknown reason)\n";1321chdir"/";1322exit;1323}13241325# Check that this is allowed, just as we would with a receive-pack1326my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1327$parenthash,$commithash);1328if( -x $cmd[0] ) {1329unless(system(@cmd) ==0)1330{1331$log->warn("Commit failed (update hook declined to update ref)");1332print"error 1 Commit failed (update hook declined)\n";1333chdir"/";1334exit;1335}1336}13371338if(system(qw(git update-ref -m),"cvsserver ci",1339"refs/heads/$state->{module}",$commithash,$parenthash)) {1340$log->warn("update-ref for$state->{module} failed.");1341print"error 1 Cannot commit -- update first\n";1342exit;1343}13441345$updater->update();13461347# foreach file specified on the command line ...1348foreachmy$filename(@committedfiles)1349{1350$filename= filecleanup($filename);13511352my$meta=$updater->getmeta($filename);1353unless(defined$meta->{revision}) {1354$meta->{revision} =1;1355}13561357my($filepart,$dirpart) = filenamesplit($filename,1);13581359$log->debug("Checked-in$dirpart:$filename");13601361print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1362if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1363{1364print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1365print"Remove-entry$dirpart\n";1366print"$filename\n";1367}else{1368if($meta->{revision} ==1) {1369print"M initial revision: 1.1\n";1370}else{1371print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1372}1373print"Checked-in$dirpart\n";1374print"$filename\n";1375my$kopts= kopts_from_path($filepart);1376print"/$filepart/1.$meta->{revision}//$kopts/\n";1377}1378}13791380chdir"/";1381print"ok\n";1382}13831384sub req_status1385{1386my($cmd,$data) =@_;13871388 argsplit("status");13891390$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1391#$log->debug("status state : " . Dumper($state));13921393# Grab a handle to the SQLite db and do any necessary updates1394my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1395$updater->update();13961397# if no files were specified, we need to work out what files we should be providing status on ...1398 argsfromdir($updater);13991400# foreach file specified on the command line ...1401foreachmy$filename( @{$state->{args}} )1402{1403$filename= filecleanup($filename);14041405my$meta=$updater->getmeta($filename);1406my$oldmeta=$meta;14071408my$wrev= revparse($filename);14091410# If the working copy is an old revision, lets get that version too for comparison.1411if(defined($wrev)and$wrev!=$meta->{revision} )1412{1413$oldmeta=$updater->getmeta($filename,$wrev);1414}14151416# TODO : All possible statuses aren't yet implemented1417my$status;1418# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1419$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1420and1421( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1422or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1423);14241425# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1426$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1427and1428($state->{entries}{$filename}{unchanged}1429or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1430);14311432# Need checkout if it exists in the repo but doesn't have a working copy1433$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );14341435# Locally modified if working copy and repo copy have the same revision but there are local changes1436$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );14371438# Needs Merge if working copy revision is less than repo copy and there are local changes1439$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );14401441$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1442$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1443$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1444$status||="File had conflicts on merge"if(0);14451446$status||="Unknown";14471448print"M ===================================================================\n";1449print"M File:$filename\tStatus:$status\n";1450if(defined($state->{entries}{$filename}{revision}) )1451{1452print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1453}else{1454print"M Working revision:\tNo entry for$filename\n";1455}1456if(defined($meta->{revision}) )1457{1458print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1459print"M Sticky Tag:\t\t(none)\n";1460print"M Sticky Date:\t\t(none)\n";1461print"M Sticky Options:\t\t(none)\n";1462}else{1463print"M Repository revision:\tNo revision control file\n";1464}1465print"M\n";1466}14671468print"ok\n";1469}14701471sub req_diff1472{1473my($cmd,$data) =@_;14741475 argsplit("diff");14761477$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1478#$log->debug("status state : " . Dumper($state));14791480my($revision1,$revision2);1481if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1482{1483$revision1=$state->{opt}{r}[0];1484$revision2=$state->{opt}{r}[1];1485}else{1486$revision1=$state->{opt}{r};1487}14881489$revision1=~s/^1\.//if(defined($revision1) );1490$revision2=~s/^1\.//if(defined($revision2) );14911492$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );14931494# Grab a handle to the SQLite db and do any necessary updates1495my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1496$updater->update();14971498# if no files were specified, we need to work out what files we should be providing status on ...1499 argsfromdir($updater);15001501# foreach file specified on the command line ...1502foreachmy$filename( @{$state->{args}} )1503{1504$filename= filecleanup($filename);15051506my($fh,$file1,$file2,$meta1,$meta2,$filediff);15071508my$wrev= revparse($filename);15091510# We need _something_ to diff against1511next unless(defined($wrev) );15121513# if we have a -r switch, use it1514if(defined($revision1) )1515{1516(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1517$meta1=$updater->getmeta($filename,$revision1);1518unless(defined($meta1)and$meta1->{filehash}ne"deleted")1519{1520print"E File$filenameat revision 1.$revision1doesn't exist\n";1521next;1522}1523 transmitfile($meta1->{filehash},$file1);1524}1525# otherwise we just use the working copy revision1526else1527{1528(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1529$meta1=$updater->getmeta($filename,$wrev);1530 transmitfile($meta1->{filehash},$file1);1531}15321533# if we have a second -r switch, use it too1534if(defined($revision2) )1535{1536(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1537$meta2=$updater->getmeta($filename,$revision2);15381539unless(defined($meta2)and$meta2->{filehash}ne"deleted")1540{1541print"E File$filenameat revision 1.$revision2doesn't exist\n";1542next;1543}15441545 transmitfile($meta2->{filehash},$file2);1546}1547# otherwise we just use the working copy1548else1549{1550$file2=$state->{entries}{$filename}{modified_filename};1551}15521553# if we have been given -r, and we don't have a $file2 yet, lets get one1554if(defined($revision1)and not defined($file2) )1555{1556(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1557$meta2=$updater->getmeta($filename,$wrev);1558 transmitfile($meta2->{filehash},$file2);1559}15601561# We need to have retrieved something useful1562next unless(defined($meta1) );15631564# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1565next if(not defined($meta2)and$wrev==$meta1->{revision}1566and1567( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1568or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1569);15701571# Apparently we only show diffs for locally modified files1572next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );15731574print"M Index:$filename\n";1575print"M ===================================================================\n";1576print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1577print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1578print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1579print"M diff ";1580foreachmy$opt(keys%{$state->{opt}} )1581{1582if(ref$state->{opt}{$opt}eq"ARRAY")1583{1584foreachmy$value( @{$state->{opt}{$opt}} )1585{1586print"-$opt$value";1587}1588}else{1589print"-$opt";1590print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1591}1592}1593print"$filename\n";15941595$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));15961597($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15981599if(exists$state->{opt}{u} )1600{1601system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1602}else{1603system("diff$file1$file2>$filediff");1604}16051606while( <$fh> )1607{1608print"M$_";1609}1610close$fh;1611}16121613print"ok\n";1614}16151616sub req_log1617{1618my($cmd,$data) =@_;16191620 argsplit("log");16211622$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1623#$log->debug("log state : " . Dumper($state));16241625my($minrev,$maxrev);1626if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1627{1628my$control=$2;1629$minrev=$1;1630$maxrev=$3;1631$minrev=~s/^1\.//if(defined($minrev) );1632$maxrev=~s/^1\.//if(defined($maxrev) );1633$minrev++if(defined($minrev)and$controleq"::");1634}16351636# Grab a handle to the SQLite db and do any necessary updates1637my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1638$updater->update();16391640# if no files were specified, we need to work out what files we should be providing status on ...1641 argsfromdir($updater);16421643# foreach file specified on the command line ...1644foreachmy$filename( @{$state->{args}} )1645{1646$filename= filecleanup($filename);16471648my$headmeta=$updater->getmeta($filename);16491650my$revisions=$updater->getlog($filename);1651my$totalrevisions=scalar(@$revisions);16521653if(defined($minrev) )1654{1655$log->debug("Removing revisions less than$minrev");1656while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1657{1658pop@$revisions;1659}1660}1661if(defined($maxrev) )1662{1663$log->debug("Removing revisions greater than$maxrev");1664while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1665{1666shift@$revisions;1667}1668}16691670next unless(scalar(@$revisions) );16711672print"M\n";1673print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1674print"M Working file:$filename\n";1675print"M head: 1.$headmeta->{revision}\n";1676print"M branch:\n";1677print"M locks: strict\n";1678print"M access list:\n";1679print"M symbolic names:\n";1680print"M keyword substitution: kv\n";1681print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1682print"M description:\n";16831684foreachmy$revision(@$revisions)1685{1686print"M ----------------------------\n";1687print"M revision 1.$revision->{revision}\n";1688# reformat the date for log output1689$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}) );1690$revision->{author} =~s/\s+.*//;1691$revision->{author} =~s/^(.{8}).*/$1/;1692print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1693my$commitmessage=$updater->commitmessage($revision->{commithash});1694$commitmessage=~s/^/M /mg;1695print$commitmessage."\n";1696}1697print"M =============================================================================\n";1698}16991700print"ok\n";1701}17021703sub req_annotate1704{1705my($cmd,$data) =@_;17061707 argsplit("annotate");17081709$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1710#$log->debug("status state : " . Dumper($state));17111712# Grab a handle to the SQLite db and do any necessary updates1713my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1714$updater->update();17151716# if no files were specified, we need to work out what files we should be providing annotate on ...1717 argsfromdir($updater);17181719# we'll need a temporary checkout dir1720my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1721my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1722$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");17231724$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1725$ENV{GIT_WORK_TREE} =".";1726$ENV{GIT_INDEX_FILE} =$file_index;17271728chdir$tmpdir;17291730# foreach file specified on the command line ...1731foreachmy$filename( @{$state->{args}} )1732{1733$filename= filecleanup($filename);17341735my$meta=$updater->getmeta($filename);17361737next unless($meta->{revision} );17381739# get all the commits that this file was in1740# in dense format -- aka skip dead revisions1741my$revisions=$updater->gethistorydense($filename);1742my$lastseenin=$revisions->[0][2];17431744# populate the temporary index based on the latest commit were we saw1745# the file -- but do it cheaply without checking out any files1746# TODO: if we got a revision from the client, use that instead1747# to look up the commithash in sqlite (still good to default to1748# the current head as we do now)1749system("git-read-tree",$lastseenin);1750unless($?==0)1751{1752print"E error running git-read-tree$lastseenin$file_index$!\n";1753return;1754}1755$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");17561757# do a checkout of the file1758system('git-checkout-index','-f','-u',$filename);1759unless($?==0) {1760print"E error running git-checkout-index -f -u$filename:$!\n";1761return;1762}17631764$log->info("Annotate$filename");17651766# Prepare a file with the commits from the linearized1767# history that annotate should know about. This prevents1768# git-jsannotate telling us about commits we are hiding1769# from the client.17701771my$a_hints="$tmpdir/.annotate_hints";1772if(!open(ANNOTATEHINTS,'>',$a_hints)) {1773print"E failed to open '$a_hints' for writing:$!\n";1774return;1775}1776for(my$i=0;$i<@$revisions;$i++)1777{1778print ANNOTATEHINTS $revisions->[$i][2];1779if($i+1<@$revisions) {# have we got a parent?1780print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1781}1782print ANNOTATEHINTS "\n";1783}17841785print ANNOTATEHINTS "\n";1786close ANNOTATEHINTS1787or(print"E failed to write$a_hints:$!\n"),return;17881789my@cmd= (qw(git-annotate -l -S),$a_hints,$filename);1790if(!open(ANNOTATE,"-|",@cmd)) {1791print"E error invoking ".join(' ',@cmd) .":$!\n";1792return;1793}1794my$metadata= {};1795print"E Annotations for$filename\n";1796print"E ***************\n";1797while( <ANNOTATE> )1798{1799if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1800{1801my$commithash=$1;1802my$data=$2;1803unless(defined($metadata->{$commithash} ) )1804{1805$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1806$metadata->{$commithash}{author} =~s/\s+.*//;1807$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1808$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1809}1810printf("M 1.%-5d (%-8s%10s):%s\n",1811$metadata->{$commithash}{revision},1812$metadata->{$commithash}{author},1813$metadata->{$commithash}{modified},1814$data1815);1816}else{1817$log->warn("Error in annotate output! LINE:$_");1818print"E Annotate error\n";1819next;1820}1821}1822close ANNOTATE;1823}18241825# done; get out of the tempdir1826chdir"/";18271828print"ok\n";18291830}18311832# This method takes the state->{arguments} array and produces two new arrays.1833# The first is $state->{args} which is everything before the '--' argument, and1834# the second is $state->{files} which is everything after it.1835sub argsplit1836{1837$state->{args} = [];1838$state->{files} = [];1839$state->{opt} = {};18401841return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");18421843my$type=shift;18441845if(defined($type) )1846{1847my$opt= {};1848$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");1849$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1850$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");1851$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1852$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1853$opt= { k =>1, m =>1}if($typeeq"add");1854$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1855$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");185618571858while(scalar( @{$state->{arguments}} ) >0)1859{1860my$arg=shift@{$state->{arguments}};18611862next if($argeq"--");1863next unless($arg=~/\S/);18641865# if the argument looks like a switch1866if($arg=~/^-(\w)(.*)/)1867{1868# if it's a switch that takes an argument1869if($opt->{$1} )1870{1871# If this switch has already been provided1872if($opt->{$1} >1and exists($state->{opt}{$1} ) )1873{1874$state->{opt}{$1} = [$state->{opt}{$1} ];1875if(length($2) >0)1876{1877push@{$state->{opt}{$1}},$2;1878}else{1879push@{$state->{opt}{$1}},shift@{$state->{arguments}};1880}1881}else{1882# if there's extra data in the arg, use that as the argument for the switch1883if(length($2) >0)1884{1885$state->{opt}{$1} =$2;1886}else{1887$state->{opt}{$1} =shift@{$state->{arguments}};1888}1889}1890}else{1891$state->{opt}{$1} =undef;1892}1893}1894else1895{1896push@{$state->{args}},$arg;1897}1898}1899}1900else1901{1902my$mode=0;19031904foreachmy$value( @{$state->{arguments}} )1905{1906if($valueeq"--")1907{1908$mode++;1909next;1910}1911push@{$state->{args}},$valueif($mode==0);1912push@{$state->{files}},$valueif($mode==1);1913}1914}1915}19161917# This method uses $state->{directory} to populate $state->{args} with a list of filenames1918sub argsfromdir1919{1920my$updater=shift;19211922$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");19231924return if(scalar( @{$state->{args}} ) >1);19251926my@gethead= @{$updater->gethead};19271928# push added files1929foreachmy$file(keys%{$state->{entries}}) {1930if(exists$state->{entries}{$file}{revision} &&1931$state->{entries}{$file}{revision} ==0)1932{1933push@gethead, { name =>$file, filehash =>'added'};1934}1935}19361937if(scalar(@{$state->{args}}) ==1)1938{1939my$arg=$state->{args}[0];1940$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );19411942$log->info("Only one arg specified, checking for directory expansion on '$arg'");19431944foreachmy$file(@gethead)1945{1946next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1947next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1948push@{$state->{args}},$file->{name};1949}19501951shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1952}else{1953$log->info("Only one arg specified, populating file list automatically");19541955$state->{args} = [];19561957foreachmy$file(@gethead)1958{1959next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1960next unless($file->{name} =~s/^$state->{prependdir}//);1961push@{$state->{args}},$file->{name};1962}1963}1964}19651966# This method cleans up the $state variable after a command that uses arguments has run1967sub statecleanup1968{1969$state->{files} = [];1970$state->{args} = [];1971$state->{arguments} = [];1972$state->{entries} = {};1973}19741975sub revparse1976{1977my$filename=shift;19781979returnundefunless(defined($state->{entries}{$filename}{revision} ) );19801981return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1982return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);19831984returnundef;1985}19861987# This method takes a file hash and does a CVS "file transfer" which transmits the1988# size of the file, and then the file contents.1989# If a second argument $targetfile is given, the file is instead written out to1990# a file by the name of $targetfile1991sub transmitfile1992{1993my$filehash=shift;1994my$targetfile=shift;19951996if(defined($filehash)and$filehasheq"deleted")1997{1998$log->warn("filehash is 'deleted'");1999return;2000}20012002die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);20032004my$type=`git-cat-file -t$filehash`;2005 chomp$type;20062007 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );20082009 my$size= `git-cat-file -s $filehash`;2010chomp$size;20112012$log->debug("transmitfile($filehash) size=$size, type=$type");20132014if(open my$fh,'-|',"git-cat-file","blob",$filehash)2015{2016if(defined($targetfile) )2017{2018open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2019print NEWFILE $_while( <$fh> );2020close NEWFILE or die("Failed to write '$targetfile':$!");2021}else{2022print"$size\n";2023printwhile( <$fh> );2024}2025close$fhor die("Couldn't close filehandle for transmitfile():$!");2026}else{2027die("Couldn't execute git-cat-file");2028}2029}20302031# This method takes a file name, and returns ( $dirpart, $filepart ) which2032# refers to the directory portion and the file portion of the filename2033# respectively2034sub filenamesplit2035{2036my$filename=shift;2037my$fixforlocaldir=shift;20382039my($filepart,$dirpart) = ($filename,".");2040($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2041$dirpart.="/";20422043if($fixforlocaldir)2044{2045$dirpart=~s/^$state->{prependdir}//;2046}20472048return($filepart,$dirpart);2049}20502051sub filecleanup2052{2053my$filename=shift;20542055returnundefunless(defined($filename));2056if($filename=~/^\// )2057{2058print"E absolute filenames '$filename' not supported by server\n";2059returnundef;2060}20612062$filename=~s/^\.\///g;2063$filename=$state->{prependdir} .$filename;2064return$filename;2065}20662067# Given a path, this function returns a string containing the kopts2068# that should go into that path's Entries line. For example, a binary2069# file should get -kb.2070sub kopts_from_path2071{2072my($path) =@_;20732074# Once it exists, the git attributes system should be used to look up2075# what attributes apply to this path.20762077# Until then, take the setting from the config file2078unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)2079{2080# Return "" to give no special treatment to any path2081return"";2082}else{2083# Alternatively, to have all files treated as if they are binary (which2084# is more like git itself), always return the "-kb" option2085return"-kb";2086}2087}20882089package GITCVS::log;20902091####2092#### Copyright The Open University UK - 2006.2093####2094#### Authors: Martyn Smith <martyn@catalyst.net.nz>2095#### Martin Langhoff <martin@catalyst.net.nz>2096####2097####20982099use strict;2100use warnings;21012102=head1 NAME21032104GITCVS::log21052106=head1 DESCRIPTION21072108This module provides very crude logging with a similar interface to2109Log::Log4perl21102111=head1 METHODS21122113=cut21142115=head2 new21162117Creates a new log object, optionally you can specify a filename here to2118indicate the file to log to. If no log file is specified, you can specify one2119later with method setfile, or indicate you no longer want logging with method2120nofile.21212122Until one of these methods is called, all log calls will buffer messages ready2123to write out.21242125=cut2126sub new2127{2128my$class=shift;2129my$filename=shift;21302131my$self= {};21322133bless$self,$class;21342135if(defined($filename) )2136{2137open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2138}21392140return$self;2141}21422143=head2 setfile21442145This methods takes a filename, and attempts to open that file as the log file.2146If successful, all buffered data is written out to the file, and any further2147logging is written directly to the file.21482149=cut2150sub setfile2151{2152my$self=shift;2153my$filename=shift;21542155if(defined($filename) )2156{2157open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2158}21592160return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21612162while(my$line=shift@{$self->{buffer}} )2163{2164print{$self->{fh}}$line;2165}2166}21672168=head2 nofile21692170This method indicates no logging is going to be used. It flushes any entries in2171the internal buffer, and sets a flag to ensure no further data is put there.21722173=cut2174sub nofile2175{2176my$self=shift;21772178$self->{nolog} =1;21792180return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21812182$self->{buffer} = [];2183}21842185=head2 _logopen21862187Internal method. Returns true if the log file is open, false otherwise.21882189=cut2190sub _logopen2191{2192my$self=shift;21932194return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2195return0;2196}21972198=head2 debug info warn fatal21992200These four methods are wrappers to _log. They provide the actual interface for2201logging data.22022203=cut2204sub debug {my$self=shift;$self->_log("debug",@_); }2205sub info {my$self=shift;$self->_log("info",@_); }2206subwarn{my$self=shift;$self->_log("warn",@_); }2207sub fatal {my$self=shift;$self->_log("fatal",@_); }22082209=head2 _log22102211This is an internal method called by the logging functions. It generates a2212timestamp and pushes the logged line either to file, or internal buffer.22132214=cut2215sub _log2216{2217my$self=shift;2218my$level=shift;22192220return if($self->{nolog} );22212222my@time=localtime;2223my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2224$time[5] +1900,2225$time[4] +1,2226$time[3],2227$time[2],2228$time[1],2229$time[0],2230uc$level,2231);22322233if($self->_logopen)2234{2235print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2236}else{2237push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2238}2239}22402241=head2 DESTROY22422243This method simply closes the file handle if one is open22442245=cut2246sub DESTROY2247{2248my$self=shift;22492250if($self->_logopen)2251{2252close$self->{fh};2253}2254}22552256package GITCVS::updater;22572258####2259#### Copyright The Open University UK - 2006.2260####2261#### Authors: Martyn Smith <martyn@catalyst.net.nz>2262#### Martin Langhoff <martin@catalyst.net.nz>2263####2264####22652266use strict;2267use warnings;2268use DBI;22692270=head1 METHODS22712272=cut22732274=head2 new22752276=cut2277sub new2278{2279my$class=shift;2280my$config=shift;2281my$module=shift;2282my$log=shift;22832284die"Need to specify a git repository"unless(defined($config)and-d $config);2285die"Need to specify a module"unless(defined($module) );22862287$class=ref($class) ||$class;22882289my$self= {};22902291bless$self,$class;22922293$self->{module} =$module;2294$self->{git_path} =$config."/";22952296$self->{log} =$log;22972298die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );22992300$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2301$cfg->{gitcvs}{dbdriver} ||"SQLite";2302$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2303$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2304$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2305$cfg->{gitcvs}{dbuser} ||"";2306$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2307$cfg->{gitcvs}{dbpass} ||"";2308my%mapping= ( m =>$module,2309 a =>$state->{method},2310 u =>getlogin||getpwuid($<) || $<,2311 G =>$self->{git_path},2312 g => mangle_dirname($self->{git_path}),2313);2314$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2315$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;23162317die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2318die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2319$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2320$self->{dbuser},2321$self->{dbpass});2322die"Error connecting to database\n"unlessdefined$self->{dbh};23232324$self->{tables} = {};2325foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2326{2327$self->{tables}{$table} =1;2328}23292330# Construct the revision table if required2331unless($self->{tables}{revision} )2332{2333$self->{dbh}->do("2334 CREATE TABLE revision (2335 name TEXT NOT NULL,2336 revision INTEGER NOT NULL,2337 filehash TEXT NOT NULL,2338 commithash TEXT NOT NULL,2339 author TEXT NOT NULL,2340 modified TEXT NOT NULL,2341 mode TEXT NOT NULL2342 )2343 ");2344$self->{dbh}->do("2345 CREATE INDEX revision_ix12346 ON revision (name,revision)2347 ");2348$self->{dbh}->do("2349 CREATE INDEX revision_ix22350 ON revision (name,commithash)2351 ");2352}23532354# Construct the head table if required2355unless($self->{tables}{head} )2356{2357$self->{dbh}->do("2358 CREATE TABLE head (2359 name TEXT NOT NULL,2360 revision INTEGER NOT NULL,2361 filehash TEXT NOT NULL,2362 commithash TEXT NOT NULL,2363 author TEXT NOT NULL,2364 modified TEXT NOT NULL,2365 mode TEXT NOT NULL2366 )2367 ");2368$self->{dbh}->do("2369 CREATE INDEX head_ix12370 ON head (name)2371 ");2372}23732374# Construct the properties table if required2375unless($self->{tables}{properties} )2376{2377$self->{dbh}->do("2378 CREATE TABLE properties (2379 key TEXT NOT NULL PRIMARY KEY,2380 value TEXT2381 )2382 ");2383}23842385# Construct the commitmsgs table if required2386unless($self->{tables}{commitmsgs} )2387{2388$self->{dbh}->do("2389 CREATE TABLE commitmsgs (2390 key TEXT NOT NULL PRIMARY KEY,2391 value TEXT2392 )2393 ");2394}23952396return$self;2397}23982399=head2 update24002401=cut2402sub update2403{2404my$self=shift;24052406# first lets get the commit list2407$ENV{GIT_DIR} =$self->{git_path};24082409my$commitsha1=`git rev-parse$self->{module}`;2410chomp$commitsha1;24112412my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2413unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2414{2415die("Invalid module '$self->{module}'");2416}241724182419my$git_log;2420my$lastcommit=$self->_get_prop("last_commit");24212422if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2423return1;2424}24252426# Start exclusive lock here...2427$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";24282429# TODO: log processing is memory bound2430# if we can parse into a 2nd file that is in reverse order2431# we can probably do something really efficient2432my@git_log_params= ('--pretty','--parents','--topo-order');24332434if(defined$lastcommit) {2435push@git_log_params,"$lastcommit..$self->{module}";2436}else{2437push@git_log_params,$self->{module};2438}2439# git-rev-list is the backend / plumbing version of git-log2440open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";24412442my@commits;24432444my%commit= ();24452446while( <GITLOG> )2447{2448chomp;2449if(m/^commit\s+(.*)$/) {2450# on ^commit lines put the just seen commit in the stack2451# and prime things for the next one2452if(keys%commit) {2453my%copy=%commit;2454unshift@commits, \%copy;2455%commit= ();2456}2457my@parents=split(m/\s+/,$1);2458$commit{hash} =shift@parents;2459$commit{parents} = \@parents;2460}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2461# on rfc822-like lines seen before we see any message,2462# lowercase the entry and put it in the hash as key-value2463$commit{lc($1)} =$2;2464}else{2465# message lines - skip initial empty line2466# and trim whitespace2467if(!exists($commit{message}) &&m/^\s*$/) {2468# define it to mark the end of headers2469$commit{message} ='';2470next;2471}2472s/^\s+//;s/\s+$//;# trim ws2473$commit{message} .=$_."\n";2474}2475}2476close GITLOG;24772478unshift@commits, \%commitif(keys%commit);24792480# Now all the commits are in the @commits bucket2481# ordered by time DESC. for each commit that needs processing,2482# determine whether it's following the last head we've seen or if2483# it's on its own branch, grab a file list, and add whatever's changed2484# NOTE: $lastcommit refers to the last commit from previous run2485# $lastpicked is the last commit we picked in this run2486my$lastpicked;2487my$head= {};2488if(defined$lastcommit) {2489$lastpicked=$lastcommit;2490}24912492my$committotal=scalar(@commits);2493my$commitcount=0;24942495# Load the head table into $head (for cached lookups during the update process)2496foreachmy$file( @{$self->gethead()} )2497{2498$head->{$file->{name}} =$file;2499}25002501foreachmy$commit(@commits)2502{2503$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2504if(defined$lastpicked)2505{2506if(!in_array($lastpicked, @{$commit->{parents}}))2507{2508# skip, we'll see this delta2509# as part of a merge later2510# warn "skipping off-track $commit->{hash}\n";2511next;2512}elsif(@{$commit->{parents}} >1) {2513# it is a merge commit, for each parent that is2514# not $lastpicked, see if we can get a log2515# from the merge-base to that parent to put it2516# in the message as a merge summary.2517my@parents= @{$commit->{parents}};2518foreachmy$parent(@parents) {2519# git-merge-base can potentially (but rarely) throw2520# several candidate merge bases. let's assume2521# that the first one is the best one.2522if($parenteq$lastpicked) {2523next;2524}2525my$base= safe_pipe_capture('git-merge-base',2526$lastpicked,$parent);2527chomp$base;2528if($base) {2529my@merged;2530# print "want to log between $base $parent \n";2531open(GITLOG,'-|','git-log',"$base..$parent")2532or die"Cannot call git-log:$!";2533my$mergedhash;2534while(<GITLOG>) {2535chomp;2536if(!defined$mergedhash) {2537if(m/^commit\s+(.+)$/) {2538$mergedhash=$1;2539}else{2540next;2541}2542}else{2543# grab the first line that looks non-rfc8222544# aka has content after leading space2545if(m/^\s+(\S.*)$/) {2546my$title=$1;2547$title=substr($title,0,100);# truncate2548unshift@merged,"$mergedhash$title";2549undef$mergedhash;2550}2551}2552}2553close GITLOG;2554if(@merged) {2555$commit->{mergemsg} =$commit->{message};2556$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2557foreachmy$summary(@merged) {2558$commit->{mergemsg} .="\t$summary\n";2559}2560$commit->{mergemsg} .="\n\n";2561# print "Message for $commit->{hash} \n$commit->{mergemsg}";2562}2563}2564}2565}2566}25672568# convert the date to CVS-happy format2569$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);25702571if(defined($lastpicked) )2572{2573my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2574local($/) ="\0";2575while( <FILELIST> )2576{2577chomp;2578unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2579{2580die("Couldn't process git-diff-tree line :$_");2581}2582my($mode,$hash,$change) = ($1,$2,$3);2583my$name= <FILELIST>;2584chomp($name);25852586# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");25872588my$git_perms="";2589$git_perms.="r"if($mode&4);2590$git_perms.="w"if($mode&2);2591$git_perms.="x"if($mode&1);2592$git_perms="rw"if($git_permseq"");25932594if($changeeq"D")2595{2596#$log->debug("DELETE $name");2597$head->{$name} = {2598 name =>$name,2599 revision =>$head->{$name}{revision} +1,2600 filehash =>"deleted",2601 commithash =>$commit->{hash},2602 modified =>$commit->{date},2603 author =>$commit->{author},2604 mode =>$git_perms,2605};2606$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2607}2608elsif($changeeq"M")2609{2610#$log->debug("MODIFIED $name");2611$head->{$name} = {2612 name =>$name,2613 revision =>$head->{$name}{revision} +1,2614 filehash =>$hash,2615 commithash =>$commit->{hash},2616 modified =>$commit->{date},2617 author =>$commit->{author},2618 mode =>$git_perms,2619};2620$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2621}2622elsif($changeeq"A")2623{2624#$log->debug("ADDED $name");2625$head->{$name} = {2626 name =>$name,2627 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2628 filehash =>$hash,2629 commithash =>$commit->{hash},2630 modified =>$commit->{date},2631 author =>$commit->{author},2632 mode =>$git_perms,2633};2634$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2635}2636else2637{2638$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2639die;2640}2641}2642close FILELIST;2643}else{2644# this is used to detect files removed from the repo2645my$seen_files= {};26462647my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2648local$/="\0";2649while( <FILELIST> )2650{2651chomp;2652unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2653{2654die("Couldn't process git-ls-tree line :$_");2655}26562657my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);26582659$seen_files->{$git_filename} =1;26602661my($oldhash,$oldrevision,$oldmode) = (2662$head->{$git_filename}{filehash},2663$head->{$git_filename}{revision},2664$head->{$git_filename}{mode}2665);26662667if($git_perms=~/^\d\d\d(\d)\d\d/o)2668{2669$git_perms="";2670$git_perms.="r"if($1&4);2671$git_perms.="w"if($1&2);2672$git_perms.="x"if($1&1);2673}else{2674$git_perms="rw";2675}26762677# unless the file exists with the same hash, we need to update it ...2678unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2679{2680my$newrevision= ($oldrevisionor0) +1;26812682$head->{$git_filename} = {2683 name =>$git_filename,2684 revision =>$newrevision,2685 filehash =>$git_hash,2686 commithash =>$commit->{hash},2687 modified =>$commit->{date},2688 author =>$commit->{author},2689 mode =>$git_perms,2690};269126922693$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2694}2695}2696close FILELIST;26972698# Detect deleted files2699foreachmy$file(keys%$head)2700{2701unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2702{2703$head->{$file}{revision}++;2704$head->{$file}{filehash} ="deleted";2705$head->{$file}{commithash} =$commit->{hash};2706$head->{$file}{modified} =$commit->{date};2707$head->{$file}{author} =$commit->{author};27082709$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2710}2711}2712# END : "Detect deleted files"2713}271427152716if(exists$commit->{mergemsg})2717{2718$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2719}27202721$lastpicked=$commit->{hash};27222723$self->_set_prop("last_commit",$commit->{hash});2724}27252726$self->delete_head();2727foreachmy$file(keys%$head)2728{2729$self->insert_head(2730$file,2731$head->{$file}{revision},2732$head->{$file}{filehash},2733$head->{$file}{commithash},2734$head->{$file}{modified},2735$head->{$file}{author},2736$head->{$file}{mode},2737);2738}2739# invalidate the gethead cache2740$self->{gethead_cache} =undef;274127422743# Ending exclusive lock here2744$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2745}27462747sub insert_rev2748{2749my$self=shift;2750my$name=shift;2751my$revision=shift;2752my$filehash=shift;2753my$commithash=shift;2754my$modified=shift;2755my$author=shift;2756my$mode=shift;27572758my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2759$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2760}27612762sub insert_mergelog2763{2764my$self=shift;2765my$key=shift;2766my$value=shift;27672768my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2769$insert_mergelog->execute($key,$value);2770}27712772sub delete_head2773{2774my$self=shift;27752776my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2777$delete_head->execute();2778}27792780sub insert_head2781{2782my$self=shift;2783my$name=shift;2784my$revision=shift;2785my$filehash=shift;2786my$commithash=shift;2787my$modified=shift;2788my$author=shift;2789my$mode=shift;27902791my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2792$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2793}27942795sub _headrev2796{2797my$self=shift;2798my$filename=shift;27992800my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2801$db_query->execute($filename);2802my($hash,$revision,$mode) =$db_query->fetchrow_array;28032804return($hash,$revision,$mode);2805}28062807sub _get_prop2808{2809my$self=shift;2810my$key=shift;28112812my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2813$db_query->execute($key);2814my($value) =$db_query->fetchrow_array;28152816return$value;2817}28182819sub _set_prop2820{2821my$self=shift;2822my$key=shift;2823my$value=shift;28242825my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2826$db_query->execute($value,$key);28272828unless($db_query->rows)2829{2830$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2831$db_query->execute($key,$value);2832}28332834return$value;2835}28362837=head2 gethead28382839=cut28402841sub gethead2842{2843my$self=shift;28442845return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );28462847my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2848$db_query->execute();28492850my$tree= [];2851while(my$file=$db_query->fetchrow_hashref)2852{2853push@$tree,$file;2854}28552856$self->{gethead_cache} =$tree;28572858return$tree;2859}28602861=head2 getlog28622863=cut28642865sub getlog2866{2867my$self=shift;2868my$filename=shift;28692870my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2871$db_query->execute($filename);28722873my$tree= [];2874while(my$file=$db_query->fetchrow_hashref)2875{2876push@$tree,$file;2877}28782879return$tree;2880}28812882=head2 getmeta28832884This function takes a filename (with path) argument and returns a hashref of2885metadata for that file.28862887=cut28882889sub getmeta2890{2891my$self=shift;2892my$filename=shift;2893my$revision=shift;28942895my$db_query;2896if(defined($revision)and$revision=~/^\d+$/)2897{2898$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2899$db_query->execute($filename,$revision);2900}2901elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2902{2903$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2904$db_query->execute($filename,$revision);2905}else{2906$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2907$db_query->execute($filename);2908}29092910return$db_query->fetchrow_hashref;2911}29122913=head2 commitmessage29142915this function takes a commithash and returns the commit message for that commit29162917=cut2918sub commitmessage2919{2920my$self=shift;2921my$commithash=shift;29222923die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);29242925my$db_query;2926$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2927$db_query->execute($commithash);29282929my($message) =$db_query->fetchrow_array;29302931if(defined($message) )2932{2933$message.=" "if($message=~/\n$/);2934return$message;2935}29362937my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2938shift@lineswhile($lines[0] =~/\S/);2939$message=join("",@lines);2940$message.=" "if($message=~/\n$/);2941return$message;2942}29432944=head2 gethistory29452946This function takes a filename (with path) argument and returns an arrayofarrays2947containing revision,filehash,commithash ordered by revision descending29482949=cut2950sub gethistory2951{2952my$self=shift;2953my$filename=shift;29542955my$db_query;2956$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2957$db_query->execute($filename);29582959return$db_query->fetchall_arrayref;2960}29612962=head2 gethistorydense29632964This function takes a filename (with path) argument and returns an arrayofarrays2965containing revision,filehash,commithash ordered by revision descending.29662967This version of gethistory skips deleted entries -- so it is useful for annotate.2968The 'dense' part is a reference to a '--dense' option available for git-rev-list2969and other git tools that depend on it.29702971=cut2972sub gethistorydense2973{2974my$self=shift;2975my$filename=shift;29762977my$db_query;2978$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2979$db_query->execute($filename);29802981return$db_query->fetchall_arrayref;2982}29832984=head2 in_array()29852986from Array::PAT - mimics the in_array() function2987found in PHP. Yuck but works for small arrays.29882989=cut2990sub in_array2991{2992my($check,@array) =@_;2993my$retval=0;2994foreachmy$test(@array){2995if($checkeq$test){2996$retval=1;2997}2998}2999return$retval;3000}30013002=head2 safe_pipe_capture30033004an alternative to `command` that allows input to be passed as an array3005to work around shell problems with weird characters in arguments30063007=cut3008sub safe_pipe_capture {30093010my@output;30113012if(my$pid=open my$child,'-|') {3013@output= (<$child>);3014close$childor die join(' ',@_).":$!$?";3015}else{3016exec(@_)or die"$!$?";# exec() can fail the executable can't be found3017}3018returnwantarray?@output:join('',@output);3019}30203021=head2 mangle_dirname30223023create a string from a directory name that is suitable to use as3024part of a filename, mainly by converting all chars except \w.- to _30253026=cut3027sub mangle_dirname {3028my$dirname=shift;3029return unlessdefined$dirname;30303031$dirname=~s/[^\w.-]/_/g;30323033return$dirname;3034}303530361;