1#!/usr/bin/perl 2 3#### 4#### This application is a CVS emulation layer for git. 5#### It is intended for clients to connect over SSH. 6#### See the documentation for more details. 7#### 8#### Copyright The Open University UK - 2006. 9#### 10#### Authors: Martyn Smith <martyn@catalyst.net.nz> 11#### Martin Langhoff <martin@catalyst.net.nz> 12#### 13#### 14#### Released under the GNU Public License, version 2. 15#### 16#### 17 18use strict; 19use warnings; 20use bytes; 21 22use Fcntl; 23use File::Temp qw/tempdir tempfile/; 24use File::Basename; 25 26my$log= GITCVS::log->new(); 27my$cfg; 28 29my$DATE_LIST= { 30 Jan =>"01", 31 Feb =>"02", 32 Mar =>"03", 33 Apr =>"04", 34 May =>"05", 35 Jun =>"06", 36 Jul =>"07", 37 Aug =>"08", 38 Sep =>"09", 39 Oct =>"10", 40 Nov =>"11", 41 Dec =>"12", 42}; 43 44# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 45$| =1; 46 47#### Definition and mappings of functions #### 48 49my$methods= { 50'Root'=> \&req_Root, 51'Valid-responses'=> \&req_Validresponses, 52'valid-requests'=> \&req_validrequests, 53'Directory'=> \&req_Directory, 54'Entry'=> \&req_Entry, 55'Modified'=> \&req_Modified, 56'Unchanged'=> \&req_Unchanged, 57'Questionable'=> \&req_Questionable, 58'Argument'=> \&req_Argument, 59'Argumentx'=> \&req_Argument, 60'expand-modules'=> \&req_expandmodules, 61'add'=> \&req_add, 62'remove'=> \&req_remove, 63'co'=> \&req_co, 64'update'=> \&req_update, 65'ci'=> \&req_ci, 66'diff'=> \&req_diff, 67'log'=> \&req_log, 68'rlog'=> \&req_log, 69'tag'=> \&req_CATCHALL, 70'status'=> \&req_status, 71'admin'=> \&req_CATCHALL, 72'history'=> \&req_CATCHALL, 73'watchers'=> \&req_CATCHALL, 74'editors'=> \&req_CATCHALL, 75'annotate'=> \&req_annotate, 76'Global_option'=> \&req_Globaloption, 77#'annotate' => \&req_CATCHALL, 78}; 79 80############################################## 81 82 83# $state holds all the bits of information the clients sends us that could 84# potentially be useful when it comes to actually _doing_ something. 85my$state= { prependdir =>''}; 86$log->info("--------------- STARTING -----------------"); 87 88my$TEMP_DIR= tempdir( CLEANUP =>1); 89$log->debug("Temporary directory is '$TEMP_DIR'"); 90 91# if we are called with a pserver argument, 92# deal with the authentication cat before entering the 93# main loop 94$state->{method} ='ext'; 95if(@ARGV&&$ARGV[0]eq'pserver') { 96$state->{method} ='pserver'; 97my$line= <STDIN>;chomp$line; 98unless($lineeq'BEGIN AUTH REQUEST') { 99die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 100} 101$line= <STDIN>;chomp$line; 102 req_Root('root',$line)# reuse Root 103or die"E Invalid root$line\n"; 104$line= <STDIN>;chomp$line; 105unless($lineeq'anonymous') { 106print"E Only anonymous user allowed via pserver\n"; 107print"I HATE YOU\n"; 108} 109$line= <STDIN>;chomp$line;# validate the password? 110$line= <STDIN>;chomp$line; 111unless($lineeq'END AUTH REQUEST') { 112die"E Do not understand$line-- expecting END AUTH REQUEST\n"; 113} 114print"I LOVE YOU\n"; 115# and now back to our regular programme... 116} 117 118# Keep going until the client closes the connection 119while(<STDIN>) 120{ 121chomp; 122 123# Check to see if we've seen this method, and call appropriate function. 124if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 125{ 126# use the $methods hash to call the appropriate sub for this command 127#$log->info("Method : $1"); 128&{$methods->{$1}}($1,$2); 129}else{ 130# log fatal because we don't understand this function. If this happens 131# we're fairly screwed because we don't know if the client is expecting 132# a response. If it is, the client will hang, we'll hang, and the whole 133# thing will be custard. 134$log->fatal("Don't understand command$_\n"); 135die("Unknown command$_"); 136} 137} 138 139$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 140$log->info("--------------- FINISH -----------------"); 141 142# Magic catchall method. 143# This is the method that will handle all commands we haven't yet 144# implemented. It simply sends a warning to the log file indicating a 145# command that hasn't been implemented has been invoked. 146sub req_CATCHALL 147{ 148my($cmd,$data) =@_; 149$log->warn("Unhandled command : req_$cmd:$data"); 150} 151 152 153# Root pathname \n 154# Response expected: no. Tell the server which CVSROOT to use. Note that 155# pathname is a local directory and not a fully qualified CVSROOT variable. 156# pathname must already exist; if creating a new root, use the init 157# request, not Root. pathname does not include the hostname of the server, 158# how to access the server, etc.; by the time the CVS protocol is in use, 159# connection, authentication, etc., are already taken care of. The Root 160# request must be sent only once, and it must be sent before any requests 161# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 162sub req_Root 163{ 164my($cmd,$data) =@_; 165$log->debug("req_Root :$data"); 166 167$state->{CVSROOT} =$data; 168 169$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 170unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 171print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 172print"E\n"; 173print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 174return0; 175} 176 177my@gitvars=`git-config -l`; 178if($?) { 179print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 180print"E\n"; 181print"error 1 - problem executing git-config\n"; 182return0; 183} 184foreachmy$line(@gitvars) 185{ 186next unless($line=~/^(.*?)\.(.*?)(?:\.(.*?))?=(.*)$/); 187unless($3) { 188$cfg->{$1}{$2} =$4; 189}else{ 190$cfg->{$1}{$2}{$3} =$4; 191} 192} 193 194unless(defined($cfg->{gitcvs}{enabled} )and$cfg->{gitcvs}{enabled} =~/^\s*(1|true|yes)\s*$/i) 195{ 196print"E GITCVS emulation needs to be enabled on this repo\n"; 197print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 198print"E\n"; 199print"error 1 GITCVS emulation disabled\n"; 200return0; 201} 202 203if(defined($cfg->{gitcvs}{logfile} ) ) 204{ 205$log->setfile($cfg->{gitcvs}{logfile}); 206}else{ 207$log->nofile(); 208} 209 210return1; 211} 212 213# Global_option option \n 214# Response expected: no. Transmit one of the global options `-q', `-Q', 215# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 216# variations (such as combining of options) are allowed. For graceful 217# handling of valid-requests, it is probably better to make new global 218# options separate requests, rather than trying to add them to this 219# request. 220sub req_Globaloption 221{ 222my($cmd,$data) =@_; 223$log->debug("req_Globaloption :$data"); 224$state->{globaloptions}{$data} =1; 225} 226 227# Valid-responses request-list \n 228# Response expected: no. Tell the server what responses the client will 229# accept. request-list is a space separated list of tokens. 230sub req_Validresponses 231{ 232my($cmd,$data) =@_; 233$log->debug("req_Validresponses :$data"); 234 235# TODO : re-enable this, currently it's not particularly useful 236#$state->{validresponses} = [ split /\s+/, $data ]; 237} 238 239# valid-requests \n 240# Response expected: yes. Ask the server to send back a Valid-requests 241# response. 242sub req_validrequests 243{ 244my($cmd,$data) =@_; 245 246$log->debug("req_validrequests"); 247 248$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 249$log->debug("SEND : ok"); 250 251print"Valid-requests ".join(" ",keys%$methods) ."\n"; 252print"ok\n"; 253} 254 255# Directory local-directory \n 256# Additional data: repository \n. Response expected: no. Tell the server 257# what directory to use. The repository should be a directory name from a 258# previous server response. Note that this both gives a default for Entry 259# and Modified and also for ci and the other commands; normal usage is to 260# send Directory for each directory in which there will be an Entry or 261# Modified, and then a final Directory for the original directory, then the 262# command. The local-directory is relative to the top level at which the 263# command is occurring (i.e. the last Directory which is sent before the 264# command); to indicate that top level, `.' should be sent for 265# local-directory. 266sub req_Directory 267{ 268my($cmd,$data) =@_; 269 270my$repository= <STDIN>; 271chomp$repository; 272 273 274$state->{localdir} =$data; 275$state->{repository} =$repository; 276$state->{path} =$repository; 277$state->{path} =~s/^$state->{CVSROOT}\///; 278$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 279$state->{path} .="/"if($state->{path} =~ /\S/ ); 280 281$state->{directory} =$state->{localdir}; 282$state->{directory} =""if($state->{directory}eq"."); 283$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 284 285if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 286{ 287$log->info("Setting prepend to '$state->{path}'"); 288$state->{prependdir} =$state->{path}; 289foreachmy$entry(keys%{$state->{entries}} ) 290{ 291$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 292delete$state->{entries}{$entry}; 293} 294} 295 296if(defined($state->{prependdir} ) ) 297{ 298$log->debug("Prepending '$state->{prependdir}' to state|directory"); 299$state->{directory} =$state->{prependdir} .$state->{directory} 300} 301$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 302} 303 304# Entry entry-line \n 305# Response expected: no. Tell the server what version of a file is on the 306# local machine. The name in entry-line is a name relative to the directory 307# most recently specified with Directory. If the user is operating on only 308# some files in a directory, Entry requests for only those files need be 309# included. If an Entry request is sent without Modified, Is-modified, or 310# Unchanged, it means the file is lost (does not exist in the working 311# directory). If both Entry and one of Modified, Is-modified, or Unchanged 312# are sent for the same file, Entry must be sent first. For a given file, 313# one can send Modified, Is-modified, or Unchanged, but not more than one 314# of these three. 315sub req_Entry 316{ 317my($cmd,$data) =@_; 318 319#$log->debug("req_Entry : $data"); 320 321my@data=split(/\//,$data); 322 323$state->{entries}{$state->{directory}.$data[1]} = { 324 revision =>$data[2], 325 conflict =>$data[3], 326 options =>$data[4], 327 tag_or_date =>$data[5], 328}; 329 330$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 331} 332 333# Questionable filename \n 334# Response expected: no. Additional data: no. Tell the server to check 335# whether filename should be ignored, and if not, next time the server 336# sends responses, send (in a M response) `?' followed by the directory and 337# filename. filename must not contain `/'; it needs to be a file in the 338# directory named by the most recent Directory request. 339sub req_Questionable 340{ 341my($cmd,$data) =@_; 342 343$log->debug("req_Questionable :$data"); 344$state->{entries}{$state->{directory}.$data}{questionable} =1; 345} 346 347# add \n 348# Response expected: yes. Add a file or directory. This uses any previous 349# Argument, Directory, Entry, or Modified requests, if they have been sent. 350# The last Directory sent specifies the working directory at the time of 351# the operation. To add a directory, send the directory to be added using 352# Directory and Argument requests. 353sub req_add 354{ 355my($cmd,$data) =@_; 356 357 argsplit("add"); 358 359my$addcount=0; 360 361foreachmy$filename( @{$state->{args}} ) 362{ 363$filename= filecleanup($filename); 364 365unless(defined($state->{entries}{$filename}{modified_filename} ) ) 366{ 367print"E cvs add: nothing known about `$filename'\n"; 368next; 369} 370# TODO : check we're not squashing an already existing file 371if(defined($state->{entries}{$filename}{revision} ) ) 372{ 373print"E cvs add: `$filename' has already been entered\n"; 374next; 375} 376 377my($filepart,$dirpart) = filenamesplit($filename,1); 378 379print"E cvs add: scheduling file `$filename' for addition\n"; 380 381print"Checked-in$dirpart\n"; 382print"$filename\n"; 383my$kopts= kopts_from_path($filepart); 384print"/$filepart/0//$kopts/\n"; 385 386$addcount++; 387} 388 389if($addcount==1) 390{ 391print"E cvs add: use `cvs commit' to add this file permanently\n"; 392} 393elsif($addcount>1) 394{ 395print"E cvs add: use `cvs commit' to add these files permanently\n"; 396} 397 398print"ok\n"; 399} 400 401# remove \n 402# Response expected: yes. Remove a file. This uses any previous Argument, 403# Directory, Entry, or Modified requests, if they have been sent. The last 404# Directory sent specifies the working directory at the time of the 405# operation. Note that this request does not actually do anything to the 406# repository; the only effect of a successful remove request is to supply 407# the client with a new entries line containing `-' to indicate a removed 408# file. In fact, the client probably could perform this operation without 409# contacting the server, although using remove may cause the server to 410# perform a few more checks. The client sends a subsequent ci request to 411# actually record the removal in the repository. 412sub req_remove 413{ 414my($cmd,$data) =@_; 415 416 argsplit("remove"); 417 418# Grab a handle to the SQLite db and do any necessary updates 419my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 420$updater->update(); 421 422#$log->debug("add state : " . Dumper($state)); 423 424my$rmcount=0; 425 426foreachmy$filename( @{$state->{args}} ) 427{ 428$filename= filecleanup($filename); 429 430if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 431{ 432print"E cvs remove: file `$filename' still in working directory\n"; 433next; 434} 435 436my$meta=$updater->getmeta($filename); 437my$wrev= revparse($filename); 438 439unless(defined($wrev) ) 440{ 441print"E cvs remove: nothing known about `$filename'\n"; 442next; 443} 444 445if(defined($wrev)and$wrev<0) 446{ 447print"E cvs remove: file `$filename' already scheduled for removal\n"; 448next; 449} 450 451unless($wrev==$meta->{revision} ) 452{ 453# TODO : not sure if the format of this message is quite correct. 454print"E cvs remove: Up to date check failed for `$filename'\n"; 455next; 456} 457 458 459my($filepart,$dirpart) = filenamesplit($filename,1); 460 461print"E cvs remove: scheduling `$filename' for removal\n"; 462 463print"Checked-in$dirpart\n"; 464print"$filename\n"; 465my$kopts= kopts_from_path($filepart); 466print"/$filepart/-1.$wrev//$kopts/\n"; 467 468$rmcount++; 469} 470 471if($rmcount==1) 472{ 473print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 474} 475elsif($rmcount>1) 476{ 477print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 478} 479 480print"ok\n"; 481} 482 483# Modified filename \n 484# Response expected: no. Additional data: mode, \n, file transmission. Send 485# the server a copy of one locally modified file. filename is a file within 486# the most recent directory sent with Directory; it must not contain `/'. 487# If the user is operating on only some files in a directory, only those 488# files need to be included. This can also be sent without Entry, if there 489# is no entry for the file. 490sub req_Modified 491{ 492my($cmd,$data) =@_; 493 494my$mode= <STDIN>; 495chomp$mode; 496my$size= <STDIN>; 497chomp$size; 498 499# Grab config information 500my$blocksize=8192; 501my$bytesleft=$size; 502my$tmp; 503 504# Get a filehandle/name to write it to 505my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 506 507# Loop over file data writing out to temporary file. 508while($bytesleft) 509{ 510$blocksize=$bytesleftif($bytesleft<$blocksize); 511read STDIN,$tmp,$blocksize; 512print$fh $tmp; 513$bytesleft-=$blocksize; 514} 515 516close$fh; 517 518# Ensure we have something sensible for the file mode 519if($mode=~/u=(\w+)/) 520{ 521$mode=$1; 522}else{ 523$mode="rw"; 524} 525 526# Save the file data in $state 527$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 528$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 529$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 530$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 531 532 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 533} 534 535# Unchanged filename\n 536# Response expected: no. Tell the server that filename has not been 537# modified in the checked out directory. The filename is a file within the 538# most recent directory sent with Directory; it must not contain `/'. 539sub req_Unchanged 540{ 541 my ($cmd,$data) =@_; 542 543$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 544 545 #$log->debug("req_Unchanged :$data"); 546} 547 548# Argument text\n 549# Response expected: no. Save argument for use in a subsequent command. 550# Arguments accumulate until an argument-using command is given, at which 551# point they are forgotten. 552# Argumentx text\n 553# Response expected: no. Append\nfollowed by text to the current argument 554# being saved. 555sub req_Argument 556{ 557 my ($cmd,$data) =@_; 558 559 # Argumentx means: append to last Argument (with a newline in front) 560 561$log->debug("$cmd:$data"); 562 563 if ($cmdeq 'Argumentx') { 564 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 565 } else { 566 push @{$state->{arguments}},$data; 567 } 568} 569 570# expand-modules\n 571# Response expected: yes. Expand the modules which are specified in the 572# arguments. Returns the data in Module-expansion responses. Note that the 573# server can assume that this is checkout or export, not rtag or rdiff; the 574# latter do not access the working directory and thus have no need to 575# expand modules on the client side. Expand may not be the best word for 576# what this request does. It does not necessarily tell you all the files 577# contained in a module, for example. Basically it is a way of telling you 578# which working directories the server needs to know about in order to 579# handle a checkout of the specified modules. For example, suppose that the 580# server has a module defined by 581# aliasmodule -a 1dir 582# That is, one can check out aliasmodule and it will take 1dir in the 583# repository and check it out to 1dir in the working directory. Now suppose 584# the client already has this module checked out and is planning on using 585# the co request to update it. Without using expand-modules, the client 586# would have two bad choices: it could either send information about all 587# working directories under the current directory, which could be 588# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 589# stands for 1dir, and neglect to send information for 1dir, which would 590# lead to incorrect operation. With expand-modules, the client would first 591# ask for the module to be expanded: 592sub req_expandmodules 593{ 594 my ($cmd,$data) =@_; 595 596 argsplit(); 597 598$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 599 600 unless ( ref$state->{arguments} eq "ARRAY" ) 601 { 602 print "ok\n"; 603 return; 604 } 605 606 foreach my$module( @{$state->{arguments}} ) 607 { 608$log->debug("SEND : Module-expansion$module"); 609 print "Module-expansion$module\n"; 610 } 611 612 print "ok\n"; 613 statecleanup(); 614} 615 616# co\n 617# Response expected: yes. Get files from the repository. This uses any 618# previous Argument, Directory, Entry, or Modified requests, if they have 619# been sent. Arguments to this command are module names; the client cannot 620# know what directories they correspond to except by (1) just sending the 621# co request, and then seeing what directory names the server sends back in 622# its responses, and (2) the expand-modules request. 623sub req_co 624{ 625 my ($cmd,$data) =@_; 626 627 argsplit("co"); 628 629 my$module=$state->{args}[0]; 630 my$checkout_path=$module; 631 632 # use the user specified directory if we're given it 633$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 634 635$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 636 637$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 638 639$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 640 641# Grab a handle to the SQLite db and do any necessary updates 642my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 643$updater->update(); 644 645$checkout_path=~ s|/$||;# get rid of trailing slashes 646 647# Eclipse seems to need the Clear-sticky command 648# to prepare the 'Entries' file for the new directory. 649print"Clear-sticky$checkout_path/\n"; 650print$state->{CVSROOT} ."/$module/\n"; 651print"Clear-static-directory$checkout_path/\n"; 652print$state->{CVSROOT} ."/$module/\n"; 653print"Clear-sticky$checkout_path/\n";# yes, twice 654print$state->{CVSROOT} ."/$module/\n"; 655print"Template$checkout_path/\n"; 656print$state->{CVSROOT} ."/$module/\n"; 657print"0\n"; 658 659# instruct the client that we're checking out to $checkout_path 660print"E cvs checkout: Updating$checkout_path\n"; 661 662my%seendirs= (); 663my$lastdir=''; 664 665# recursive 666sub prepdir { 667my($dir,$repodir,$remotedir,$seendirs) =@_; 668my$parent= dirname($dir); 669$dir=~ s|/+$||; 670$repodir=~ s|/+$||; 671$remotedir=~ s|/+$||; 672$parent=~ s|/+$||; 673$log->debug("announcedir$dir,$repodir,$remotedir"); 674 675if($parenteq'.'||$parenteq'./') { 676$parent=''; 677} 678# recurse to announce unseen parents first 679if(length($parent) && !exists($seendirs->{$parent})) { 680 prepdir($parent,$repodir,$remotedir,$seendirs); 681} 682# Announce that we are going to modify at the parent level 683if($parent) { 684print"E cvs checkout: Updating$remotedir/$parent\n"; 685}else{ 686print"E cvs checkout: Updating$remotedir\n"; 687} 688print"Clear-sticky$remotedir/$parent/\n"; 689print"$repodir/$parent/\n"; 690 691print"Clear-static-directory$remotedir/$dir/\n"; 692print"$repodir/$dir/\n"; 693print"Clear-sticky$remotedir/$parent/\n";# yes, twice 694print"$repodir/$parent/\n"; 695print"Template$remotedir/$dir/\n"; 696print"$repodir/$dir/\n"; 697print"0\n"; 698 699$seendirs->{$dir} =1; 700} 701 702foreachmy$git( @{$updater->gethead} ) 703{ 704# Don't want to check out deleted files 705next if($git->{filehash}eq"deleted"); 706 707($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 708 709if(length($git->{dir}) &&$git->{dir}ne'./' 710&&$git->{dir}ne$lastdir) { 711unless(exists($seendirs{$git->{dir}})) { 712 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 713$checkout_path, \%seendirs); 714$lastdir=$git->{dir}; 715$seendirs{$git->{dir}} =1; 716} 717print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 718} 719 720# modification time of this file 721print"Mod-time$git->{modified}\n"; 722 723# print some information to the client 724if(defined($git->{dir} )and$git->{dir}ne"./") 725{ 726print"M U$checkout_path/$git->{dir}$git->{name}\n"; 727}else{ 728print"M U$checkout_path/$git->{name}\n"; 729} 730 731# instruct client we're sending a file to put in this path 732print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 733 734print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 735 736# this is an "entries" line 737my$kopts= kopts_from_path($git->{name}); 738print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 739# permissions 740print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 741 742# transmit file 743 transmitfile($git->{filehash}); 744} 745 746print"ok\n"; 747 748 statecleanup(); 749} 750 751# update \n 752# Response expected: yes. Actually do a cvs update command. This uses any 753# previous Argument, Directory, Entry, or Modified requests, if they have 754# been sent. The last Directory sent specifies the working directory at the 755# time of the operation. The -I option is not used--files which the client 756# can decide whether to ignore are not mentioned and the client sends the 757# Questionable request for others. 758sub req_update 759{ 760my($cmd,$data) =@_; 761 762$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 763 764 argsplit("update"); 765 766# 767# It may just be a client exploring the available heads/modules 768# in that case, list them as top level directories and leave it 769# at that. Eclipse uses this technique to offer you a list of 770# projects (heads in this case) to checkout. 771# 772if($state->{module}eq'') { 773print"E cvs update: Updating .\n"; 774opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 775while(my$head=readdir(HEADS)) { 776if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 777print"E cvs update: New directory `$head'\n"; 778} 779} 780closedir HEADS; 781print"ok\n"; 782return1; 783} 784 785 786# Grab a handle to the SQLite db and do any necessary updates 787my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 788 789$updater->update(); 790 791 argsfromdir($updater); 792 793#$log->debug("update state : " . Dumper($state)); 794 795# foreach file specified on the command line ... 796foreachmy$filename( @{$state->{args}} ) 797{ 798$filename= filecleanup($filename); 799 800$log->debug("Processing file$filename"); 801 802# if we have a -C we should pretend we never saw modified stuff 803if(exists($state->{opt}{C} ) ) 804{ 805delete$state->{entries}{$filename}{modified_hash}; 806delete$state->{entries}{$filename}{modified_filename}; 807$state->{entries}{$filename}{unchanged} =1; 808} 809 810my$meta; 811if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 812{ 813$meta=$updater->getmeta($filename,$1); 814}else{ 815$meta=$updater->getmeta($filename); 816} 817 818if( !defined$meta) 819{ 820$meta= { 821 name =>$filename, 822 revision =>0, 823 filehash =>'added' 824}; 825} 826 827my$oldmeta=$meta; 828 829my$wrev= revparse($filename); 830 831# If the working copy is an old revision, lets get that version too for comparison. 832if(defined($wrev)and$wrev!=$meta->{revision} ) 833{ 834$oldmeta=$updater->getmeta($filename,$wrev); 835} 836 837#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 838 839# Files are up to date if the working copy and repo copy have the same revision, 840# and the working copy is unmodified _and_ the user hasn't specified -C 841next if(defined($wrev) 842and defined($meta->{revision}) 843and$wrev==$meta->{revision} 844and$state->{entries}{$filename}{unchanged} 845and not exists($state->{opt}{C} ) ); 846 847# If the working copy and repo copy have the same revision, 848# but the working copy is modified, tell the client it's modified 849if(defined($wrev) 850and defined($meta->{revision}) 851and$wrev==$meta->{revision} 852and not exists($state->{opt}{C} ) ) 853{ 854$log->info("Tell the client the file is modified"); 855print"MT text M\n"; 856print"MT fname$filename\n"; 857print"MT newline\n"; 858next; 859} 860 861if($meta->{filehash}eq"deleted") 862{ 863my($filepart,$dirpart) = filenamesplit($filename,1); 864 865$log->info("Removing '$filename' from working copy (no longer in the repo)"); 866 867print"E cvs update: `$filename' is no longer in the repository\n"; 868# Don't want to actually _DO_ the update if -n specified 869unless($state->{globaloptions}{-n} ) { 870print"Removed$dirpart\n"; 871print"$filepart\n"; 872} 873} 874elsif(not defined($state->{entries}{$filename}{modified_hash} ) 875or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 876or$meta->{filehash}eq'added') 877{ 878# normal update, just send the new revision (either U=Update, 879# or A=Add, or R=Remove) 880if(defined($wrev) &&$wrev<0) 881{ 882$log->info("Tell the client the file is scheduled for removal"); 883print"MT text R\n"; 884print"MT fname$filename\n"; 885print"MT newline\n"; 886next; 887} 888elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 889{ 890$log->info("Tell the client the file is scheduled for addition"); 891print"MT text A\n"; 892print"MT fname$filename\n"; 893print"MT newline\n"; 894next; 895 896} 897else{ 898$log->info("Updating '$filename' to ".$meta->{revision}); 899print"MT +updated\n"; 900print"MT text U\n"; 901print"MT fname$filename\n"; 902print"MT newline\n"; 903print"MT -updated\n"; 904} 905 906my($filepart,$dirpart) = filenamesplit($filename,1); 907 908# Don't want to actually _DO_ the update if -n specified 909unless($state->{globaloptions}{-n} ) 910{ 911if(defined($wrev) ) 912{ 913# instruct client we're sending a file to put in this path as a replacement 914print"Update-existing$dirpart\n"; 915$log->debug("Updating existing file 'Update-existing$dirpart'"); 916}else{ 917# instruct client we're sending a file to put in this path as a new file 918print"Clear-static-directory$dirpart\n"; 919print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 920print"Clear-sticky$dirpart\n"; 921print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 922 923$log->debug("Creating new file 'Created$dirpart'"); 924print"Created$dirpart\n"; 925} 926print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 927 928# this is an "entries" line 929my$kopts= kopts_from_path($filepart); 930$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 931print"/$filepart/1.$meta->{revision}//$kopts/\n"; 932 933# permissions 934$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 935print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 936 937# transmit file 938 transmitfile($meta->{filehash}); 939} 940}else{ 941$log->info("Updating '$filename'"); 942my($filepart,$dirpart) = filenamesplit($meta->{name},1); 943 944my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 945 946chdir$dir; 947my$file_local=$filepart.".mine"; 948system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 949my$file_old=$filepart.".".$oldmeta->{revision}; 950 transmitfile($oldmeta->{filehash},$file_old); 951my$file_new=$filepart.".".$meta->{revision}; 952 transmitfile($meta->{filehash},$file_new); 953 954# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 955$log->info("Merging$file_local,$file_old,$file_new"); 956print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n"; 957 958$log->debug("Temporary directory for merge is$dir"); 959 960my$return=system("git","merge-file",$file_local,$file_old,$file_new); 961$return>>=8; 962 963if($return==0) 964{ 965$log->info("Merged successfully"); 966print"M M$filename\n"; 967$log->debug("Merged$dirpart"); 968 969# Don't want to actually _DO_ the update if -n specified 970unless($state->{globaloptions}{-n} ) 971{ 972print"Merged$dirpart\n"; 973$log->debug($state->{CVSROOT} ."/$state->{module}/$filename"); 974print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 975my$kopts= kopts_from_path($filepart); 976$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 977print"/$filepart/1.$meta->{revision}//$kopts/\n"; 978} 979} 980elsif($return==1) 981{ 982$log->info("Merged with conflicts"); 983print"E cvs update: conflicts found in$filename\n"; 984print"M C$filename\n"; 985 986# Don't want to actually _DO_ the update if -n specified 987unless($state->{globaloptions}{-n} ) 988{ 989print"Merged$dirpart\n"; 990print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 991my$kopts= kopts_from_path($filepart); 992print"/$filepart/1.$meta->{revision}/+/$kopts/\n"; 993} 994} 995else 996{ 997$log->warn("Merge failed"); 998next; 999}10001001# Don't want to actually _DO_ the update if -n specified1002unless($state->{globaloptions}{-n} )1003{1004# permissions1005$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1006print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10071008# transmit file, format is single integer on a line by itself (file1009# size) followed by the file contents1010# TODO : we should copy files in blocks1011my$data=`cat$file_local`;1012$log->debug("File size : " . length($data));1013 print length($data) . "\n";1014 print$data;1015 }10161017 chdir "/";1018 }10191020 }10211022 print "ok\n";1023}10241025sub req_ci1026{1027 my ($cmd,$data) =@_;10281029 argsplit("ci");10301031 #$log->debug("State : " . Dumper($state));10321033$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10341035 if ($state->{method} eq 'pserver')1036 {1037 print "error 1 pserver access cannot commit\n";1038 exit;1039 }10401041 if ( -e$state->{CVSROOT} . "/index" )1042 {1043$log->warn("file 'index' already exists in the git repository");1044 print "error 1 Index already exists in git repo\n";1045 exit;1046 }10471048 # Grab a handle to the SQLite db and do any necessary updates1049 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1050$updater->update();10511052 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1053 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1054$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");10551056$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1057$ENV{GIT_INDEX_FILE} =$file_index;10581059 # Remember where the head was at the beginning.1060 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1061 chomp$parenthash;1062 if ($parenthash!~ /^[0-9a-f]{40}$/) {1063 print "error 1 pserver cannot find the current HEAD of module";1064 exit;1065 }10661067 chdir$tmpdir;10681069 # populate the temporary index based1070 system("git-read-tree",$parenthash);1071 unless ($?== 0)1072 {1073 die "Error running git-read-tree$state->{module}$file_index$!";1074 }1075$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");10761077 my@committedfiles= ();1078 my%oldmeta;10791080 # foreach file specified on the command line ...1081 foreach my$filename( @{$state->{args}} )1082 {1083 my$committedfile=$filename;1084$filename= filecleanup($filename);10851086 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );10871088 my$meta=$updater->getmeta($filename);1089$oldmeta{$filename} =$meta;10901091 my$wrev= revparse($filename);10921093 my ($filepart,$dirpart) = filenamesplit($filename);10941095 # do a checkout of the file if it part of this tree1096 if ($wrev) {1097 system('git-checkout-index', '-f', '-u',$filename);1098 unless ($?== 0) {1099 die "Error running git-checkout-index -f -u$filename:$!";1100 }1101 }11021103 my$addflag= 0;1104 my$rmflag= 0;1105$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1106$addflag= 1 unless ( -e$filename);11071108 # Do up to date checking1109 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1110 {1111 # fail everything if an up to date check fails1112 print "error 1 Up to date check failed for$filename\n";1113 chdir "/";1114 exit;1115 }11161117 push@committedfiles,$committedfile;1118$log->info("Committing$filename");11191120 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11211122 unless ($rmflag)1123 {1124$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1125 rename$state->{entries}{$filename}{modified_filename},$filename;11261127 # Calculate modes to remove1128 my$invmode= "";1129 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11301131$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1132 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1133 }11341135 if ($rmflag)1136 {1137$log->info("Removing file '$filename'");1138 unlink($filename);1139 system("git-update-index", "--remove",$filename);1140 }1141 elsif ($addflag)1142 {1143$log->info("Adding file '$filename'");1144 system("git-update-index", "--add",$filename);1145 } else {1146$log->info("Updating file '$filename'");1147 system("git-update-index",$filename);1148 }1149 }11501151 unless ( scalar(@committedfiles) > 0 )1152 {1153 print "E No files to commit\n";1154 print "ok\n";1155 chdir "/";1156 return;1157 }11581159 my$treehash= `git-write-tree`;1160 chomp$treehash;11611162$log->debug("Treehash :$treehash, Parenthash :$parenthash");11631164 # write our commit message out if we have one ...1165 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1166 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1167 print$msg_fh"\n\nvia git-CVS emulator\n";1168 close$msg_fh;11691170 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1171chomp($commithash);1172$log->info("Commit hash :$commithash");11731174unless($commithash=~/[a-zA-Z0-9]{40}/)1175{1176$log->warn("Commit failed (Invalid commit hash)");1177print"error 1 Commit failed (unknown reason)\n";1178chdir"/";1179exit;1180}11811182# Check that this is allowed, just as we would with a receive-pack1183my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1184$parenthash,$commithash);1185if( -x $cmd[0] ) {1186unless(system(@cmd) ==0)1187{1188$log->warn("Commit failed (update hook declined to update ref)");1189print"error 1 Commit failed (update hook declined)\n";1190chdir"/";1191exit;1192}1193}11941195if(system(qw(git update-ref -m),"cvsserver ci",1196"refs/heads/$state->{module}",$commithash,$parenthash)) {1197$log->warn("update-ref for$state->{module} failed.");1198print"error 1 Cannot commit -- update first\n";1199exit;1200}12011202$updater->update();12031204# foreach file specified on the command line ...1205foreachmy$filename(@committedfiles)1206{1207$filename= filecleanup($filename);12081209my$meta=$updater->getmeta($filename);1210unless(defined$meta->{revision}) {1211$meta->{revision} =1;1212}12131214my($filepart,$dirpart) = filenamesplit($filename,1);12151216$log->debug("Checked-in$dirpart:$filename");12171218print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1219if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1220{1221print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1222print"Remove-entry$dirpart\n";1223print"$filename\n";1224}else{1225if($meta->{revision} ==1) {1226print"M initial revision: 1.1\n";1227}else{1228print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1229}1230print"Checked-in$dirpart\n";1231print"$filename\n";1232my$kopts= kopts_from_path($filepart);1233print"/$filepart/1.$meta->{revision}//$kopts/\n";1234}1235}12361237chdir"/";1238print"ok\n";1239}12401241sub req_status1242{1243my($cmd,$data) =@_;12441245 argsplit("status");12461247$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1248#$log->debug("status state : " . Dumper($state));12491250# Grab a handle to the SQLite db and do any necessary updates1251my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1252$updater->update();12531254# if no files were specified, we need to work out what files we should be providing status on ...1255 argsfromdir($updater);12561257# foreach file specified on the command line ...1258foreachmy$filename( @{$state->{args}} )1259{1260$filename= filecleanup($filename);12611262my$meta=$updater->getmeta($filename);1263my$oldmeta=$meta;12641265my$wrev= revparse($filename);12661267# If the working copy is an old revision, lets get that version too for comparison.1268if(defined($wrev)and$wrev!=$meta->{revision} )1269{1270$oldmeta=$updater->getmeta($filename,$wrev);1271}12721273# TODO : All possible statuses aren't yet implemented1274my$status;1275# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1276$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1277and1278( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1279or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1280);12811282# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1283$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1284and1285($state->{entries}{$filename}{unchanged}1286or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1287);12881289# Need checkout if it exists in the repo but doesn't have a working copy1290$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );12911292# Locally modified if working copy and repo copy have the same revision but there are local changes1293$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );12941295# Needs Merge if working copy revision is less than repo copy and there are local changes1296$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );12971298$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1299$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1300$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1301$status||="File had conflicts on merge"if(0);13021303$status||="Unknown";13041305print"M ===================================================================\n";1306print"M File:$filename\tStatus:$status\n";1307if(defined($state->{entries}{$filename}{revision}) )1308{1309print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1310}else{1311print"M Working revision:\tNo entry for$filename\n";1312}1313if(defined($meta->{revision}) )1314{1315print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1316print"M Sticky Tag:\t\t(none)\n";1317print"M Sticky Date:\t\t(none)\n";1318print"M Sticky Options:\t\t(none)\n";1319}else{1320print"M Repository revision:\tNo revision control file\n";1321}1322print"M\n";1323}13241325print"ok\n";1326}13271328sub req_diff1329{1330my($cmd,$data) =@_;13311332 argsplit("diff");13331334$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1335#$log->debug("status state : " . Dumper($state));13361337my($revision1,$revision2);1338if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1339{1340$revision1=$state->{opt}{r}[0];1341$revision2=$state->{opt}{r}[1];1342}else{1343$revision1=$state->{opt}{r};1344}13451346$revision1=~s/^1\.//if(defined($revision1) );1347$revision2=~s/^1\.//if(defined($revision2) );13481349$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );13501351# Grab a handle to the SQLite db and do any necessary updates1352my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1353$updater->update();13541355# if no files were specified, we need to work out what files we should be providing status on ...1356 argsfromdir($updater);13571358# foreach file specified on the command line ...1359foreachmy$filename( @{$state->{args}} )1360{1361$filename= filecleanup($filename);13621363my($fh,$file1,$file2,$meta1,$meta2,$filediff);13641365my$wrev= revparse($filename);13661367# We need _something_ to diff against1368next unless(defined($wrev) );13691370# if we have a -r switch, use it1371if(defined($revision1) )1372{1373(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1374$meta1=$updater->getmeta($filename,$revision1);1375unless(defined($meta1)and$meta1->{filehash}ne"deleted")1376{1377print"E File$filenameat revision 1.$revision1doesn't exist\n";1378next;1379}1380 transmitfile($meta1->{filehash},$file1);1381}1382# otherwise we just use the working copy revision1383else1384{1385(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1386$meta1=$updater->getmeta($filename,$wrev);1387 transmitfile($meta1->{filehash},$file1);1388}13891390# if we have a second -r switch, use it too1391if(defined($revision2) )1392{1393(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1394$meta2=$updater->getmeta($filename,$revision2);13951396unless(defined($meta2)and$meta2->{filehash}ne"deleted")1397{1398print"E File$filenameat revision 1.$revision2doesn't exist\n";1399next;1400}14011402 transmitfile($meta2->{filehash},$file2);1403}1404# otherwise we just use the working copy1405else1406{1407$file2=$state->{entries}{$filename}{modified_filename};1408}14091410# if we have been given -r, and we don't have a $file2 yet, lets get one1411if(defined($revision1)and not defined($file2) )1412{1413(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1414$meta2=$updater->getmeta($filename,$wrev);1415 transmitfile($meta2->{filehash},$file2);1416}14171418# We need to have retrieved something useful1419next unless(defined($meta1) );14201421# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1422next if(not defined($meta2)and$wrev==$meta1->{revision}1423and1424( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1425or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1426);14271428# Apparently we only show diffs for locally modified files1429next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14301431print"M Index:$filename\n";1432print"M ===================================================================\n";1433print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1434print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1435print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1436print"M diff ";1437foreachmy$opt(keys%{$state->{opt}} )1438{1439if(ref$state->{opt}{$opt}eq"ARRAY")1440{1441foreachmy$value( @{$state->{opt}{$opt}} )1442{1443print"-$opt$value";1444}1445}else{1446print"-$opt";1447print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1448}1449}1450print"$filename\n";14511452$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));14531454($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);14551456if(exists$state->{opt}{u} )1457{1458system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1459}else{1460system("diff$file1$file2>$filediff");1461}14621463while( <$fh> )1464{1465print"M$_";1466}1467close$fh;1468}14691470print"ok\n";1471}14721473sub req_log1474{1475my($cmd,$data) =@_;14761477 argsplit("log");14781479$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1480#$log->debug("log state : " . Dumper($state));14811482my($minrev,$maxrev);1483if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1484{1485my$control=$2;1486$minrev=$1;1487$maxrev=$3;1488$minrev=~s/^1\.//if(defined($minrev) );1489$maxrev=~s/^1\.//if(defined($maxrev) );1490$minrev++if(defined($minrev)and$controleq"::");1491}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$headmeta=$updater->getmeta($filename);15061507my$revisions=$updater->getlog($filename);1508my$totalrevisions=scalar(@$revisions);15091510if(defined($minrev) )1511{1512$log->debug("Removing revisions less than$minrev");1513while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1514{1515pop@$revisions;1516}1517}1518if(defined($maxrev) )1519{1520$log->debug("Removing revisions greater than$maxrev");1521while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1522{1523shift@$revisions;1524}1525}15261527next unless(scalar(@$revisions) );15281529print"M\n";1530print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1531print"M Working file:$filename\n";1532print"M head: 1.$headmeta->{revision}\n";1533print"M branch:\n";1534print"M locks: strict\n";1535print"M access list:\n";1536print"M symbolic names:\n";1537print"M keyword substitution: kv\n";1538print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1539print"M description:\n";15401541foreachmy$revision(@$revisions)1542{1543print"M ----------------------------\n";1544print"M revision 1.$revision->{revision}\n";1545# reformat the date for log output1546$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}) );1547$revision->{author} =~s/\s+.*//;1548$revision->{author} =~s/^(.{8}).*/$1/;1549print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1550my$commitmessage=$updater->commitmessage($revision->{commithash});1551$commitmessage=~s/^/M /mg;1552print$commitmessage."\n";1553}1554print"M =============================================================================\n";1555}15561557print"ok\n";1558}15591560sub req_annotate1561{1562my($cmd,$data) =@_;15631564 argsplit("annotate");15651566$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1567#$log->debug("status state : " . Dumper($state));15681569# Grab a handle to the SQLite db and do any necessary updates1570my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1571$updater->update();15721573# if no files were specified, we need to work out what files we should be providing annotate on ...1574 argsfromdir($updater);15751576# we'll need a temporary checkout dir1577my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1578my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1579$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");15801581$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1582$ENV{GIT_INDEX_FILE} =$file_index;15831584chdir$tmpdir;15851586# foreach file specified on the command line ...1587foreachmy$filename( @{$state->{args}} )1588{1589$filename= filecleanup($filename);15901591my$meta=$updater->getmeta($filename);15921593next unless($meta->{revision} );15941595# get all the commits that this file was in1596# in dense format -- aka skip dead revisions1597my$revisions=$updater->gethistorydense($filename);1598my$lastseenin=$revisions->[0][2];15991600# populate the temporary index based on the latest commit were we saw1601# the file -- but do it cheaply without checking out any files1602# TODO: if we got a revision from the client, use that instead1603# to look up the commithash in sqlite (still good to default to1604# the current head as we do now)1605system("git-read-tree",$lastseenin);1606unless($?==0)1607{1608die"Error running git-read-tree$lastseenin$file_index$!";1609}1610$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16111612# do a checkout of the file1613system('git-checkout-index','-f','-u',$filename);1614unless($?==0) {1615die"Error running git-checkout-index -f -u$filename:$!";1616}16171618$log->info("Annotate$filename");16191620# Prepare a file with the commits from the linearized1621# history that annotate should know about. This prevents1622# git-jsannotate telling us about commits we are hiding1623# from the client.16241625open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1626for(my$i=0;$i<@$revisions;$i++)1627{1628print ANNOTATEHINTS $revisions->[$i][2];1629if($i+1<@$revisions) {# have we got a parent?1630print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1631}1632print ANNOTATEHINTS "\n";1633}16341635print ANNOTATEHINTS "\n";1636close ANNOTATEHINTS;16371638my$annotatecmd='git-annotate';1639open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1640or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1641my$metadata= {};1642print"E Annotations for$filename\n";1643print"E ***************\n";1644while( <ANNOTATE> )1645{1646if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1647{1648my$commithash=$1;1649my$data=$2;1650unless(defined($metadata->{$commithash} ) )1651{1652$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1653$metadata->{$commithash}{author} =~s/\s+.*//;1654$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1655$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1656}1657printf("M 1.%-5d (%-8s%10s):%s\n",1658$metadata->{$commithash}{revision},1659$metadata->{$commithash}{author},1660$metadata->{$commithash}{modified},1661$data1662);1663}else{1664$log->warn("Error in annotate output! LINE:$_");1665print"E Annotate error\n";1666next;1667}1668}1669close ANNOTATE;1670}16711672# done; get out of the tempdir1673chdir"/";16741675print"ok\n";16761677}16781679# This method takes the state->{arguments} array and produces two new arrays.1680# The first is $state->{args} which is everything before the '--' argument, and1681# the second is $state->{files} which is everything after it.1682sub argsplit1683{1684return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");16851686my$type=shift;16871688$state->{args} = [];1689$state->{files} = [];1690$state->{opt} = {};16911692if(defined($type) )1693{1694my$opt= {};1695$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");1696$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1697$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");1698$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1699$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1700$opt= { k =>1, m =>1}if($typeeq"add");1701$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1702$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");170317041705while(scalar( @{$state->{arguments}} ) >0)1706{1707my$arg=shift@{$state->{arguments}};17081709next if($argeq"--");1710next unless($arg=~/\S/);17111712# if the argument looks like a switch1713if($arg=~/^-(\w)(.*)/)1714{1715# if it's a switch that takes an argument1716if($opt->{$1} )1717{1718# If this switch has already been provided1719if($opt->{$1} >1and exists($state->{opt}{$1} ) )1720{1721$state->{opt}{$1} = [$state->{opt}{$1} ];1722if(length($2) >0)1723{1724push@{$state->{opt}{$1}},$2;1725}else{1726push@{$state->{opt}{$1}},shift@{$state->{arguments}};1727}1728}else{1729# if there's extra data in the arg, use that as the argument for the switch1730if(length($2) >0)1731{1732$state->{opt}{$1} =$2;1733}else{1734$state->{opt}{$1} =shift@{$state->{arguments}};1735}1736}1737}else{1738$state->{opt}{$1} =undef;1739}1740}1741else1742{1743push@{$state->{args}},$arg;1744}1745}1746}1747else1748{1749my$mode=0;17501751foreachmy$value( @{$state->{arguments}} )1752{1753if($valueeq"--")1754{1755$mode++;1756next;1757}1758push@{$state->{args}},$valueif($mode==0);1759push@{$state->{files}},$valueif($mode==1);1760}1761}1762}17631764# This method uses $state->{directory} to populate $state->{args} with a list of filenames1765sub argsfromdir1766{1767my$updater=shift;17681769$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");17701771return if(scalar( @{$state->{args}} ) >1);17721773my@gethead= @{$updater->gethead};17741775# push added files1776foreachmy$file(keys%{$state->{entries}}) {1777if(exists$state->{entries}{$file}{revision} &&1778$state->{entries}{$file}{revision} ==0)1779{1780push@gethead, { name =>$file, filehash =>'added'};1781}1782}17831784if(scalar(@{$state->{args}}) ==1)1785{1786my$arg=$state->{args}[0];1787$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );17881789$log->info("Only one arg specified, checking for directory expansion on '$arg'");17901791foreachmy$file(@gethead)1792{1793next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1794next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1795push@{$state->{args}},$file->{name};1796}17971798shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1799}else{1800$log->info("Only one arg specified, populating file list automatically");18011802$state->{args} = [];18031804foreachmy$file(@gethead)1805{1806next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1807next unless($file->{name} =~s/^$state->{prependdir}//);1808push@{$state->{args}},$file->{name};1809}1810}1811}18121813# This method cleans up the $state variable after a command that uses arguments has run1814sub statecleanup1815{1816$state->{files} = [];1817$state->{args} = [];1818$state->{arguments} = [];1819$state->{entries} = {};1820}18211822sub revparse1823{1824my$filename=shift;18251826returnundefunless(defined($state->{entries}{$filename}{revision} ) );18271828return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1829return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18301831returnundef;1832}18331834# This method takes a file hash and does a CVS "file transfer" which transmits the1835# size of the file, and then the file contents.1836# If a second argument $targetfile is given, the file is instead written out to1837# a file by the name of $targetfile1838sub transmitfile1839{1840my$filehash=shift;1841my$targetfile=shift;18421843if(defined($filehash)and$filehasheq"deleted")1844{1845$log->warn("filehash is 'deleted'");1846return;1847}18481849die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);18501851my$type=`git-cat-file -t$filehash`;1852 chomp$type;18531854 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );18551856 my$size= `git-cat-file -s $filehash`;1857chomp$size;18581859$log->debug("transmitfile($filehash) size=$size, type=$type");18601861if(open my$fh,'-|',"git-cat-file","blob",$filehash)1862{1863if(defined($targetfile) )1864{1865open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1866print NEWFILE $_while( <$fh> );1867close NEWFILE;1868}else{1869print"$size\n";1870printwhile( <$fh> );1871}1872close$fhor die("Couldn't close filehandle for transmitfile()");1873}else{1874die("Couldn't execute git-cat-file");1875}1876}18771878# This method takes a file name, and returns ( $dirpart, $filepart ) which1879# refers to the directory portion and the file portion of the filename1880# respectively1881sub filenamesplit1882{1883my$filename=shift;1884my$fixforlocaldir=shift;18851886my($filepart,$dirpart) = ($filename,".");1887($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1888$dirpart.="/";18891890if($fixforlocaldir)1891{1892$dirpart=~s/^$state->{prependdir}//;1893}18941895return($filepart,$dirpart);1896}18971898sub filecleanup1899{1900my$filename=shift;19011902returnundefunless(defined($filename));1903if($filename=~/^\// )1904{1905print"E absolute filenames '$filename' not supported by server\n";1906returnundef;1907}19081909$filename=~s/^\.\///g;1910$filename=$state->{prependdir} .$filename;1911return$filename;1912}19131914# Given a path, this function returns a string containing the kopts1915# that should go into that path's Entries line. For example, a binary1916# file should get -kb.1917sub kopts_from_path1918{1919my($path) =@_;19201921# Once it exists, the git attributes system should be used to look up1922# what attributes apply to this path.19231924# Until then, take the setting from the config file1925unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1926{1927# Return "" to give no special treatment to any path1928return"";1929}else{1930# Alternatively, to have all files treated as if they are binary (which1931# is more like git itself), always return the "-kb" option1932return"-kb";1933}1934}19351936package GITCVS::log;19371938####1939#### Copyright The Open University UK - 2006.1940####1941#### Authors: Martyn Smith <martyn@catalyst.net.nz>1942#### Martin Langhoff <martin@catalyst.net.nz>1943####1944####19451946use strict;1947use warnings;19481949=head1 NAME19501951GITCVS::log19521953=head1 DESCRIPTION19541955This module provides very crude logging with a similar interface to1956Log::Log4perl19571958=head1 METHODS19591960=cut19611962=head2 new19631964Creates a new log object, optionally you can specify a filename here to1965indicate the file to log to. If no log file is specified, you can specify one1966later with method setfile, or indicate you no longer want logging with method1967nofile.19681969Until one of these methods is called, all log calls will buffer messages ready1970to write out.19711972=cut1973sub new1974{1975my$class=shift;1976my$filename=shift;19771978my$self= {};19791980bless$self,$class;19811982if(defined($filename) )1983{1984open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1985}19861987return$self;1988}19891990=head2 setfile19911992This methods takes a filename, and attempts to open that file as the log file.1993If successful, all buffered data is written out to the file, and any further1994logging is written directly to the file.19951996=cut1997sub setfile1998{1999my$self=shift;2000my$filename=shift;20012002if(defined($filename) )2003{2004open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2005}20062007return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20082009while(my$line=shift@{$self->{buffer}} )2010{2011print{$self->{fh}}$line;2012}2013}20142015=head2 nofile20162017This method indicates no logging is going to be used. It flushes any entries in2018the internal buffer, and sets a flag to ensure no further data is put there.20192020=cut2021sub nofile2022{2023my$self=shift;20242025$self->{nolog} =1;20262027return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20282029$self->{buffer} = [];2030}20312032=head2 _logopen20332034Internal method. Returns true if the log file is open, false otherwise.20352036=cut2037sub _logopen2038{2039my$self=shift;20402041return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2042return0;2043}20442045=head2 debug info warn fatal20462047These four methods are wrappers to _log. They provide the actual interface for2048logging data.20492050=cut2051sub debug {my$self=shift;$self->_log("debug",@_); }2052sub info {my$self=shift;$self->_log("info",@_); }2053subwarn{my$self=shift;$self->_log("warn",@_); }2054sub fatal {my$self=shift;$self->_log("fatal",@_); }20552056=head2 _log20572058This is an internal method called by the logging functions. It generates a2059timestamp and pushes the logged line either to file, or internal buffer.20602061=cut2062sub _log2063{2064my$self=shift;2065my$level=shift;20662067return if($self->{nolog} );20682069my@time=localtime;2070my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2071$time[5] +1900,2072$time[4] +1,2073$time[3],2074$time[2],2075$time[1],2076$time[0],2077uc$level,2078);20792080if($self->_logopen)2081{2082print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2083}else{2084push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2085}2086}20872088=head2 DESTROY20892090This method simply closes the file handle if one is open20912092=cut2093sub DESTROY2094{2095my$self=shift;20962097if($self->_logopen)2098{2099close$self->{fh};2100}2101}21022103package GITCVS::updater;21042105####2106#### Copyright The Open University UK - 2006.2107####2108#### Authors: Martyn Smith <martyn@catalyst.net.nz>2109#### Martin Langhoff <martin@catalyst.net.nz>2110####2111####21122113use strict;2114use warnings;2115use DBI;21162117=head1 METHODS21182119=cut21202121=head2 new21222123=cut2124sub new2125{2126my$class=shift;2127my$config=shift;2128my$module=shift;2129my$log=shift;21302131die"Need to specify a git repository"unless(defined($config)and-d $config);2132die"Need to specify a module"unless(defined($module) );21332134$class=ref($class) ||$class;21352136my$self= {};21372138bless$self,$class;21392140$self->{dbdir} =$config."/";2141die"Database dir '$self->{dbdir}' isn't a directory"unless(defined($self->{dbdir})and-d $self->{dbdir} );21422143$self->{module} =$module;2144$self->{file} =$self->{dbdir} ."/gitcvs.$module.sqlite";21452146$self->{git_path} =$config."/";21472148$self->{log} =$log;21492150die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );21512152$self->{dbh} = DBI->connect("dbi:SQLite:dbname=".$self->{file},"","");21532154$self->{tables} = {};2155foreachmy$table($self->{dbh}->tables)2156{2157$table=~s/^"//;2158$table=~s/"$//;2159$self->{tables}{$table} =1;2160}21612162# Construct the revision table if required2163unless($self->{tables}{revision} )2164{2165$self->{dbh}->do("2166 CREATE TABLE revision (2167 name TEXT NOT NULL,2168 revision INTEGER NOT NULL,2169 filehash TEXT NOT NULL,2170 commithash TEXT NOT NULL,2171 author TEXT NOT NULL,2172 modified TEXT NOT NULL,2173 mode TEXT NOT NULL2174 )2175 ");2176$self->{dbh}->do("2177 CREATE INDEX revision_ix12178 ON revision (name,revision)2179 ");2180$self->{dbh}->do("2181 CREATE INDEX revision_ix22182 ON revision (name,commithash)2183 ");2184}21852186# Construct the head table if required2187unless($self->{tables}{head} )2188{2189$self->{dbh}->do("2190 CREATE TABLE head (2191 name TEXT NOT NULL,2192 revision INTEGER NOT NULL,2193 filehash TEXT NOT NULL,2194 commithash TEXT NOT NULL,2195 author TEXT NOT NULL,2196 modified TEXT NOT NULL,2197 mode TEXT NOT NULL2198 )2199 ");2200$self->{dbh}->do("2201 CREATE INDEX head_ix12202 ON head (name)2203 ");2204}22052206# Construct the properties table if required2207unless($self->{tables}{properties} )2208{2209$self->{dbh}->do("2210 CREATE TABLE properties (2211 key TEXT NOT NULL PRIMARY KEY,2212 value TEXT2213 )2214 ");2215}22162217# Construct the commitmsgs table if required2218unless($self->{tables}{commitmsgs} )2219{2220$self->{dbh}->do("2221 CREATE TABLE commitmsgs (2222 key TEXT NOT NULL PRIMARY KEY,2223 value TEXT2224 )2225 ");2226}22272228return$self;2229}22302231=head2 update22322233=cut2234sub update2235{2236my$self=shift;22372238# first lets get the commit list2239$ENV{GIT_DIR} =$self->{git_path};22402241my$commitsha1=`git rev-parse$self->{module}`;2242chomp$commitsha1;22432244my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2245unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2246{2247die("Invalid module '$self->{module}'");2248}224922502251my$git_log;2252my$lastcommit=$self->_get_prop("last_commit");22532254if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2255return1;2256}22572258# Start exclusive lock here...2259$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";22602261# TODO: log processing is memory bound2262# if we can parse into a 2nd file that is in reverse order2263# we can probably do something really efficient2264my@git_log_params= ('--pretty','--parents','--topo-order');22652266if(defined$lastcommit) {2267push@git_log_params,"$lastcommit..$self->{module}";2268}else{2269push@git_log_params,$self->{module};2270}2271# git-rev-list is the backend / plumbing version of git-log2272open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";22732274my@commits;22752276my%commit= ();22772278while( <GITLOG> )2279{2280chomp;2281if(m/^commit\s+(.*)$/) {2282# on ^commit lines put the just seen commit in the stack2283# and prime things for the next one2284if(keys%commit) {2285my%copy=%commit;2286unshift@commits, \%copy;2287%commit= ();2288}2289my@parents=split(m/\s+/,$1);2290$commit{hash} =shift@parents;2291$commit{parents} = \@parents;2292}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2293# on rfc822-like lines seen before we see any message,2294# lowercase the entry and put it in the hash as key-value2295$commit{lc($1)} =$2;2296}else{2297# message lines - skip initial empty line2298# and trim whitespace2299if(!exists($commit{message}) &&m/^\s*$/) {2300# define it to mark the end of headers2301$commit{message} ='';2302next;2303}2304s/^\s+//;s/\s+$//;# trim ws2305$commit{message} .=$_."\n";2306}2307}2308close GITLOG;23092310unshift@commits, \%commitif(keys%commit);23112312# Now all the commits are in the @commits bucket2313# ordered by time DESC. for each commit that needs processing,2314# determine whether it's following the last head we've seen or if2315# it's on its own branch, grab a file list, and add whatever's changed2316# NOTE: $lastcommit refers to the last commit from previous run2317# $lastpicked is the last commit we picked in this run2318my$lastpicked;2319my$head= {};2320if(defined$lastcommit) {2321$lastpicked=$lastcommit;2322}23232324my$committotal=scalar(@commits);2325my$commitcount=0;23262327# Load the head table into $head (for cached lookups during the update process)2328foreachmy$file( @{$self->gethead()} )2329{2330$head->{$file->{name}} =$file;2331}23322333foreachmy$commit(@commits)2334{2335$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2336if(defined$lastpicked)2337{2338if(!in_array($lastpicked, @{$commit->{parents}}))2339{2340# skip, we'll see this delta2341# as part of a merge later2342# warn "skipping off-track $commit->{hash}\n";2343next;2344}elsif(@{$commit->{parents}} >1) {2345# it is a merge commit, for each parent that is2346# not $lastpicked, see if we can get a log2347# from the merge-base to that parent to put it2348# in the message as a merge summary.2349my@parents= @{$commit->{parents}};2350foreachmy$parent(@parents) {2351# git-merge-base can potentially (but rarely) throw2352# several candidate merge bases. let's assume2353# that the first one is the best one.2354if($parenteq$lastpicked) {2355next;2356}2357open my$p,'git-merge-base '.$lastpicked.' '2358.$parent.'|';2359my@output= (<$p>);2360close$p;2361my$base=join('',@output);2362chomp$base;2363if($base) {2364my@merged;2365# print "want to log between $base $parent \n";2366open(GITLOG,'-|','git-log',"$base..$parent")2367or die"Cannot call git-log:$!";2368my$mergedhash;2369while(<GITLOG>) {2370chomp;2371if(!defined$mergedhash) {2372if(m/^commit\s+(.+)$/) {2373$mergedhash=$1;2374}else{2375next;2376}2377}else{2378# grab the first line that looks non-rfc8222379# aka has content after leading space2380if(m/^\s+(\S.*)$/) {2381my$title=$1;2382$title=substr($title,0,100);# truncate2383unshift@merged,"$mergedhash$title";2384undef$mergedhash;2385}2386}2387}2388close GITLOG;2389if(@merged) {2390$commit->{mergemsg} =$commit->{message};2391$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2392foreachmy$summary(@merged) {2393$commit->{mergemsg} .="\t$summary\n";2394}2395$commit->{mergemsg} .="\n\n";2396# print "Message for $commit->{hash} \n$commit->{mergemsg}";2397}2398}2399}2400}2401}24022403# convert the date to CVS-happy format2404$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24052406if(defined($lastpicked) )2407{2408my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2409local($/) ="\0";2410while( <FILELIST> )2411{2412chomp;2413unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2414{2415die("Couldn't process git-diff-tree line :$_");2416}2417my($mode,$hash,$change) = ($1,$2,$3);2418my$name= <FILELIST>;2419chomp($name);24202421# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24222423my$git_perms="";2424$git_perms.="r"if($mode&4);2425$git_perms.="w"if($mode&2);2426$git_perms.="x"if($mode&1);2427$git_perms="rw"if($git_permseq"");24282429if($changeeq"D")2430{2431#$log->debug("DELETE $name");2432$head->{$name} = {2433 name =>$name,2434 revision =>$head->{$name}{revision} +1,2435 filehash =>"deleted",2436 commithash =>$commit->{hash},2437 modified =>$commit->{date},2438 author =>$commit->{author},2439 mode =>$git_perms,2440};2441$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2442}2443elsif($changeeq"M")2444{2445#$log->debug("MODIFIED $name");2446$head->{$name} = {2447 name =>$name,2448 revision =>$head->{$name}{revision} +1,2449 filehash =>$hash,2450 commithash =>$commit->{hash},2451 modified =>$commit->{date},2452 author =>$commit->{author},2453 mode =>$git_perms,2454};2455$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2456}2457elsif($changeeq"A")2458{2459#$log->debug("ADDED $name");2460$head->{$name} = {2461 name =>$name,2462 revision =>1,2463 filehash =>$hash,2464 commithash =>$commit->{hash},2465 modified =>$commit->{date},2466 author =>$commit->{author},2467 mode =>$git_perms,2468};2469$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2470}2471else2472{2473$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2474die;2475}2476}2477close FILELIST;2478}else{2479# this is used to detect files removed from the repo2480my$seen_files= {};24812482my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2483local$/="\0";2484while( <FILELIST> )2485{2486chomp;2487unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2488{2489die("Couldn't process git-ls-tree line :$_");2490}24912492my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);24932494$seen_files->{$git_filename} =1;24952496my($oldhash,$oldrevision,$oldmode) = (2497$head->{$git_filename}{filehash},2498$head->{$git_filename}{revision},2499$head->{$git_filename}{mode}2500);25012502if($git_perms=~/^\d\d\d(\d)\d\d/o)2503{2504$git_perms="";2505$git_perms.="r"if($1&4);2506$git_perms.="w"if($1&2);2507$git_perms.="x"if($1&1);2508}else{2509$git_perms="rw";2510}25112512# unless the file exists with the same hash, we need to update it ...2513unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2514{2515my$newrevision= ($oldrevisionor0) +1;25162517$head->{$git_filename} = {2518 name =>$git_filename,2519 revision =>$newrevision,2520 filehash =>$git_hash,2521 commithash =>$commit->{hash},2522 modified =>$commit->{date},2523 author =>$commit->{author},2524 mode =>$git_perms,2525};252625272528$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2529}2530}2531close FILELIST;25322533# Detect deleted files2534foreachmy$file(keys%$head)2535{2536unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2537{2538$head->{$file}{revision}++;2539$head->{$file}{filehash} ="deleted";2540$head->{$file}{commithash} =$commit->{hash};2541$head->{$file}{modified} =$commit->{date};2542$head->{$file}{author} =$commit->{author};25432544$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2545}2546}2547# END : "Detect deleted files"2548}254925502551if(exists$commit->{mergemsg})2552{2553$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2554}25552556$lastpicked=$commit->{hash};25572558$self->_set_prop("last_commit",$commit->{hash});2559}25602561$self->delete_head();2562foreachmy$file(keys%$head)2563{2564$self->insert_head(2565$file,2566$head->{$file}{revision},2567$head->{$file}{filehash},2568$head->{$file}{commithash},2569$head->{$file}{modified},2570$head->{$file}{author},2571$head->{$file}{mode},2572);2573}2574# invalidate the gethead cache2575$self->{gethead_cache} =undef;257625772578# Ending exclusive lock here2579$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2580}25812582sub insert_rev2583{2584my$self=shift;2585my$name=shift;2586my$revision=shift;2587my$filehash=shift;2588my$commithash=shift;2589my$modified=shift;2590my$author=shift;2591my$mode=shift;25922593my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2594$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2595}25962597sub insert_mergelog2598{2599my$self=shift;2600my$key=shift;2601my$value=shift;26022603my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2604$insert_mergelog->execute($key,$value);2605}26062607sub delete_head2608{2609my$self=shift;26102611my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2612$delete_head->execute();2613}26142615sub insert_head2616{2617my$self=shift;2618my$name=shift;2619my$revision=shift;2620my$filehash=shift;2621my$commithash=shift;2622my$modified=shift;2623my$author=shift;2624my$mode=shift;26252626my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2627$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2628}26292630sub _headrev2631{2632my$self=shift;2633my$filename=shift;26342635my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2636$db_query->execute($filename);2637my($hash,$revision,$mode) =$db_query->fetchrow_array;26382639return($hash,$revision,$mode);2640}26412642sub _get_prop2643{2644my$self=shift;2645my$key=shift;26462647my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2648$db_query->execute($key);2649my($value) =$db_query->fetchrow_array;26502651return$value;2652}26532654sub _set_prop2655{2656my$self=shift;2657my$key=shift;2658my$value=shift;26592660my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2661$db_query->execute($value,$key);26622663unless($db_query->rows)2664{2665$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2666$db_query->execute($key,$value);2667}26682669return$value;2670}26712672=head2 gethead26732674=cut26752676sub gethead2677{2678my$self=shift;26792680return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );26812682my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2683$db_query->execute();26842685my$tree= [];2686while(my$file=$db_query->fetchrow_hashref)2687{2688push@$tree,$file;2689}26902691$self->{gethead_cache} =$tree;26922693return$tree;2694}26952696=head2 getlog26972698=cut26992700sub getlog2701{2702my$self=shift;2703my$filename=shift;27042705my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2706$db_query->execute($filename);27072708my$tree= [];2709while(my$file=$db_query->fetchrow_hashref)2710{2711push@$tree,$file;2712}27132714return$tree;2715}27162717=head2 getmeta27182719This function takes a filename (with path) argument and returns a hashref of2720metadata for that file.27212722=cut27232724sub getmeta2725{2726my$self=shift;2727my$filename=shift;2728my$revision=shift;27292730my$db_query;2731if(defined($revision)and$revision=~/^\d+$/)2732{2733$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2734$db_query->execute($filename,$revision);2735}2736elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2737{2738$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2739$db_query->execute($filename,$revision);2740}else{2741$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2742$db_query->execute($filename);2743}27442745return$db_query->fetchrow_hashref;2746}27472748=head2 commitmessage27492750this function takes a commithash and returns the commit message for that commit27512752=cut2753sub commitmessage2754{2755my$self=shift;2756my$commithash=shift;27572758die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);27592760my$db_query;2761$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2762$db_query->execute($commithash);27632764my($message) =$db_query->fetchrow_array;27652766if(defined($message) )2767{2768$message.=" "if($message=~/\n$/);2769return$message;2770}27712772my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2773shift@lineswhile($lines[0] =~/\S/);2774$message=join("",@lines);2775$message.=" "if($message=~/\n$/);2776return$message;2777}27782779=head2 gethistory27802781This function takes a filename (with path) argument and returns an arrayofarrays2782containing revision,filehash,commithash ordered by revision descending27832784=cut2785sub gethistory2786{2787my$self=shift;2788my$filename=shift;27892790my$db_query;2791$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2792$db_query->execute($filename);27932794return$db_query->fetchall_arrayref;2795}27962797=head2 gethistorydense27982799This function takes a filename (with path) argument and returns an arrayofarrays2800containing revision,filehash,commithash ordered by revision descending.28012802This version of gethistory skips deleted entries -- so it is useful for annotate.2803The 'dense' part is a reference to a '--dense' option available for git-rev-list2804and other git tools that depend on it.28052806=cut2807sub gethistorydense2808{2809my$self=shift;2810my$filename=shift;28112812my$db_query;2813$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2814$db_query->execute($filename);28152816return$db_query->fetchall_arrayref;2817}28182819=head2 in_array()28202821from Array::PAT - mimics the in_array() function2822found in PHP. Yuck but works for small arrays.28232824=cut2825sub in_array2826{2827my($check,@array) =@_;2828my$retval=0;2829foreachmy$test(@array){2830if($checkeq$test){2831$retval=1;2832}2833}2834return$retval;2835}28362837=head2 safe_pipe_capture28382839an alternative to `command` that allows input to be passed as an array2840to work around shell problems with weird characters in arguments28412842=cut2843sub safe_pipe_capture {28442845my@output;28462847if(my$pid=open my$child,'-|') {2848@output= (<$child>);2849close$childor die join(' ',@_).":$!$?";2850}else{2851exec(@_)or die"$!$?";# exec() can fail the executable can't be found2852}2853returnwantarray?@output:join('',@output);2854}2855285628571;