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"; 108exit; 109} 110$line= <STDIN>;chomp$line;# validate the password? 111$line= <STDIN>;chomp$line; 112unless($lineeq'END AUTH REQUEST') { 113die"E Do not understand$line-- expecting END AUTH REQUEST\n"; 114} 115print"I LOVE YOU\n"; 116# and now back to our regular programme... 117} 118 119# Keep going until the client closes the connection 120while(<STDIN>) 121{ 122chomp; 123 124# Check to see if we've seen this method, and call appropriate function. 125if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 126{ 127# use the $methods hash to call the appropriate sub for this command 128#$log->info("Method : $1"); 129&{$methods->{$1}}($1,$2); 130}else{ 131# log fatal because we don't understand this function. If this happens 132# we're fairly screwed because we don't know if the client is expecting 133# a response. If it is, the client will hang, we'll hang, and the whole 134# thing will be custard. 135$log->fatal("Don't understand command$_\n"); 136die("Unknown command$_"); 137} 138} 139 140$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 141$log->info("--------------- FINISH -----------------"); 142 143# Magic catchall method. 144# This is the method that will handle all commands we haven't yet 145# implemented. It simply sends a warning to the log file indicating a 146# command that hasn't been implemented has been invoked. 147sub req_CATCHALL 148{ 149my($cmd,$data) =@_; 150$log->warn("Unhandled command : req_$cmd:$data"); 151} 152 153 154# Root pathname \n 155# Response expected: no. Tell the server which CVSROOT to use. Note that 156# pathname is a local directory and not a fully qualified CVSROOT variable. 157# pathname must already exist; if creating a new root, use the init 158# request, not Root. pathname does not include the hostname of the server, 159# how to access the server, etc.; by the time the CVS protocol is in use, 160# connection, authentication, etc., are already taken care of. The Root 161# request must be sent only once, and it must be sent before any requests 162# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 163sub req_Root 164{ 165my($cmd,$data) =@_; 166$log->debug("req_Root :$data"); 167 168$state->{CVSROOT} =$data; 169 170$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 171unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 172print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 173print"E\n"; 174print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 175return0; 176} 177 178my@gitvars=`git-config -l`; 179if($?) { 180print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 181print"E\n"; 182print"error 1 - problem executing git-config\n"; 183return0; 184} 185foreachmy$line(@gitvars) 186{ 187next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 188unless($2) { 189$cfg->{$1}{$3} =$4; 190}else{ 191$cfg->{$1}{$2}{$3} =$4; 192} 193} 194 195unless( ($cfg->{gitcvs}{$state->{method}}{enabled} 196and$cfg->{gitcvs}{$state->{method}}{enabled} =~/^\s*(1|true|yes)\s*$/i) 197or($cfg->{gitcvs}{enabled} 198and$cfg->{gitcvs}{enabled} =~/^\s*(1|true|yes)\s*$/i) ) 199{ 200print"E GITCVS emulation needs to be enabled on this repo\n"; 201print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 202print"E\n"; 203print"error 1 GITCVS emulation disabled\n"; 204return0; 205} 206 207my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 208if($logfile) 209{ 210$log->setfile($logfile); 211}else{ 212$log->nofile(); 213} 214 215return1; 216} 217 218# Global_option option \n 219# Response expected: no. Transmit one of the global options `-q', `-Q', 220# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 221# variations (such as combining of options) are allowed. For graceful 222# handling of valid-requests, it is probably better to make new global 223# options separate requests, rather than trying to add them to this 224# request. 225sub req_Globaloption 226{ 227my($cmd,$data) =@_; 228$log->debug("req_Globaloption :$data"); 229$state->{globaloptions}{$data} =1; 230} 231 232# Valid-responses request-list \n 233# Response expected: no. Tell the server what responses the client will 234# accept. request-list is a space separated list of tokens. 235sub req_Validresponses 236{ 237my($cmd,$data) =@_; 238$log->debug("req_Validresponses :$data"); 239 240# TODO : re-enable this, currently it's not particularly useful 241#$state->{validresponses} = [ split /\s+/, $data ]; 242} 243 244# valid-requests \n 245# Response expected: yes. Ask the server to send back a Valid-requests 246# response. 247sub req_validrequests 248{ 249my($cmd,$data) =@_; 250 251$log->debug("req_validrequests"); 252 253$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 254$log->debug("SEND : ok"); 255 256print"Valid-requests ".join(" ",keys%$methods) ."\n"; 257print"ok\n"; 258} 259 260# Directory local-directory \n 261# Additional data: repository \n. Response expected: no. Tell the server 262# what directory to use. The repository should be a directory name from a 263# previous server response. Note that this both gives a default for Entry 264# and Modified and also for ci and the other commands; normal usage is to 265# send Directory for each directory in which there will be an Entry or 266# Modified, and then a final Directory for the original directory, then the 267# command. The local-directory is relative to the top level at which the 268# command is occurring (i.e. the last Directory which is sent before the 269# command); to indicate that top level, `.' should be sent for 270# local-directory. 271sub req_Directory 272{ 273my($cmd,$data) =@_; 274 275my$repository= <STDIN>; 276chomp$repository; 277 278 279$state->{localdir} =$data; 280$state->{repository} =$repository; 281$state->{path} =$repository; 282$state->{path} =~s/^$state->{CVSROOT}\///; 283$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 284$state->{path} .="/"if($state->{path} =~ /\S/ ); 285 286$state->{directory} =$state->{localdir}; 287$state->{directory} =""if($state->{directory}eq"."); 288$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 289 290if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 291{ 292$log->info("Setting prepend to '$state->{path}'"); 293$state->{prependdir} =$state->{path}; 294foreachmy$entry(keys%{$state->{entries}} ) 295{ 296$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 297delete$state->{entries}{$entry}; 298} 299} 300 301if(defined($state->{prependdir} ) ) 302{ 303$log->debug("Prepending '$state->{prependdir}' to state|directory"); 304$state->{directory} =$state->{prependdir} .$state->{directory} 305} 306$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 307} 308 309# Entry entry-line \n 310# Response expected: no. Tell the server what version of a file is on the 311# local machine. The name in entry-line is a name relative to the directory 312# most recently specified with Directory. If the user is operating on only 313# some files in a directory, Entry requests for only those files need be 314# included. If an Entry request is sent without Modified, Is-modified, or 315# Unchanged, it means the file is lost (does not exist in the working 316# directory). If both Entry and one of Modified, Is-modified, or Unchanged 317# are sent for the same file, Entry must be sent first. For a given file, 318# one can send Modified, Is-modified, or Unchanged, but not more than one 319# of these three. 320sub req_Entry 321{ 322my($cmd,$data) =@_; 323 324#$log->debug("req_Entry : $data"); 325 326my@data=split(/\//,$data); 327 328$state->{entries}{$state->{directory}.$data[1]} = { 329 revision =>$data[2], 330 conflict =>$data[3], 331 options =>$data[4], 332 tag_or_date =>$data[5], 333}; 334 335$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 336} 337 338# Questionable filename \n 339# Response expected: no. Additional data: no. Tell the server to check 340# whether filename should be ignored, and if not, next time the server 341# sends responses, send (in a M response) `?' followed by the directory and 342# filename. filename must not contain `/'; it needs to be a file in the 343# directory named by the most recent Directory request. 344sub req_Questionable 345{ 346my($cmd,$data) =@_; 347 348$log->debug("req_Questionable :$data"); 349$state->{entries}{$state->{directory}.$data}{questionable} =1; 350} 351 352# add \n 353# Response expected: yes. Add a file or directory. This uses any previous 354# Argument, Directory, Entry, or Modified requests, if they have been sent. 355# The last Directory sent specifies the working directory at the time of 356# the operation. To add a directory, send the directory to be added using 357# Directory and Argument requests. 358sub req_add 359{ 360my($cmd,$data) =@_; 361 362 argsplit("add"); 363 364my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 365$updater->update(); 366 367 argsfromdir($updater); 368 369my$addcount=0; 370 371foreachmy$filename( @{$state->{args}} ) 372{ 373$filename= filecleanup($filename); 374 375my$meta=$updater->getmeta($filename); 376my$wrev= revparse($filename); 377 378if($wrev&&$meta&& ($wrev<0)) 379{ 380# previously removed file, add back 381$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 382 383print"MT +updated\n"; 384print"MT text U\n"; 385print"MT fname$filename\n"; 386print"MT newline\n"; 387print"MT -updated\n"; 388 389unless($state->{globaloptions}{-n} ) 390{ 391my($filepart,$dirpart) = filenamesplit($filename,1); 392 393print"Created$dirpart\n"; 394print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 395 396# this is an "entries" line 397my$kopts= kopts_from_path($filepart); 398$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 399print"/$filepart/1.$meta->{revision}//$kopts/\n"; 400# permissions 401$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 402print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 403# transmit file 404 transmitfile($meta->{filehash}); 405} 406 407next; 408} 409 410unless(defined($state->{entries}{$filename}{modified_filename} ) ) 411{ 412print"E cvs add: nothing known about `$filename'\n"; 413next; 414} 415# TODO : check we're not squashing an already existing file 416if(defined($state->{entries}{$filename}{revision} ) ) 417{ 418print"E cvs add: `$filename' has already been entered\n"; 419next; 420} 421 422my($filepart,$dirpart) = filenamesplit($filename,1); 423 424print"E cvs add: scheduling file `$filename' for addition\n"; 425 426print"Checked-in$dirpart\n"; 427print"$filename\n"; 428my$kopts= kopts_from_path($filepart); 429print"/$filepart/0//$kopts/\n"; 430 431$addcount++; 432} 433 434if($addcount==1) 435{ 436print"E cvs add: use `cvs commit' to add this file permanently\n"; 437} 438elsif($addcount>1) 439{ 440print"E cvs add: use `cvs commit' to add these files permanently\n"; 441} 442 443print"ok\n"; 444} 445 446# remove \n 447# Response expected: yes. Remove a file. This uses any previous Argument, 448# Directory, Entry, or Modified requests, if they have been sent. The last 449# Directory sent specifies the working directory at the time of the 450# operation. Note that this request does not actually do anything to the 451# repository; the only effect of a successful remove request is to supply 452# the client with a new entries line containing `-' to indicate a removed 453# file. In fact, the client probably could perform this operation without 454# contacting the server, although using remove may cause the server to 455# perform a few more checks. The client sends a subsequent ci request to 456# actually record the removal in the repository. 457sub req_remove 458{ 459my($cmd,$data) =@_; 460 461 argsplit("remove"); 462 463# Grab a handle to the SQLite db and do any necessary updates 464my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 465$updater->update(); 466 467#$log->debug("add state : " . Dumper($state)); 468 469my$rmcount=0; 470 471foreachmy$filename( @{$state->{args}} ) 472{ 473$filename= filecleanup($filename); 474 475if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 476{ 477print"E cvs remove: file `$filename' still in working directory\n"; 478next; 479} 480 481my$meta=$updater->getmeta($filename); 482my$wrev= revparse($filename); 483 484unless(defined($wrev) ) 485{ 486print"E cvs remove: nothing known about `$filename'\n"; 487next; 488} 489 490if(defined($wrev)and$wrev<0) 491{ 492print"E cvs remove: file `$filename' already scheduled for removal\n"; 493next; 494} 495 496unless($wrev==$meta->{revision} ) 497{ 498# TODO : not sure if the format of this message is quite correct. 499print"E cvs remove: Up to date check failed for `$filename'\n"; 500next; 501} 502 503 504my($filepart,$dirpart) = filenamesplit($filename,1); 505 506print"E cvs remove: scheduling `$filename' for removal\n"; 507 508print"Checked-in$dirpart\n"; 509print"$filename\n"; 510my$kopts= kopts_from_path($filepart); 511print"/$filepart/-1.$wrev//$kopts/\n"; 512 513$rmcount++; 514} 515 516if($rmcount==1) 517{ 518print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 519} 520elsif($rmcount>1) 521{ 522print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 523} 524 525print"ok\n"; 526} 527 528# Modified filename \n 529# Response expected: no. Additional data: mode, \n, file transmission. Send 530# the server a copy of one locally modified file. filename is a file within 531# the most recent directory sent with Directory; it must not contain `/'. 532# If the user is operating on only some files in a directory, only those 533# files need to be included. This can also be sent without Entry, if there 534# is no entry for the file. 535sub req_Modified 536{ 537my($cmd,$data) =@_; 538 539my$mode= <STDIN>; 540chomp$mode; 541my$size= <STDIN>; 542chomp$size; 543 544# Grab config information 545my$blocksize=8192; 546my$bytesleft=$size; 547my$tmp; 548 549# Get a filehandle/name to write it to 550my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 551 552# Loop over file data writing out to temporary file. 553while($bytesleft) 554{ 555$blocksize=$bytesleftif($bytesleft<$blocksize); 556read STDIN,$tmp,$blocksize; 557print$fh $tmp; 558$bytesleft-=$blocksize; 559} 560 561close$fh; 562 563# Ensure we have something sensible for the file mode 564if($mode=~/u=(\w+)/) 565{ 566$mode=$1; 567}else{ 568$mode="rw"; 569} 570 571# Save the file data in $state 572$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 573$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 574$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 575$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 576 577 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 578} 579 580# Unchanged filename\n 581# Response expected: no. Tell the server that filename has not been 582# modified in the checked out directory. The filename is a file within the 583# most recent directory sent with Directory; it must not contain `/'. 584sub req_Unchanged 585{ 586 my ($cmd,$data) =@_; 587 588$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 589 590 #$log->debug("req_Unchanged :$data"); 591} 592 593# Argument text\n 594# Response expected: no. Save argument for use in a subsequent command. 595# Arguments accumulate until an argument-using command is given, at which 596# point they are forgotten. 597# Argumentx text\n 598# Response expected: no. Append\nfollowed by text to the current argument 599# being saved. 600sub req_Argument 601{ 602 my ($cmd,$data) =@_; 603 604 # Argumentx means: append to last Argument (with a newline in front) 605 606$log->debug("$cmd:$data"); 607 608 if ($cmdeq 'Argumentx') { 609 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 610 } else { 611 push @{$state->{arguments}},$data; 612 } 613} 614 615# expand-modules\n 616# Response expected: yes. Expand the modules which are specified in the 617# arguments. Returns the data in Module-expansion responses. Note that the 618# server can assume that this is checkout or export, not rtag or rdiff; the 619# latter do not access the working directory and thus have no need to 620# expand modules on the client side. Expand may not be the best word for 621# what this request does. It does not necessarily tell you all the files 622# contained in a module, for example. Basically it is a way of telling you 623# which working directories the server needs to know about in order to 624# handle a checkout of the specified modules. For example, suppose that the 625# server has a module defined by 626# aliasmodule -a 1dir 627# That is, one can check out aliasmodule and it will take 1dir in the 628# repository and check it out to 1dir in the working directory. Now suppose 629# the client already has this module checked out and is planning on using 630# the co request to update it. Without using expand-modules, the client 631# would have two bad choices: it could either send information about all 632# working directories under the current directory, which could be 633# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 634# stands for 1dir, and neglect to send information for 1dir, which would 635# lead to incorrect operation. With expand-modules, the client would first 636# ask for the module to be expanded: 637sub req_expandmodules 638{ 639 my ($cmd,$data) =@_; 640 641 argsplit(); 642 643$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 644 645 unless ( ref$state->{arguments} eq "ARRAY" ) 646 { 647 print "ok\n"; 648 return; 649 } 650 651 foreach my$module( @{$state->{arguments}} ) 652 { 653$log->debug("SEND : Module-expansion$module"); 654 print "Module-expansion$module\n"; 655 } 656 657 print "ok\n"; 658 statecleanup(); 659} 660 661# co\n 662# Response expected: yes. Get files from the repository. This uses any 663# previous Argument, Directory, Entry, or Modified requests, if they have 664# been sent. Arguments to this command are module names; the client cannot 665# know what directories they correspond to except by (1) just sending the 666# co request, and then seeing what directory names the server sends back in 667# its responses, and (2) the expand-modules request. 668sub req_co 669{ 670 my ($cmd,$data) =@_; 671 672 argsplit("co"); 673 674 my$module=$state->{args}[0]; 675 my$checkout_path=$module; 676 677 # use the user specified directory if we're given it 678$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 679 680$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 681 682$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 683 684$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 685 686# Grab a handle to the SQLite db and do any necessary updates 687my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 688$updater->update(); 689 690$checkout_path=~ s|/$||;# get rid of trailing slashes 691 692# Eclipse seems to need the Clear-sticky command 693# to prepare the 'Entries' file for the new directory. 694print"Clear-sticky$checkout_path/\n"; 695print$state->{CVSROOT} ."/$module/\n"; 696print"Clear-static-directory$checkout_path/\n"; 697print$state->{CVSROOT} ."/$module/\n"; 698print"Clear-sticky$checkout_path/\n";# yes, twice 699print$state->{CVSROOT} ."/$module/\n"; 700print"Template$checkout_path/\n"; 701print$state->{CVSROOT} ."/$module/\n"; 702print"0\n"; 703 704# instruct the client that we're checking out to $checkout_path 705print"E cvs checkout: Updating$checkout_path\n"; 706 707my%seendirs= (); 708my$lastdir=''; 709 710# recursive 711sub prepdir { 712my($dir,$repodir,$remotedir,$seendirs) =@_; 713my$parent= dirname($dir); 714$dir=~ s|/+$||; 715$repodir=~ s|/+$||; 716$remotedir=~ s|/+$||; 717$parent=~ s|/+$||; 718$log->debug("announcedir$dir,$repodir,$remotedir"); 719 720if($parenteq'.'||$parenteq'./') { 721$parent=''; 722} 723# recurse to announce unseen parents first 724if(length($parent) && !exists($seendirs->{$parent})) { 725 prepdir($parent,$repodir,$remotedir,$seendirs); 726} 727# Announce that we are going to modify at the parent level 728if($parent) { 729print"E cvs checkout: Updating$remotedir/$parent\n"; 730}else{ 731print"E cvs checkout: Updating$remotedir\n"; 732} 733print"Clear-sticky$remotedir/$parent/\n"; 734print"$repodir/$parent/\n"; 735 736print"Clear-static-directory$remotedir/$dir/\n"; 737print"$repodir/$dir/\n"; 738print"Clear-sticky$remotedir/$parent/\n";# yes, twice 739print"$repodir/$parent/\n"; 740print"Template$remotedir/$dir/\n"; 741print"$repodir/$dir/\n"; 742print"0\n"; 743 744$seendirs->{$dir} =1; 745} 746 747foreachmy$git( @{$updater->gethead} ) 748{ 749# Don't want to check out deleted files 750next if($git->{filehash}eq"deleted"); 751 752($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 753 754if(length($git->{dir}) &&$git->{dir}ne'./' 755&&$git->{dir}ne$lastdir) { 756unless(exists($seendirs{$git->{dir}})) { 757 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 758$checkout_path, \%seendirs); 759$lastdir=$git->{dir}; 760$seendirs{$git->{dir}} =1; 761} 762print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 763} 764 765# modification time of this file 766print"Mod-time$git->{modified}\n"; 767 768# print some information to the client 769if(defined($git->{dir} )and$git->{dir}ne"./") 770{ 771print"M U$checkout_path/$git->{dir}$git->{name}\n"; 772}else{ 773print"M U$checkout_path/$git->{name}\n"; 774} 775 776# instruct client we're sending a file to put in this path 777print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 778 779print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 780 781# this is an "entries" line 782my$kopts= kopts_from_path($git->{name}); 783print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 784# permissions 785print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 786 787# transmit file 788 transmitfile($git->{filehash}); 789} 790 791print"ok\n"; 792 793 statecleanup(); 794} 795 796# update \n 797# Response expected: yes. Actually do a cvs update command. This uses any 798# previous Argument, Directory, Entry, or Modified requests, if they have 799# been sent. The last Directory sent specifies the working directory at the 800# time of the operation. The -I option is not used--files which the client 801# can decide whether to ignore are not mentioned and the client sends the 802# Questionable request for others. 803sub req_update 804{ 805my($cmd,$data) =@_; 806 807$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 808 809 argsplit("update"); 810 811# 812# It may just be a client exploring the available heads/modules 813# in that case, list them as top level directories and leave it 814# at that. Eclipse uses this technique to offer you a list of 815# projects (heads in this case) to checkout. 816# 817if($state->{module}eq'') { 818print"E cvs update: Updating .\n"; 819opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 820while(my$head=readdir(HEADS)) { 821if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 822print"E cvs update: New directory `$head'\n"; 823} 824} 825closedir HEADS; 826print"ok\n"; 827return1; 828} 829 830 831# Grab a handle to the SQLite db and do any necessary updates 832my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 833 834$updater->update(); 835 836 argsfromdir($updater); 837 838#$log->debug("update state : " . Dumper($state)); 839 840# foreach file specified on the command line ... 841foreachmy$filename( @{$state->{args}} ) 842{ 843$filename= filecleanup($filename); 844 845$log->debug("Processing file$filename"); 846 847# if we have a -C we should pretend we never saw modified stuff 848if(exists($state->{opt}{C} ) ) 849{ 850delete$state->{entries}{$filename}{modified_hash}; 851delete$state->{entries}{$filename}{modified_filename}; 852$state->{entries}{$filename}{unchanged} =1; 853} 854 855my$meta; 856if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 857{ 858$meta=$updater->getmeta($filename,$1); 859}else{ 860$meta=$updater->getmeta($filename); 861} 862 863if( !defined$meta) 864{ 865$meta= { 866 name =>$filename, 867 revision =>0, 868 filehash =>'added' 869}; 870} 871 872my$oldmeta=$meta; 873 874my$wrev= revparse($filename); 875 876# If the working copy is an old revision, lets get that version too for comparison. 877if(defined($wrev)and$wrev!=$meta->{revision} ) 878{ 879$oldmeta=$updater->getmeta($filename,$wrev); 880} 881 882#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 883 884# Files are up to date if the working copy and repo copy have the same revision, 885# and the working copy is unmodified _and_ the user hasn't specified -C 886next if(defined($wrev) 887and defined($meta->{revision}) 888and$wrev==$meta->{revision} 889and$state->{entries}{$filename}{unchanged} 890and not exists($state->{opt}{C} ) ); 891 892# If the working copy and repo copy have the same revision, 893# but the working copy is modified, tell the client it's modified 894if(defined($wrev) 895and defined($meta->{revision}) 896and$wrev==$meta->{revision} 897and defined($state->{entries}{$filename}{modified_hash}) 898and not exists($state->{opt}{C} ) ) 899{ 900$log->info("Tell the client the file is modified"); 901print"MT text M\n"; 902print"MT fname$filename\n"; 903print"MT newline\n"; 904next; 905} 906 907if($meta->{filehash}eq"deleted") 908{ 909my($filepart,$dirpart) = filenamesplit($filename,1); 910 911$log->info("Removing '$filename' from working copy (no longer in the repo)"); 912 913print"E cvs update: `$filename' is no longer in the repository\n"; 914# Don't want to actually _DO_ the update if -n specified 915unless($state->{globaloptions}{-n} ) { 916print"Removed$dirpart\n"; 917print"$filepart\n"; 918} 919} 920elsif(not defined($state->{entries}{$filename}{modified_hash} ) 921or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 922or$meta->{filehash}eq'added') 923{ 924# normal update, just send the new revision (either U=Update, 925# or A=Add, or R=Remove) 926if(defined($wrev) &&$wrev<0) 927{ 928$log->info("Tell the client the file is scheduled for removal"); 929print"MT text R\n"; 930print"MT fname$filename\n"; 931print"MT newline\n"; 932next; 933} 934elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 935{ 936$log->info("Tell the client the file is scheduled for addition"); 937print"MT text A\n"; 938print"MT fname$filename\n"; 939print"MT newline\n"; 940next; 941 942} 943else{ 944$log->info("Updating '$filename' to ".$meta->{revision}); 945print"MT +updated\n"; 946print"MT text U\n"; 947print"MT fname$filename\n"; 948print"MT newline\n"; 949print"MT -updated\n"; 950} 951 952my($filepart,$dirpart) = filenamesplit($filename,1); 953 954# Don't want to actually _DO_ the update if -n specified 955unless($state->{globaloptions}{-n} ) 956{ 957if(defined($wrev) ) 958{ 959# instruct client we're sending a file to put in this path as a replacement 960print"Update-existing$dirpart\n"; 961$log->debug("Updating existing file 'Update-existing$dirpart'"); 962}else{ 963# instruct client we're sending a file to put in this path as a new file 964print"Clear-static-directory$dirpart\n"; 965print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 966print"Clear-sticky$dirpart\n"; 967print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 968 969$log->debug("Creating new file 'Created$dirpart'"); 970print"Created$dirpart\n"; 971} 972print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 973 974# this is an "entries" line 975my$kopts= kopts_from_path($filepart); 976$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 977print"/$filepart/1.$meta->{revision}//$kopts/\n"; 978 979# permissions 980$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 981print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 982 983# transmit file 984 transmitfile($meta->{filehash}); 985} 986}else{ 987$log->info("Updating '$filename'"); 988my($filepart,$dirpart) = filenamesplit($meta->{name},1); 989 990my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 991 992chdir$dir; 993my$file_local=$filepart.".mine"; 994system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 995my$file_old=$filepart.".".$oldmeta->{revision}; 996 transmitfile($oldmeta->{filehash},$file_old); 997my$file_new=$filepart.".".$meta->{revision}; 998 transmitfile($meta->{filehash},$file_new); 9991000# we need to merge with the local changes ( M=successful merge, C=conflict merge )1001$log->info("Merging$file_local,$file_old,$file_new");1002print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10031004$log->debug("Temporary directory for merge is$dir");10051006my$return=system("git","merge-file",$file_local,$file_old,$file_new);1007$return>>=8;10081009if($return==0)1010{1011$log->info("Merged successfully");1012print"M M$filename\n";1013$log->debug("Merged$dirpart");10141015# Don't want to actually _DO_ the update if -n specified1016unless($state->{globaloptions}{-n} )1017{1018print"Merged$dirpart\n";1019$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1020print$state->{CVSROOT} ."/$state->{module}/$filename\n";1021my$kopts= kopts_from_path($filepart);1022$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1023print"/$filepart/1.$meta->{revision}//$kopts/\n";1024}1025}1026elsif($return==1)1027{1028$log->info("Merged with conflicts");1029print"E cvs update: conflicts found in$filename\n";1030print"M C$filename\n";10311032# Don't want to actually _DO_ the update if -n specified1033unless($state->{globaloptions}{-n} )1034{1035print"Merged$dirpart\n";1036print$state->{CVSROOT} ."/$state->{module}/$filename\n";1037my$kopts= kopts_from_path($filepart);1038print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1039}1040}1041else1042{1043$log->warn("Merge failed");1044next;1045}10461047# Don't want to actually _DO_ the update if -n specified1048unless($state->{globaloptions}{-n} )1049{1050# permissions1051$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1052print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10531054# transmit file, format is single integer on a line by itself (file1055# size) followed by the file contents1056# TODO : we should copy files in blocks1057my$data=`cat$file_local`;1058$log->debug("File size : " . length($data));1059 print length($data) . "\n";1060 print$data;1061 }10621063 chdir "/";1064 }10651066 }10671068 print "ok\n";1069}10701071sub req_ci1072{1073 my ($cmd,$data) =@_;10741075 argsplit("ci");10761077 #$log->debug("State : " . Dumper($state));10781079$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10801081 if ($state->{method} eq 'pserver')1082 {1083 print "error 1 pserver access cannot commit\n";1084 exit;1085 }10861087 if ( -e$state->{CVSROOT} . "/index" )1088 {1089$log->warn("file 'index' already exists in the git repository");1090 print "error 1 Index already exists in git repo\n";1091 exit;1092 }10931094 # Grab a handle to the SQLite db and do any necessary updates1095 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1096$updater->update();10971098 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1099 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1100$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");11011102$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1103$ENV{GIT_INDEX_FILE} =$file_index;11041105 # Remember where the head was at the beginning.1106 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1107 chomp$parenthash;1108 if ($parenthash!~ /^[0-9a-f]{40}$/) {1109 print "error 1 pserver cannot find the current HEAD of module";1110 exit;1111 }11121113 chdir$tmpdir;11141115 # populate the temporary index based1116 system("git-read-tree",$parenthash);1117 unless ($?== 0)1118 {1119 die "Error running git-read-tree$state->{module}$file_index$!";1120 }1121$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");11221123 my@committedfiles= ();1124 my%oldmeta;11251126 # foreach file specified on the command line ...1127 foreach my$filename( @{$state->{args}} )1128 {1129 my$committedfile=$filename;1130$filename= filecleanup($filename);11311132 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );11331134 my$meta=$updater->getmeta($filename);1135$oldmeta{$filename} =$meta;11361137 my$wrev= revparse($filename);11381139 my ($filepart,$dirpart) = filenamesplit($filename);11401141 # do a checkout of the file if it part of this tree1142 if ($wrev) {1143 system('git-checkout-index', '-f', '-u',$filename);1144 unless ($?== 0) {1145 die "Error running git-checkout-index -f -u$filename:$!";1146 }1147 }11481149 my$addflag= 0;1150 my$rmflag= 0;1151$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1152$addflag= 1 unless ( -e$filename);11531154 # Do up to date checking1155 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1156 {1157 # fail everything if an up to date check fails1158 print "error 1 Up to date check failed for$filename\n";1159 chdir "/";1160 exit;1161 }11621163 push@committedfiles,$committedfile;1164$log->info("Committing$filename");11651166 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11671168 unless ($rmflag)1169 {1170$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1171 rename$state->{entries}{$filename}{modified_filename},$filename;11721173 # Calculate modes to remove1174 my$invmode= "";1175 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11761177$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1178 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1179 }11801181 if ($rmflag)1182 {1183$log->info("Removing file '$filename'");1184 unlink($filename);1185 system("git-update-index", "--remove",$filename);1186 }1187 elsif ($addflag)1188 {1189$log->info("Adding file '$filename'");1190 system("git-update-index", "--add",$filename);1191 } else {1192$log->info("Updating file '$filename'");1193 system("git-update-index",$filename);1194 }1195 }11961197 unless ( scalar(@committedfiles) > 0 )1198 {1199 print "E No files to commit\n";1200 print "ok\n";1201 chdir "/";1202 return;1203 }12041205 my$treehash= `git-write-tree`;1206 chomp$treehash;12071208$log->debug("Treehash :$treehash, Parenthash :$parenthash");12091210 # write our commit message out if we have one ...1211 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1212 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1213 print$msg_fh"\n\nvia git-CVS emulator\n";1214 close$msg_fh;12151216 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1217chomp($commithash);1218$log->info("Commit hash :$commithash");12191220unless($commithash=~/[a-zA-Z0-9]{40}/)1221{1222$log->warn("Commit failed (Invalid commit hash)");1223print"error 1 Commit failed (unknown reason)\n";1224chdir"/";1225exit;1226}12271228# Check that this is allowed, just as we would with a receive-pack1229my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1230$parenthash,$commithash);1231if( -x $cmd[0] ) {1232unless(system(@cmd) ==0)1233{1234$log->warn("Commit failed (update hook declined to update ref)");1235print"error 1 Commit failed (update hook declined)\n";1236chdir"/";1237exit;1238}1239}12401241if(system(qw(git update-ref -m),"cvsserver ci",1242"refs/heads/$state->{module}",$commithash,$parenthash)) {1243$log->warn("update-ref for$state->{module} failed.");1244print"error 1 Cannot commit -- update first\n";1245exit;1246}12471248$updater->update();12491250# foreach file specified on the command line ...1251foreachmy$filename(@committedfiles)1252{1253$filename= filecleanup($filename);12541255my$meta=$updater->getmeta($filename);1256unless(defined$meta->{revision}) {1257$meta->{revision} =1;1258}12591260my($filepart,$dirpart) = filenamesplit($filename,1);12611262$log->debug("Checked-in$dirpart:$filename");12631264print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1265if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1266{1267print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1268print"Remove-entry$dirpart\n";1269print"$filename\n";1270}else{1271if($meta->{revision} ==1) {1272print"M initial revision: 1.1\n";1273}else{1274print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1275}1276print"Checked-in$dirpart\n";1277print"$filename\n";1278my$kopts= kopts_from_path($filepart);1279print"/$filepart/1.$meta->{revision}//$kopts/\n";1280}1281}12821283chdir"/";1284print"ok\n";1285}12861287sub req_status1288{1289my($cmd,$data) =@_;12901291 argsplit("status");12921293$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1294#$log->debug("status state : " . Dumper($state));12951296# Grab a handle to the SQLite db and do any necessary updates1297my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1298$updater->update();12991300# if no files were specified, we need to work out what files we should be providing status on ...1301 argsfromdir($updater);13021303# foreach file specified on the command line ...1304foreachmy$filename( @{$state->{args}} )1305{1306$filename= filecleanup($filename);13071308my$meta=$updater->getmeta($filename);1309my$oldmeta=$meta;13101311my$wrev= revparse($filename);13121313# If the working copy is an old revision, lets get that version too for comparison.1314if(defined($wrev)and$wrev!=$meta->{revision} )1315{1316$oldmeta=$updater->getmeta($filename,$wrev);1317}13181319# TODO : All possible statuses aren't yet implemented1320my$status;1321# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1322$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1323and1324( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1325or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1326);13271328# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1329$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1330and1331($state->{entries}{$filename}{unchanged}1332or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1333);13341335# Need checkout if it exists in the repo but doesn't have a working copy1336$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );13371338# Locally modified if working copy and repo copy have the same revision but there are local changes1339$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );13401341# Needs Merge if working copy revision is less than repo copy and there are local changes1342$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );13431344$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1345$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1346$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1347$status||="File had conflicts on merge"if(0);13481349$status||="Unknown";13501351print"M ===================================================================\n";1352print"M File:$filename\tStatus:$status\n";1353if(defined($state->{entries}{$filename}{revision}) )1354{1355print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1356}else{1357print"M Working revision:\tNo entry for$filename\n";1358}1359if(defined($meta->{revision}) )1360{1361print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1362print"M Sticky Tag:\t\t(none)\n";1363print"M Sticky Date:\t\t(none)\n";1364print"M Sticky Options:\t\t(none)\n";1365}else{1366print"M Repository revision:\tNo revision control file\n";1367}1368print"M\n";1369}13701371print"ok\n";1372}13731374sub req_diff1375{1376my($cmd,$data) =@_;13771378 argsplit("diff");13791380$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1381#$log->debug("status state : " . Dumper($state));13821383my($revision1,$revision2);1384if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1385{1386$revision1=$state->{opt}{r}[0];1387$revision2=$state->{opt}{r}[1];1388}else{1389$revision1=$state->{opt}{r};1390}13911392$revision1=~s/^1\.//if(defined($revision1) );1393$revision2=~s/^1\.//if(defined($revision2) );13941395$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );13961397# Grab a handle to the SQLite db and do any necessary updates1398my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1399$updater->update();14001401# if no files were specified, we need to work out what files we should be providing status on ...1402 argsfromdir($updater);14031404# foreach file specified on the command line ...1405foreachmy$filename( @{$state->{args}} )1406{1407$filename= filecleanup($filename);14081409my($fh,$file1,$file2,$meta1,$meta2,$filediff);14101411my$wrev= revparse($filename);14121413# We need _something_ to diff against1414next unless(defined($wrev) );14151416# if we have a -r switch, use it1417if(defined($revision1) )1418{1419(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1420$meta1=$updater->getmeta($filename,$revision1);1421unless(defined($meta1)and$meta1->{filehash}ne"deleted")1422{1423print"E File$filenameat revision 1.$revision1doesn't exist\n";1424next;1425}1426 transmitfile($meta1->{filehash},$file1);1427}1428# otherwise we just use the working copy revision1429else1430{1431(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1432$meta1=$updater->getmeta($filename,$wrev);1433 transmitfile($meta1->{filehash},$file1);1434}14351436# if we have a second -r switch, use it too1437if(defined($revision2) )1438{1439(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1440$meta2=$updater->getmeta($filename,$revision2);14411442unless(defined($meta2)and$meta2->{filehash}ne"deleted")1443{1444print"E File$filenameat revision 1.$revision2doesn't exist\n";1445next;1446}14471448 transmitfile($meta2->{filehash},$file2);1449}1450# otherwise we just use the working copy1451else1452{1453$file2=$state->{entries}{$filename}{modified_filename};1454}14551456# if we have been given -r, and we don't have a $file2 yet, lets get one1457if(defined($revision1)and not defined($file2) )1458{1459(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1460$meta2=$updater->getmeta($filename,$wrev);1461 transmitfile($meta2->{filehash},$file2);1462}14631464# We need to have retrieved something useful1465next unless(defined($meta1) );14661467# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1468next if(not defined($meta2)and$wrev==$meta1->{revision}1469and1470( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1471or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1472);14731474# Apparently we only show diffs for locally modified files1475next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14761477print"M Index:$filename\n";1478print"M ===================================================================\n";1479print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1480print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1481print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1482print"M diff ";1483foreachmy$opt(keys%{$state->{opt}} )1484{1485if(ref$state->{opt}{$opt}eq"ARRAY")1486{1487foreachmy$value( @{$state->{opt}{$opt}} )1488{1489print"-$opt$value";1490}1491}else{1492print"-$opt";1493print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1494}1495}1496print"$filename\n";14971498$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));14991500($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15011502if(exists$state->{opt}{u} )1503{1504system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1505}else{1506system("diff$file1$file2>$filediff");1507}15081509while( <$fh> )1510{1511print"M$_";1512}1513close$fh;1514}15151516print"ok\n";1517}15181519sub req_log1520{1521my($cmd,$data) =@_;15221523 argsplit("log");15241525$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1526#$log->debug("log state : " . Dumper($state));15271528my($minrev,$maxrev);1529if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1530{1531my$control=$2;1532$minrev=$1;1533$maxrev=$3;1534$minrev=~s/^1\.//if(defined($minrev) );1535$maxrev=~s/^1\.//if(defined($maxrev) );1536$minrev++if(defined($minrev)and$controleq"::");1537}15381539# Grab a handle to the SQLite db and do any necessary updates1540my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1541$updater->update();15421543# if no files were specified, we need to work out what files we should be providing status on ...1544 argsfromdir($updater);15451546# foreach file specified on the command line ...1547foreachmy$filename( @{$state->{args}} )1548{1549$filename= filecleanup($filename);15501551my$headmeta=$updater->getmeta($filename);15521553my$revisions=$updater->getlog($filename);1554my$totalrevisions=scalar(@$revisions);15551556if(defined($minrev) )1557{1558$log->debug("Removing revisions less than$minrev");1559while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1560{1561pop@$revisions;1562}1563}1564if(defined($maxrev) )1565{1566$log->debug("Removing revisions greater than$maxrev");1567while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1568{1569shift@$revisions;1570}1571}15721573next unless(scalar(@$revisions) );15741575print"M\n";1576print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1577print"M Working file:$filename\n";1578print"M head: 1.$headmeta->{revision}\n";1579print"M branch:\n";1580print"M locks: strict\n";1581print"M access list:\n";1582print"M symbolic names:\n";1583print"M keyword substitution: kv\n";1584print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1585print"M description:\n";15861587foreachmy$revision(@$revisions)1588{1589print"M ----------------------------\n";1590print"M revision 1.$revision->{revision}\n";1591# reformat the date for log output1592$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}) );1593$revision->{author} =~s/\s+.*//;1594$revision->{author} =~s/^(.{8}).*/$1/;1595print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1596my$commitmessage=$updater->commitmessage($revision->{commithash});1597$commitmessage=~s/^/M /mg;1598print$commitmessage."\n";1599}1600print"M =============================================================================\n";1601}16021603print"ok\n";1604}16051606sub req_annotate1607{1608my($cmd,$data) =@_;16091610 argsplit("annotate");16111612$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1613#$log->debug("status state : " . Dumper($state));16141615# Grab a handle to the SQLite db and do any necessary updates1616my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1617$updater->update();16181619# if no files were specified, we need to work out what files we should be providing annotate on ...1620 argsfromdir($updater);16211622# we'll need a temporary checkout dir1623my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1624my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1625$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");16261627$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1628$ENV{GIT_INDEX_FILE} =$file_index;16291630chdir$tmpdir;16311632# foreach file specified on the command line ...1633foreachmy$filename( @{$state->{args}} )1634{1635$filename= filecleanup($filename);16361637my$meta=$updater->getmeta($filename);16381639next unless($meta->{revision} );16401641# get all the commits that this file was in1642# in dense format -- aka skip dead revisions1643my$revisions=$updater->gethistorydense($filename);1644my$lastseenin=$revisions->[0][2];16451646# populate the temporary index based on the latest commit were we saw1647# the file -- but do it cheaply without checking out any files1648# TODO: if we got a revision from the client, use that instead1649# to look up the commithash in sqlite (still good to default to1650# the current head as we do now)1651system("git-read-tree",$lastseenin);1652unless($?==0)1653{1654die"Error running git-read-tree$lastseenin$file_index$!";1655}1656$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16571658# do a checkout of the file1659system('git-checkout-index','-f','-u',$filename);1660unless($?==0) {1661die"Error running git-checkout-index -f -u$filename:$!";1662}16631664$log->info("Annotate$filename");16651666# Prepare a file with the commits from the linearized1667# history that annotate should know about. This prevents1668# git-jsannotate telling us about commits we are hiding1669# from the client.16701671open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1672for(my$i=0;$i<@$revisions;$i++)1673{1674print ANNOTATEHINTS $revisions->[$i][2];1675if($i+1<@$revisions) {# have we got a parent?1676print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1677}1678print ANNOTATEHINTS "\n";1679}16801681print ANNOTATEHINTS "\n";1682close ANNOTATEHINTS;16831684my$annotatecmd='git-annotate';1685open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1686or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1687my$metadata= {};1688print"E Annotations for$filename\n";1689print"E ***************\n";1690while( <ANNOTATE> )1691{1692if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1693{1694my$commithash=$1;1695my$data=$2;1696unless(defined($metadata->{$commithash} ) )1697{1698$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1699$metadata->{$commithash}{author} =~s/\s+.*//;1700$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1701$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1702}1703printf("M 1.%-5d (%-8s%10s):%s\n",1704$metadata->{$commithash}{revision},1705$metadata->{$commithash}{author},1706$metadata->{$commithash}{modified},1707$data1708);1709}else{1710$log->warn("Error in annotate output! LINE:$_");1711print"E Annotate error\n";1712next;1713}1714}1715close ANNOTATE;1716}17171718# done; get out of the tempdir1719chdir"/";17201721print"ok\n";17221723}17241725# This method takes the state->{arguments} array and produces two new arrays.1726# The first is $state->{args} which is everything before the '--' argument, and1727# the second is $state->{files} which is everything after it.1728sub argsplit1729{1730return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");17311732my$type=shift;17331734$state->{args} = [];1735$state->{files} = [];1736$state->{opt} = {};17371738if(defined($type) )1739{1740my$opt= {};1741$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");1742$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1743$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");1744$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1745$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1746$opt= { k =>1, m =>1}if($typeeq"add");1747$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1748$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");174917501751while(scalar( @{$state->{arguments}} ) >0)1752{1753my$arg=shift@{$state->{arguments}};17541755next if($argeq"--");1756next unless($arg=~/\S/);17571758# if the argument looks like a switch1759if($arg=~/^-(\w)(.*)/)1760{1761# if it's a switch that takes an argument1762if($opt->{$1} )1763{1764# If this switch has already been provided1765if($opt->{$1} >1and exists($state->{opt}{$1} ) )1766{1767$state->{opt}{$1} = [$state->{opt}{$1} ];1768if(length($2) >0)1769{1770push@{$state->{opt}{$1}},$2;1771}else{1772push@{$state->{opt}{$1}},shift@{$state->{arguments}};1773}1774}else{1775# if there's extra data in the arg, use that as the argument for the switch1776if(length($2) >0)1777{1778$state->{opt}{$1} =$2;1779}else{1780$state->{opt}{$1} =shift@{$state->{arguments}};1781}1782}1783}else{1784$state->{opt}{$1} =undef;1785}1786}1787else1788{1789push@{$state->{args}},$arg;1790}1791}1792}1793else1794{1795my$mode=0;17961797foreachmy$value( @{$state->{arguments}} )1798{1799if($valueeq"--")1800{1801$mode++;1802next;1803}1804push@{$state->{args}},$valueif($mode==0);1805push@{$state->{files}},$valueif($mode==1);1806}1807}1808}18091810# This method uses $state->{directory} to populate $state->{args} with a list of filenames1811sub argsfromdir1812{1813my$updater=shift;18141815$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");18161817return if(scalar( @{$state->{args}} ) >1);18181819my@gethead= @{$updater->gethead};18201821# push added files1822foreachmy$file(keys%{$state->{entries}}) {1823if(exists$state->{entries}{$file}{revision} &&1824$state->{entries}{$file}{revision} ==0)1825{1826push@gethead, { name =>$file, filehash =>'added'};1827}1828}18291830if(scalar(@{$state->{args}}) ==1)1831{1832my$arg=$state->{args}[0];1833$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );18341835$log->info("Only one arg specified, checking for directory expansion on '$arg'");18361837foreachmy$file(@gethead)1838{1839next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1840next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1841push@{$state->{args}},$file->{name};1842}18431844shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1845}else{1846$log->info("Only one arg specified, populating file list automatically");18471848$state->{args} = [];18491850foreachmy$file(@gethead)1851{1852next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1853next unless($file->{name} =~s/^$state->{prependdir}//);1854push@{$state->{args}},$file->{name};1855}1856}1857}18581859# This method cleans up the $state variable after a command that uses arguments has run1860sub statecleanup1861{1862$state->{files} = [];1863$state->{args} = [];1864$state->{arguments} = [];1865$state->{entries} = {};1866}18671868sub revparse1869{1870my$filename=shift;18711872returnundefunless(defined($state->{entries}{$filename}{revision} ) );18731874return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1875return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18761877returnundef;1878}18791880# This method takes a file hash and does a CVS "file transfer" which transmits the1881# size of the file, and then the file contents.1882# If a second argument $targetfile is given, the file is instead written out to1883# a file by the name of $targetfile1884sub transmitfile1885{1886my$filehash=shift;1887my$targetfile=shift;18881889if(defined($filehash)and$filehasheq"deleted")1890{1891$log->warn("filehash is 'deleted'");1892return;1893}18941895die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);18961897my$type=`git-cat-file -t$filehash`;1898 chomp$type;18991900 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );19011902 my$size= `git-cat-file -s $filehash`;1903chomp$size;19041905$log->debug("transmitfile($filehash) size=$size, type=$type");19061907if(open my$fh,'-|',"git-cat-file","blob",$filehash)1908{1909if(defined($targetfile) )1910{1911open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1912print NEWFILE $_while( <$fh> );1913close NEWFILE;1914}else{1915print"$size\n";1916printwhile( <$fh> );1917}1918close$fhor die("Couldn't close filehandle for transmitfile()");1919}else{1920die("Couldn't execute git-cat-file");1921}1922}19231924# This method takes a file name, and returns ( $dirpart, $filepart ) which1925# refers to the directory portion and the file portion of the filename1926# respectively1927sub filenamesplit1928{1929my$filename=shift;1930my$fixforlocaldir=shift;19311932my($filepart,$dirpart) = ($filename,".");1933($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1934$dirpart.="/";19351936if($fixforlocaldir)1937{1938$dirpart=~s/^$state->{prependdir}//;1939}19401941return($filepart,$dirpart);1942}19431944sub filecleanup1945{1946my$filename=shift;19471948returnundefunless(defined($filename));1949if($filename=~/^\// )1950{1951print"E absolute filenames '$filename' not supported by server\n";1952returnundef;1953}19541955$filename=~s/^\.\///g;1956$filename=$state->{prependdir} .$filename;1957return$filename;1958}19591960# Given a path, this function returns a string containing the kopts1961# that should go into that path's Entries line. For example, a binary1962# file should get -kb.1963sub kopts_from_path1964{1965my($path) =@_;19661967# Once it exists, the git attributes system should be used to look up1968# what attributes apply to this path.19691970# Until then, take the setting from the config file1971unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1972{1973# Return "" to give no special treatment to any path1974return"";1975}else{1976# Alternatively, to have all files treated as if they are binary (which1977# is more like git itself), always return the "-kb" option1978return"-kb";1979}1980}19811982package GITCVS::log;19831984####1985#### Copyright The Open University UK - 2006.1986####1987#### Authors: Martyn Smith <martyn@catalyst.net.nz>1988#### Martin Langhoff <martin@catalyst.net.nz>1989####1990####19911992use strict;1993use warnings;19941995=head1 NAME19961997GITCVS::log19981999=head1 DESCRIPTION20002001This module provides very crude logging with a similar interface to2002Log::Log4perl20032004=head1 METHODS20052006=cut20072008=head2 new20092010Creates a new log object, optionally you can specify a filename here to2011indicate the file to log to. If no log file is specified, you can specify one2012later with method setfile, or indicate you no longer want logging with method2013nofile.20142015Until one of these methods is called, all log calls will buffer messages ready2016to write out.20172018=cut2019sub new2020{2021my$class=shift;2022my$filename=shift;20232024my$self= {};20252026bless$self,$class;20272028if(defined($filename) )2029{2030open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2031}20322033return$self;2034}20352036=head2 setfile20372038This methods takes a filename, and attempts to open that file as the log file.2039If successful, all buffered data is written out to the file, and any further2040logging is written directly to the file.20412042=cut2043sub setfile2044{2045my$self=shift;2046my$filename=shift;20472048if(defined($filename) )2049{2050open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2051}20522053return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20542055while(my$line=shift@{$self->{buffer}} )2056{2057print{$self->{fh}}$line;2058}2059}20602061=head2 nofile20622063This method indicates no logging is going to be used. It flushes any entries in2064the internal buffer, and sets a flag to ensure no further data is put there.20652066=cut2067sub nofile2068{2069my$self=shift;20702071$self->{nolog} =1;20722073return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20742075$self->{buffer} = [];2076}20772078=head2 _logopen20792080Internal method. Returns true if the log file is open, false otherwise.20812082=cut2083sub _logopen2084{2085my$self=shift;20862087return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2088return0;2089}20902091=head2 debug info warn fatal20922093These four methods are wrappers to _log. They provide the actual interface for2094logging data.20952096=cut2097sub debug {my$self=shift;$self->_log("debug",@_); }2098sub info {my$self=shift;$self->_log("info",@_); }2099subwarn{my$self=shift;$self->_log("warn",@_); }2100sub fatal {my$self=shift;$self->_log("fatal",@_); }21012102=head2 _log21032104This is an internal method called by the logging functions. It generates a2105timestamp and pushes the logged line either to file, or internal buffer.21062107=cut2108sub _log2109{2110my$self=shift;2111my$level=shift;21122113return if($self->{nolog} );21142115my@time=localtime;2116my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2117$time[5] +1900,2118$time[4] +1,2119$time[3],2120$time[2],2121$time[1],2122$time[0],2123uc$level,2124);21252126if($self->_logopen)2127{2128print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2129}else{2130push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2131}2132}21332134=head2 DESTROY21352136This method simply closes the file handle if one is open21372138=cut2139sub DESTROY2140{2141my$self=shift;21422143if($self->_logopen)2144{2145close$self->{fh};2146}2147}21482149package GITCVS::updater;21502151####2152#### Copyright The Open University UK - 2006.2153####2154#### Authors: Martyn Smith <martyn@catalyst.net.nz>2155#### Martin Langhoff <martin@catalyst.net.nz>2156####2157####21582159use strict;2160use warnings;2161use DBI;21622163=head1 METHODS21642165=cut21662167=head2 new21682169=cut2170sub new2171{2172my$class=shift;2173my$config=shift;2174my$module=shift;2175my$log=shift;21762177die"Need to specify a git repository"unless(defined($config)and-d $config);2178die"Need to specify a module"unless(defined($module) );21792180$class=ref($class) ||$class;21812182my$self= {};21832184bless$self,$class;21852186$self->{module} =$module;2187$self->{git_path} =$config."/";21882189$self->{log} =$log;21902191die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );21922193$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2194$cfg->{gitcvs}{dbdriver} ||"SQLite";2195$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2196$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2197$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2198$cfg->{gitcvs}{dbuser} ||"";2199$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2200$cfg->{gitcvs}{dbpass} ||"";2201my%mapping= ( m =>$module,2202 a =>$state->{method},2203 u =>getlogin||getpwuid($<) || $<,2204 G =>$self->{git_path},2205 g => mangle_dirname($self->{git_path}),2206);2207$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2208$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;22092210die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2211die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2212$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2213$self->{dbuser},2214$self->{dbpass});2215die"Error connecting to database\n"unlessdefined$self->{dbh};22162217$self->{tables} = {};2218foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2219{2220$self->{tables}{$table} =1;2221}22222223# Construct the revision table if required2224unless($self->{tables}{revision} )2225{2226$self->{dbh}->do("2227 CREATE TABLE revision (2228 name TEXT NOT NULL,2229 revision INTEGER NOT NULL,2230 filehash TEXT NOT NULL,2231 commithash TEXT NOT NULL,2232 author TEXT NOT NULL,2233 modified TEXT NOT NULL,2234 mode TEXT NOT NULL2235 )2236 ");2237$self->{dbh}->do("2238 CREATE INDEX revision_ix12239 ON revision (name,revision)2240 ");2241$self->{dbh}->do("2242 CREATE INDEX revision_ix22243 ON revision (name,commithash)2244 ");2245}22462247# Construct the head table if required2248unless($self->{tables}{head} )2249{2250$self->{dbh}->do("2251 CREATE TABLE head (2252 name TEXT NOT NULL,2253 revision INTEGER NOT NULL,2254 filehash TEXT NOT NULL,2255 commithash TEXT NOT NULL,2256 author TEXT NOT NULL,2257 modified TEXT NOT NULL,2258 mode TEXT NOT NULL2259 )2260 ");2261$self->{dbh}->do("2262 CREATE INDEX head_ix12263 ON head (name)2264 ");2265}22662267# Construct the properties table if required2268unless($self->{tables}{properties} )2269{2270$self->{dbh}->do("2271 CREATE TABLE properties (2272 key TEXT NOT NULL PRIMARY KEY,2273 value TEXT2274 )2275 ");2276}22772278# Construct the commitmsgs table if required2279unless($self->{tables}{commitmsgs} )2280{2281$self->{dbh}->do("2282 CREATE TABLE commitmsgs (2283 key TEXT NOT NULL PRIMARY KEY,2284 value TEXT2285 )2286 ");2287}22882289return$self;2290}22912292=head2 update22932294=cut2295sub update2296{2297my$self=shift;22982299# first lets get the commit list2300$ENV{GIT_DIR} =$self->{git_path};23012302my$commitsha1=`git rev-parse$self->{module}`;2303chomp$commitsha1;23042305my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2306unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2307{2308die("Invalid module '$self->{module}'");2309}231023112312my$git_log;2313my$lastcommit=$self->_get_prop("last_commit");23142315if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2316return1;2317}23182319# Start exclusive lock here...2320$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";23212322# TODO: log processing is memory bound2323# if we can parse into a 2nd file that is in reverse order2324# we can probably do something really efficient2325my@git_log_params= ('--pretty','--parents','--topo-order');23262327if(defined$lastcommit) {2328push@git_log_params,"$lastcommit..$self->{module}";2329}else{2330push@git_log_params,$self->{module};2331}2332# git-rev-list is the backend / plumbing version of git-log2333open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";23342335my@commits;23362337my%commit= ();23382339while( <GITLOG> )2340{2341chomp;2342if(m/^commit\s+(.*)$/) {2343# on ^commit lines put the just seen commit in the stack2344# and prime things for the next one2345if(keys%commit) {2346my%copy=%commit;2347unshift@commits, \%copy;2348%commit= ();2349}2350my@parents=split(m/\s+/,$1);2351$commit{hash} =shift@parents;2352$commit{parents} = \@parents;2353}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2354# on rfc822-like lines seen before we see any message,2355# lowercase the entry and put it in the hash as key-value2356$commit{lc($1)} =$2;2357}else{2358# message lines - skip initial empty line2359# and trim whitespace2360if(!exists($commit{message}) &&m/^\s*$/) {2361# define it to mark the end of headers2362$commit{message} ='';2363next;2364}2365s/^\s+//;s/\s+$//;# trim ws2366$commit{message} .=$_."\n";2367}2368}2369close GITLOG;23702371unshift@commits, \%commitif(keys%commit);23722373# Now all the commits are in the @commits bucket2374# ordered by time DESC. for each commit that needs processing,2375# determine whether it's following the last head we've seen or if2376# it's on its own branch, grab a file list, and add whatever's changed2377# NOTE: $lastcommit refers to the last commit from previous run2378# $lastpicked is the last commit we picked in this run2379my$lastpicked;2380my$head= {};2381if(defined$lastcommit) {2382$lastpicked=$lastcommit;2383}23842385my$committotal=scalar(@commits);2386my$commitcount=0;23872388# Load the head table into $head (for cached lookups during the update process)2389foreachmy$file( @{$self->gethead()} )2390{2391$head->{$file->{name}} =$file;2392}23932394foreachmy$commit(@commits)2395{2396$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2397if(defined$lastpicked)2398{2399if(!in_array($lastpicked, @{$commit->{parents}}))2400{2401# skip, we'll see this delta2402# as part of a merge later2403# warn "skipping off-track $commit->{hash}\n";2404next;2405}elsif(@{$commit->{parents}} >1) {2406# it is a merge commit, for each parent that is2407# not $lastpicked, see if we can get a log2408# from the merge-base to that parent to put it2409# in the message as a merge summary.2410my@parents= @{$commit->{parents}};2411foreachmy$parent(@parents) {2412# git-merge-base can potentially (but rarely) throw2413# several candidate merge bases. let's assume2414# that the first one is the best one.2415if($parenteq$lastpicked) {2416next;2417}2418open my$p,'git-merge-base '.$lastpicked.' '2419.$parent.'|';2420my@output= (<$p>);2421close$p;2422my$base=join('',@output);2423chomp$base;2424if($base) {2425my@merged;2426# print "want to log between $base $parent \n";2427open(GITLOG,'-|','git-log',"$base..$parent")2428or die"Cannot call git-log:$!";2429my$mergedhash;2430while(<GITLOG>) {2431chomp;2432if(!defined$mergedhash) {2433if(m/^commit\s+(.+)$/) {2434$mergedhash=$1;2435}else{2436next;2437}2438}else{2439# grab the first line that looks non-rfc8222440# aka has content after leading space2441if(m/^\s+(\S.*)$/) {2442my$title=$1;2443$title=substr($title,0,100);# truncate2444unshift@merged,"$mergedhash$title";2445undef$mergedhash;2446}2447}2448}2449close GITLOG;2450if(@merged) {2451$commit->{mergemsg} =$commit->{message};2452$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2453foreachmy$summary(@merged) {2454$commit->{mergemsg} .="\t$summary\n";2455}2456$commit->{mergemsg} .="\n\n";2457# print "Message for $commit->{hash} \n$commit->{mergemsg}";2458}2459}2460}2461}2462}24632464# convert the date to CVS-happy format2465$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24662467if(defined($lastpicked) )2468{2469my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2470local($/) ="\0";2471while( <FILELIST> )2472{2473chomp;2474unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2475{2476die("Couldn't process git-diff-tree line :$_");2477}2478my($mode,$hash,$change) = ($1,$2,$3);2479my$name= <FILELIST>;2480chomp($name);24812482# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24832484my$git_perms="";2485$git_perms.="r"if($mode&4);2486$git_perms.="w"if($mode&2);2487$git_perms.="x"if($mode&1);2488$git_perms="rw"if($git_permseq"");24892490if($changeeq"D")2491{2492#$log->debug("DELETE $name");2493$head->{$name} = {2494 name =>$name,2495 revision =>$head->{$name}{revision} +1,2496 filehash =>"deleted",2497 commithash =>$commit->{hash},2498 modified =>$commit->{date},2499 author =>$commit->{author},2500 mode =>$git_perms,2501};2502$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2503}2504elsif($changeeq"M")2505{2506#$log->debug("MODIFIED $name");2507$head->{$name} = {2508 name =>$name,2509 revision =>$head->{$name}{revision} +1,2510 filehash =>$hash,2511 commithash =>$commit->{hash},2512 modified =>$commit->{date},2513 author =>$commit->{author},2514 mode =>$git_perms,2515};2516$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2517}2518elsif($changeeq"A")2519{2520#$log->debug("ADDED $name");2521$head->{$name} = {2522 name =>$name,2523 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2524 filehash =>$hash,2525 commithash =>$commit->{hash},2526 modified =>$commit->{date},2527 author =>$commit->{author},2528 mode =>$git_perms,2529};2530$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2531}2532else2533{2534$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2535die;2536}2537}2538close FILELIST;2539}else{2540# this is used to detect files removed from the repo2541my$seen_files= {};25422543my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2544local$/="\0";2545while( <FILELIST> )2546{2547chomp;2548unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2549{2550die("Couldn't process git-ls-tree line :$_");2551}25522553my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);25542555$seen_files->{$git_filename} =1;25562557my($oldhash,$oldrevision,$oldmode) = (2558$head->{$git_filename}{filehash},2559$head->{$git_filename}{revision},2560$head->{$git_filename}{mode}2561);25622563if($git_perms=~/^\d\d\d(\d)\d\d/o)2564{2565$git_perms="";2566$git_perms.="r"if($1&4);2567$git_perms.="w"if($1&2);2568$git_perms.="x"if($1&1);2569}else{2570$git_perms="rw";2571}25722573# unless the file exists with the same hash, we need to update it ...2574unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2575{2576my$newrevision= ($oldrevisionor0) +1;25772578$head->{$git_filename} = {2579 name =>$git_filename,2580 revision =>$newrevision,2581 filehash =>$git_hash,2582 commithash =>$commit->{hash},2583 modified =>$commit->{date},2584 author =>$commit->{author},2585 mode =>$git_perms,2586};258725882589$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2590}2591}2592close FILELIST;25932594# Detect deleted files2595foreachmy$file(keys%$head)2596{2597unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2598{2599$head->{$file}{revision}++;2600$head->{$file}{filehash} ="deleted";2601$head->{$file}{commithash} =$commit->{hash};2602$head->{$file}{modified} =$commit->{date};2603$head->{$file}{author} =$commit->{author};26042605$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2606}2607}2608# END : "Detect deleted files"2609}261026112612if(exists$commit->{mergemsg})2613{2614$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2615}26162617$lastpicked=$commit->{hash};26182619$self->_set_prop("last_commit",$commit->{hash});2620}26212622$self->delete_head();2623foreachmy$file(keys%$head)2624{2625$self->insert_head(2626$file,2627$head->{$file}{revision},2628$head->{$file}{filehash},2629$head->{$file}{commithash},2630$head->{$file}{modified},2631$head->{$file}{author},2632$head->{$file}{mode},2633);2634}2635# invalidate the gethead cache2636$self->{gethead_cache} =undef;263726382639# Ending exclusive lock here2640$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2641}26422643sub insert_rev2644{2645my$self=shift;2646my$name=shift;2647my$revision=shift;2648my$filehash=shift;2649my$commithash=shift;2650my$modified=shift;2651my$author=shift;2652my$mode=shift;26532654my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2655$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2656}26572658sub insert_mergelog2659{2660my$self=shift;2661my$key=shift;2662my$value=shift;26632664my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2665$insert_mergelog->execute($key,$value);2666}26672668sub delete_head2669{2670my$self=shift;26712672my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2673$delete_head->execute();2674}26752676sub insert_head2677{2678my$self=shift;2679my$name=shift;2680my$revision=shift;2681my$filehash=shift;2682my$commithash=shift;2683my$modified=shift;2684my$author=shift;2685my$mode=shift;26862687my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2688$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2689}26902691sub _headrev2692{2693my$self=shift;2694my$filename=shift;26952696my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2697$db_query->execute($filename);2698my($hash,$revision,$mode) =$db_query->fetchrow_array;26992700return($hash,$revision,$mode);2701}27022703sub _get_prop2704{2705my$self=shift;2706my$key=shift;27072708my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2709$db_query->execute($key);2710my($value) =$db_query->fetchrow_array;27112712return$value;2713}27142715sub _set_prop2716{2717my$self=shift;2718my$key=shift;2719my$value=shift;27202721my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2722$db_query->execute($value,$key);27232724unless($db_query->rows)2725{2726$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2727$db_query->execute($key,$value);2728}27292730return$value;2731}27322733=head2 gethead27342735=cut27362737sub gethead2738{2739my$self=shift;27402741return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );27422743my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2744$db_query->execute();27452746my$tree= [];2747while(my$file=$db_query->fetchrow_hashref)2748{2749push@$tree,$file;2750}27512752$self->{gethead_cache} =$tree;27532754return$tree;2755}27562757=head2 getlog27582759=cut27602761sub getlog2762{2763my$self=shift;2764my$filename=shift;27652766my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2767$db_query->execute($filename);27682769my$tree= [];2770while(my$file=$db_query->fetchrow_hashref)2771{2772push@$tree,$file;2773}27742775return$tree;2776}27772778=head2 getmeta27792780This function takes a filename (with path) argument and returns a hashref of2781metadata for that file.27822783=cut27842785sub getmeta2786{2787my$self=shift;2788my$filename=shift;2789my$revision=shift;27902791my$db_query;2792if(defined($revision)and$revision=~/^\d+$/)2793{2794$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2795$db_query->execute($filename,$revision);2796}2797elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2798{2799$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2800$db_query->execute($filename,$revision);2801}else{2802$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2803$db_query->execute($filename);2804}28052806return$db_query->fetchrow_hashref;2807}28082809=head2 commitmessage28102811this function takes a commithash and returns the commit message for that commit28122813=cut2814sub commitmessage2815{2816my$self=shift;2817my$commithash=shift;28182819die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);28202821my$db_query;2822$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2823$db_query->execute($commithash);28242825my($message) =$db_query->fetchrow_array;28262827if(defined($message) )2828{2829$message.=" "if($message=~/\n$/);2830return$message;2831}28322833my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2834shift@lineswhile($lines[0] =~/\S/);2835$message=join("",@lines);2836$message.=" "if($message=~/\n$/);2837return$message;2838}28392840=head2 gethistory28412842This function takes a filename (with path) argument and returns an arrayofarrays2843containing revision,filehash,commithash ordered by revision descending28442845=cut2846sub gethistory2847{2848my$self=shift;2849my$filename=shift;28502851my$db_query;2852$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2853$db_query->execute($filename);28542855return$db_query->fetchall_arrayref;2856}28572858=head2 gethistorydense28592860This function takes a filename (with path) argument and returns an arrayofarrays2861containing revision,filehash,commithash ordered by revision descending.28622863This version of gethistory skips deleted entries -- so it is useful for annotate.2864The 'dense' part is a reference to a '--dense' option available for git-rev-list2865and other git tools that depend on it.28662867=cut2868sub gethistorydense2869{2870my$self=shift;2871my$filename=shift;28722873my$db_query;2874$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2875$db_query->execute($filename);28762877return$db_query->fetchall_arrayref;2878}28792880=head2 in_array()28812882from Array::PAT - mimics the in_array() function2883found in PHP. Yuck but works for small arrays.28842885=cut2886sub in_array2887{2888my($check,@array) =@_;2889my$retval=0;2890foreachmy$test(@array){2891if($checkeq$test){2892$retval=1;2893}2894}2895return$retval;2896}28972898=head2 safe_pipe_capture28992900an alternative to `command` that allows input to be passed as an array2901to work around shell problems with weird characters in arguments29022903=cut2904sub safe_pipe_capture {29052906my@output;29072908if(my$pid=open my$child,'-|') {2909@output= (<$child>);2910close$childor die join(' ',@_).":$!$?";2911}else{2912exec(@_)or die"$!$?";# exec() can fail the executable can't be found2913}2914returnwantarray?@output:join('',@output);2915}29162917=head2 mangle_dirname29182919create a string from a directory name that is suitable to use as2920part of a filename, mainly by converting all chars except \w.- to _29212922=cut2923sub mangle_dirname {2924my$dirname=shift;2925return unlessdefined$dirname;29262927$dirname=~s/[^\w.-]/_/g;29282929return$dirname;2930}293129321;