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