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 defined($state->{entries}{$filename}{modified_hash}) 897and not exists($state->{opt}{C} ) ) 898{ 899$log->info("Tell the client the file is modified"); 900print"MT text M\n"; 901print"MT fname$filename\n"; 902print"MT newline\n"; 903next; 904} 905 906if($meta->{filehash}eq"deleted") 907{ 908my($filepart,$dirpart) = filenamesplit($filename,1); 909 910$log->info("Removing '$filename' from working copy (no longer in the repo)"); 911 912print"E cvs update: `$filename' is no longer in the repository\n"; 913# Don't want to actually _DO_ the update if -n specified 914unless($state->{globaloptions}{-n} ) { 915print"Removed$dirpart\n"; 916print"$filepart\n"; 917} 918} 919elsif(not defined($state->{entries}{$filename}{modified_hash} ) 920or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 921or$meta->{filehash}eq'added') 922{ 923# normal update, just send the new revision (either U=Update, 924# or A=Add, or R=Remove) 925if(defined($wrev) &&$wrev<0) 926{ 927$log->info("Tell the client the file is scheduled for removal"); 928print"MT text R\n"; 929print"MT fname$filename\n"; 930print"MT newline\n"; 931next; 932} 933elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 934{ 935$log->info("Tell the client the file is scheduled for addition"); 936print"MT text A\n"; 937print"MT fname$filename\n"; 938print"MT newline\n"; 939next; 940 941} 942else{ 943$log->info("Updating '$filename' to ".$meta->{revision}); 944print"MT +updated\n"; 945print"MT text U\n"; 946print"MT fname$filename\n"; 947print"MT newline\n"; 948print"MT -updated\n"; 949} 950 951my($filepart,$dirpart) = filenamesplit($filename,1); 952 953# Don't want to actually _DO_ the update if -n specified 954unless($state->{globaloptions}{-n} ) 955{ 956if(defined($wrev) ) 957{ 958# instruct client we're sending a file to put in this path as a replacement 959print"Update-existing$dirpart\n"; 960$log->debug("Updating existing file 'Update-existing$dirpart'"); 961}else{ 962# instruct client we're sending a file to put in this path as a new file 963print"Clear-static-directory$dirpart\n"; 964print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 965print"Clear-sticky$dirpart\n"; 966print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 967 968$log->debug("Creating new file 'Created$dirpart'"); 969print"Created$dirpart\n"; 970} 971print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 972 973# this is an "entries" line 974my$kopts= kopts_from_path($filepart); 975$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 976print"/$filepart/1.$meta->{revision}//$kopts/\n"; 977 978# permissions 979$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 980print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 981 982# transmit file 983 transmitfile($meta->{filehash}); 984} 985}else{ 986$log->info("Updating '$filename'"); 987my($filepart,$dirpart) = filenamesplit($meta->{name},1); 988 989my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 990 991chdir$dir; 992my$file_local=$filepart.".mine"; 993system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 994my$file_old=$filepart.".".$oldmeta->{revision}; 995 transmitfile($oldmeta->{filehash},$file_old); 996my$file_new=$filepart.".".$meta->{revision}; 997 transmitfile($meta->{filehash},$file_new); 998 999# we need to merge with the local changes ( M=successful merge, C=conflict merge )1000$log->info("Merging$file_local,$file_old,$file_new");1001print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10021003$log->debug("Temporary directory for merge is$dir");10041005my$return=system("git","merge-file",$file_local,$file_old,$file_new);1006$return>>=8;10071008if($return==0)1009{1010$log->info("Merged successfully");1011print"M M$filename\n";1012$log->debug("Merged$dirpart");10131014# Don't want to actually _DO_ the update if -n specified1015unless($state->{globaloptions}{-n} )1016{1017print"Merged$dirpart\n";1018$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1019print$state->{CVSROOT} ."/$state->{module}/$filename\n";1020my$kopts= kopts_from_path($filepart);1021$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1022print"/$filepart/1.$meta->{revision}//$kopts/\n";1023}1024}1025elsif($return==1)1026{1027$log->info("Merged with conflicts");1028print"E cvs update: conflicts found in$filename\n";1029print"M C$filename\n";10301031# Don't want to actually _DO_ the update if -n specified1032unless($state->{globaloptions}{-n} )1033{1034print"Merged$dirpart\n";1035print$state->{CVSROOT} ."/$state->{module}/$filename\n";1036my$kopts= kopts_from_path($filepart);1037print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1038}1039}1040else1041{1042$log->warn("Merge failed");1043next;1044}10451046# Don't want to actually _DO_ the update if -n specified1047unless($state->{globaloptions}{-n} )1048{1049# permissions1050$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1051print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10521053# transmit file, format is single integer on a line by itself (file1054# size) followed by the file contents1055# TODO : we should copy files in blocks1056my$data=`cat$file_local`;1057$log->debug("File size : " . length($data));1058 print length($data) . "\n";1059 print$data;1060 }10611062 chdir "/";1063 }10641065 }10661067 print "ok\n";1068}10691070sub req_ci1071{1072 my ($cmd,$data) =@_;10731074 argsplit("ci");10751076 #$log->debug("State : " . Dumper($state));10771078$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10791080 if ($state->{method} eq 'pserver')1081 {1082 print "error 1 pserver access cannot commit\n";1083 exit;1084 }10851086 if ( -e$state->{CVSROOT} . "/index" )1087 {1088$log->warn("file 'index' already exists in the git repository");1089 print "error 1 Index already exists in git repo\n";1090 exit;1091 }10921093 # Grab a handle to the SQLite db and do any necessary updates1094 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1095$updater->update();10961097 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1098 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1099$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");11001101$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1102$ENV{GIT_INDEX_FILE} =$file_index;11031104 # Remember where the head was at the beginning.1105 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1106 chomp$parenthash;1107 if ($parenthash!~ /^[0-9a-f]{40}$/) {1108 print "error 1 pserver cannot find the current HEAD of module";1109 exit;1110 }11111112 chdir$tmpdir;11131114 # populate the temporary index based1115 system("git-read-tree",$parenthash);1116 unless ($?== 0)1117 {1118 die "Error running git-read-tree$state->{module}$file_index$!";1119 }1120$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");11211122 my@committedfiles= ();1123 my%oldmeta;11241125 # foreach file specified on the command line ...1126 foreach my$filename( @{$state->{args}} )1127 {1128 my$committedfile=$filename;1129$filename= filecleanup($filename);11301131 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );11321133 my$meta=$updater->getmeta($filename);1134$oldmeta{$filename} =$meta;11351136 my$wrev= revparse($filename);11371138 my ($filepart,$dirpart) = filenamesplit($filename);11391140 # do a checkout of the file if it part of this tree1141 if ($wrev) {1142 system('git-checkout-index', '-f', '-u',$filename);1143 unless ($?== 0) {1144 die "Error running git-checkout-index -f -u$filename:$!";1145 }1146 }11471148 my$addflag= 0;1149 my$rmflag= 0;1150$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1151$addflag= 1 unless ( -e$filename);11521153 # Do up to date checking1154 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1155 {1156 # fail everything if an up to date check fails1157 print "error 1 Up to date check failed for$filename\n";1158 chdir "/";1159 exit;1160 }11611162 push@committedfiles,$committedfile;1163$log->info("Committing$filename");11641165 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11661167 unless ($rmflag)1168 {1169$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1170 rename$state->{entries}{$filename}{modified_filename},$filename;11711172 # Calculate modes to remove1173 my$invmode= "";1174 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11751176$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1177 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1178 }11791180 if ($rmflag)1181 {1182$log->info("Removing file '$filename'");1183 unlink($filename);1184 system("git-update-index", "--remove",$filename);1185 }1186 elsif ($addflag)1187 {1188$log->info("Adding file '$filename'");1189 system("git-update-index", "--add",$filename);1190 } else {1191$log->info("Updating file '$filename'");1192 system("git-update-index",$filename);1193 }1194 }11951196 unless ( scalar(@committedfiles) > 0 )1197 {1198 print "E No files to commit\n";1199 print "ok\n";1200 chdir "/";1201 return;1202 }12031204 my$treehash= `git-write-tree`;1205 chomp$treehash;12061207$log->debug("Treehash :$treehash, Parenthash :$parenthash");12081209 # write our commit message out if we have one ...1210 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1211 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1212 print$msg_fh"\n\nvia git-CVS emulator\n";1213 close$msg_fh;12141215 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1216chomp($commithash);1217$log->info("Commit hash :$commithash");12181219unless($commithash=~/[a-zA-Z0-9]{40}/)1220{1221$log->warn("Commit failed (Invalid commit hash)");1222print"error 1 Commit failed (unknown reason)\n";1223chdir"/";1224exit;1225}12261227# Check that this is allowed, just as we would with a receive-pack1228my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1229$parenthash,$commithash);1230if( -x $cmd[0] ) {1231unless(system(@cmd) ==0)1232{1233$log->warn("Commit failed (update hook declined to update ref)");1234print"error 1 Commit failed (update hook declined)\n";1235chdir"/";1236exit;1237}1238}12391240if(system(qw(git update-ref -m),"cvsserver ci",1241"refs/heads/$state->{module}",$commithash,$parenthash)) {1242$log->warn("update-ref for$state->{module} failed.");1243print"error 1 Cannot commit -- update first\n";1244exit;1245}12461247$updater->update();12481249# foreach file specified on the command line ...1250foreachmy$filename(@committedfiles)1251{1252$filename= filecleanup($filename);12531254my$meta=$updater->getmeta($filename);1255unless(defined$meta->{revision}) {1256$meta->{revision} =1;1257}12581259my($filepart,$dirpart) = filenamesplit($filename,1);12601261$log->debug("Checked-in$dirpart:$filename");12621263print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1264if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1265{1266print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1267print"Remove-entry$dirpart\n";1268print"$filename\n";1269}else{1270if($meta->{revision} ==1) {1271print"M initial revision: 1.1\n";1272}else{1273print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1274}1275print"Checked-in$dirpart\n";1276print"$filename\n";1277my$kopts= kopts_from_path($filepart);1278print"/$filepart/1.$meta->{revision}//$kopts/\n";1279}1280}12811282chdir"/";1283print"ok\n";1284}12851286sub req_status1287{1288my($cmd,$data) =@_;12891290 argsplit("status");12911292$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1293#$log->debug("status state : " . Dumper($state));12941295# Grab a handle to the SQLite db and do any necessary updates1296my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1297$updater->update();12981299# if no files were specified, we need to work out what files we should be providing status on ...1300 argsfromdir($updater);13011302# foreach file specified on the command line ...1303foreachmy$filename( @{$state->{args}} )1304{1305$filename= filecleanup($filename);13061307my$meta=$updater->getmeta($filename);1308my$oldmeta=$meta;13091310my$wrev= revparse($filename);13111312# If the working copy is an old revision, lets get that version too for comparison.1313if(defined($wrev)and$wrev!=$meta->{revision} )1314{1315$oldmeta=$updater->getmeta($filename,$wrev);1316}13171318# TODO : All possible statuses aren't yet implemented1319my$status;1320# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1321$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1322and1323( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1324or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1325);13261327# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1328$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1329and1330($state->{entries}{$filename}{unchanged}1331or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1332);13331334# Need checkout if it exists in the repo but doesn't have a working copy1335$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );13361337# Locally modified if working copy and repo copy have the same revision but there are local changes1338$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );13391340# Needs Merge if working copy revision is less than repo copy and there are local changes1341$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );13421343$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1344$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1345$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1346$status||="File had conflicts on merge"if(0);13471348$status||="Unknown";13491350print"M ===================================================================\n";1351print"M File:$filename\tStatus:$status\n";1352if(defined($state->{entries}{$filename}{revision}) )1353{1354print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1355}else{1356print"M Working revision:\tNo entry for$filename\n";1357}1358if(defined($meta->{revision}) )1359{1360print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1361print"M Sticky Tag:\t\t(none)\n";1362print"M Sticky Date:\t\t(none)\n";1363print"M Sticky Options:\t\t(none)\n";1364}else{1365print"M Repository revision:\tNo revision control file\n";1366}1367print"M\n";1368}13691370print"ok\n";1371}13721373sub req_diff1374{1375my($cmd,$data) =@_;13761377 argsplit("diff");13781379$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1380#$log->debug("status state : " . Dumper($state));13811382my($revision1,$revision2);1383if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1384{1385$revision1=$state->{opt}{r}[0];1386$revision2=$state->{opt}{r}[1];1387}else{1388$revision1=$state->{opt}{r};1389}13901391$revision1=~s/^1\.//if(defined($revision1) );1392$revision2=~s/^1\.//if(defined($revision2) );13931394$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );13951396# Grab a handle to the SQLite db and do any necessary updates1397my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1398$updater->update();13991400# if no files were specified, we need to work out what files we should be providing status on ...1401 argsfromdir($updater);14021403# foreach file specified on the command line ...1404foreachmy$filename( @{$state->{args}} )1405{1406$filename= filecleanup($filename);14071408my($fh,$file1,$file2,$meta1,$meta2,$filediff);14091410my$wrev= revparse($filename);14111412# We need _something_ to diff against1413next unless(defined($wrev) );14141415# if we have a -r switch, use it1416if(defined($revision1) )1417{1418(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1419$meta1=$updater->getmeta($filename,$revision1);1420unless(defined($meta1)and$meta1->{filehash}ne"deleted")1421{1422print"E File$filenameat revision 1.$revision1doesn't exist\n";1423next;1424}1425 transmitfile($meta1->{filehash},$file1);1426}1427# otherwise we just use the working copy revision1428else1429{1430(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1431$meta1=$updater->getmeta($filename,$wrev);1432 transmitfile($meta1->{filehash},$file1);1433}14341435# if we have a second -r switch, use it too1436if(defined($revision2) )1437{1438(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1439$meta2=$updater->getmeta($filename,$revision2);14401441unless(defined($meta2)and$meta2->{filehash}ne"deleted")1442{1443print"E File$filenameat revision 1.$revision2doesn't exist\n";1444next;1445}14461447 transmitfile($meta2->{filehash},$file2);1448}1449# otherwise we just use the working copy1450else1451{1452$file2=$state->{entries}{$filename}{modified_filename};1453}14541455# if we have been given -r, and we don't have a $file2 yet, lets get one1456if(defined($revision1)and not defined($file2) )1457{1458(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1459$meta2=$updater->getmeta($filename,$wrev);1460 transmitfile($meta2->{filehash},$file2);1461}14621463# We need to have retrieved something useful1464next unless(defined($meta1) );14651466# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1467next if(not defined($meta2)and$wrev==$meta1->{revision}1468and1469( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1470or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1471);14721473# Apparently we only show diffs for locally modified files1474next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14751476print"M Index:$filename\n";1477print"M ===================================================================\n";1478print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1479print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1480print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1481print"M diff ";1482foreachmy$opt(keys%{$state->{opt}} )1483{1484if(ref$state->{opt}{$opt}eq"ARRAY")1485{1486foreachmy$value( @{$state->{opt}{$opt}} )1487{1488print"-$opt$value";1489}1490}else{1491print"-$opt";1492print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1493}1494}1495print"$filename\n";14961497$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));14981499($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15001501if(exists$state->{opt}{u} )1502{1503system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1504}else{1505system("diff$file1$file2>$filediff");1506}15071508while( <$fh> )1509{1510print"M$_";1511}1512close$fh;1513}15141515print"ok\n";1516}15171518sub req_log1519{1520my($cmd,$data) =@_;15211522 argsplit("log");15231524$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1525#$log->debug("log state : " . Dumper($state));15261527my($minrev,$maxrev);1528if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1529{1530my$control=$2;1531$minrev=$1;1532$maxrev=$3;1533$minrev=~s/^1\.//if(defined($minrev) );1534$maxrev=~s/^1\.//if(defined($maxrev) );1535$minrev++if(defined($minrev)and$controleq"::");1536}15371538# Grab a handle to the SQLite db and do any necessary updates1539my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1540$updater->update();15411542# if no files were specified, we need to work out what files we should be providing status on ...1543 argsfromdir($updater);15441545# foreach file specified on the command line ...1546foreachmy$filename( @{$state->{args}} )1547{1548$filename= filecleanup($filename);15491550my$headmeta=$updater->getmeta($filename);15511552my$revisions=$updater->getlog($filename);1553my$totalrevisions=scalar(@$revisions);15541555if(defined($minrev) )1556{1557$log->debug("Removing revisions less than$minrev");1558while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1559{1560pop@$revisions;1561}1562}1563if(defined($maxrev) )1564{1565$log->debug("Removing revisions greater than$maxrev");1566while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1567{1568shift@$revisions;1569}1570}15711572next unless(scalar(@$revisions) );15731574print"M\n";1575print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1576print"M Working file:$filename\n";1577print"M head: 1.$headmeta->{revision}\n";1578print"M branch:\n";1579print"M locks: strict\n";1580print"M access list:\n";1581print"M symbolic names:\n";1582print"M keyword substitution: kv\n";1583print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1584print"M description:\n";15851586foreachmy$revision(@$revisions)1587{1588print"M ----------------------------\n";1589print"M revision 1.$revision->{revision}\n";1590# reformat the date for log output1591$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}) );1592$revision->{author} =~s/\s+.*//;1593$revision->{author} =~s/^(.{8}).*/$1/;1594print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1595my$commitmessage=$updater->commitmessage($revision->{commithash});1596$commitmessage=~s/^/M /mg;1597print$commitmessage."\n";1598}1599print"M =============================================================================\n";1600}16011602print"ok\n";1603}16041605sub req_annotate1606{1607my($cmd,$data) =@_;16081609 argsplit("annotate");16101611$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1612#$log->debug("status state : " . Dumper($state));16131614# Grab a handle to the SQLite db and do any necessary updates1615my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1616$updater->update();16171618# if no files were specified, we need to work out what files we should be providing annotate on ...1619 argsfromdir($updater);16201621# we'll need a temporary checkout dir1622my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1623my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1624$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");16251626$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1627$ENV{GIT_INDEX_FILE} =$file_index;16281629chdir$tmpdir;16301631# foreach file specified on the command line ...1632foreachmy$filename( @{$state->{args}} )1633{1634$filename= filecleanup($filename);16351636my$meta=$updater->getmeta($filename);16371638next unless($meta->{revision} );16391640# get all the commits that this file was in1641# in dense format -- aka skip dead revisions1642my$revisions=$updater->gethistorydense($filename);1643my$lastseenin=$revisions->[0][2];16441645# populate the temporary index based on the latest commit were we saw1646# the file -- but do it cheaply without checking out any files1647# TODO: if we got a revision from the client, use that instead1648# to look up the commithash in sqlite (still good to default to1649# the current head as we do now)1650system("git-read-tree",$lastseenin);1651unless($?==0)1652{1653die"Error running git-read-tree$lastseenin$file_index$!";1654}1655$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16561657# do a checkout of the file1658system('git-checkout-index','-f','-u',$filename);1659unless($?==0) {1660die"Error running git-checkout-index -f -u$filename:$!";1661}16621663$log->info("Annotate$filename");16641665# Prepare a file with the commits from the linearized1666# history that annotate should know about. This prevents1667# git-jsannotate telling us about commits we are hiding1668# from the client.16691670open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1671for(my$i=0;$i<@$revisions;$i++)1672{1673print ANNOTATEHINTS $revisions->[$i][2];1674if($i+1<@$revisions) {# have we got a parent?1675print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1676}1677print ANNOTATEHINTS "\n";1678}16791680print ANNOTATEHINTS "\n";1681close ANNOTATEHINTS;16821683my$annotatecmd='git-annotate';1684open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1685or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1686my$metadata= {};1687print"E Annotations for$filename\n";1688print"E ***************\n";1689while( <ANNOTATE> )1690{1691if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1692{1693my$commithash=$1;1694my$data=$2;1695unless(defined($metadata->{$commithash} ) )1696{1697$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1698$metadata->{$commithash}{author} =~s/\s+.*//;1699$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1700$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1701}1702printf("M 1.%-5d (%-8s%10s):%s\n",1703$metadata->{$commithash}{revision},1704$metadata->{$commithash}{author},1705$metadata->{$commithash}{modified},1706$data1707);1708}else{1709$log->warn("Error in annotate output! LINE:$_");1710print"E Annotate error\n";1711next;1712}1713}1714close ANNOTATE;1715}17161717# done; get out of the tempdir1718chdir"/";17191720print"ok\n";17211722}17231724# This method takes the state->{arguments} array and produces two new arrays.1725# The first is $state->{args} which is everything before the '--' argument, and1726# the second is $state->{files} which is everything after it.1727sub argsplit1728{1729return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");17301731my$type=shift;17321733$state->{args} = [];1734$state->{files} = [];1735$state->{opt} = {};17361737if(defined($type) )1738{1739my$opt= {};1740$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");1741$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1742$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");1743$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1744$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1745$opt= { k =>1, m =>1}if($typeeq"add");1746$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1747$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");174817491750while(scalar( @{$state->{arguments}} ) >0)1751{1752my$arg=shift@{$state->{arguments}};17531754next if($argeq"--");1755next unless($arg=~/\S/);17561757# if the argument looks like a switch1758if($arg=~/^-(\w)(.*)/)1759{1760# if it's a switch that takes an argument1761if($opt->{$1} )1762{1763# If this switch has already been provided1764if($opt->{$1} >1and exists($state->{opt}{$1} ) )1765{1766$state->{opt}{$1} = [$state->{opt}{$1} ];1767if(length($2) >0)1768{1769push@{$state->{opt}{$1}},$2;1770}else{1771push@{$state->{opt}{$1}},shift@{$state->{arguments}};1772}1773}else{1774# if there's extra data in the arg, use that as the argument for the switch1775if(length($2) >0)1776{1777$state->{opt}{$1} =$2;1778}else{1779$state->{opt}{$1} =shift@{$state->{arguments}};1780}1781}1782}else{1783$state->{opt}{$1} =undef;1784}1785}1786else1787{1788push@{$state->{args}},$arg;1789}1790}1791}1792else1793{1794my$mode=0;17951796foreachmy$value( @{$state->{arguments}} )1797{1798if($valueeq"--")1799{1800$mode++;1801next;1802}1803push@{$state->{args}},$valueif($mode==0);1804push@{$state->{files}},$valueif($mode==1);1805}1806}1807}18081809# This method uses $state->{directory} to populate $state->{args} with a list of filenames1810sub argsfromdir1811{1812my$updater=shift;18131814$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");18151816return if(scalar( @{$state->{args}} ) >1);18171818my@gethead= @{$updater->gethead};18191820# push added files1821foreachmy$file(keys%{$state->{entries}}) {1822if(exists$state->{entries}{$file}{revision} &&1823$state->{entries}{$file}{revision} ==0)1824{1825push@gethead, { name =>$file, filehash =>'added'};1826}1827}18281829if(scalar(@{$state->{args}}) ==1)1830{1831my$arg=$state->{args}[0];1832$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );18331834$log->info("Only one arg specified, checking for directory expansion on '$arg'");18351836foreachmy$file(@gethead)1837{1838next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1839next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1840push@{$state->{args}},$file->{name};1841}18421843shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1844}else{1845$log->info("Only one arg specified, populating file list automatically");18461847$state->{args} = [];18481849foreachmy$file(@gethead)1850{1851next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1852next unless($file->{name} =~s/^$state->{prependdir}//);1853push@{$state->{args}},$file->{name};1854}1855}1856}18571858# This method cleans up the $state variable after a command that uses arguments has run1859sub statecleanup1860{1861$state->{files} = [];1862$state->{args} = [];1863$state->{arguments} = [];1864$state->{entries} = {};1865}18661867sub revparse1868{1869my$filename=shift;18701871returnundefunless(defined($state->{entries}{$filename}{revision} ) );18721873return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1874return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18751876returnundef;1877}18781879# This method takes a file hash and does a CVS "file transfer" which transmits the1880# size of the file, and then the file contents.1881# If a second argument $targetfile is given, the file is instead written out to1882# a file by the name of $targetfile1883sub transmitfile1884{1885my$filehash=shift;1886my$targetfile=shift;18871888if(defined($filehash)and$filehasheq"deleted")1889{1890$log->warn("filehash is 'deleted'");1891return;1892}18931894die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);18951896my$type=`git-cat-file -t$filehash`;1897 chomp$type;18981899 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );19001901 my$size= `git-cat-file -s $filehash`;1902chomp$size;19031904$log->debug("transmitfile($filehash) size=$size, type=$type");19051906if(open my$fh,'-|',"git-cat-file","blob",$filehash)1907{1908if(defined($targetfile) )1909{1910open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1911print NEWFILE $_while( <$fh> );1912close NEWFILE;1913}else{1914print"$size\n";1915printwhile( <$fh> );1916}1917close$fhor die("Couldn't close filehandle for transmitfile()");1918}else{1919die("Couldn't execute git-cat-file");1920}1921}19221923# This method takes a file name, and returns ( $dirpart, $filepart ) which1924# refers to the directory portion and the file portion of the filename1925# respectively1926sub filenamesplit1927{1928my$filename=shift;1929my$fixforlocaldir=shift;19301931my($filepart,$dirpart) = ($filename,".");1932($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1933$dirpart.="/";19341935if($fixforlocaldir)1936{1937$dirpart=~s/^$state->{prependdir}//;1938}19391940return($filepart,$dirpart);1941}19421943sub filecleanup1944{1945my$filename=shift;19461947returnundefunless(defined($filename));1948if($filename=~/^\// )1949{1950print"E absolute filenames '$filename' not supported by server\n";1951returnundef;1952}19531954$filename=~s/^\.\///g;1955$filename=$state->{prependdir} .$filename;1956return$filename;1957}19581959# Given a path, this function returns a string containing the kopts1960# that should go into that path's Entries line. For example, a binary1961# file should get -kb.1962sub kopts_from_path1963{1964my($path) =@_;19651966# Once it exists, the git attributes system should be used to look up1967# what attributes apply to this path.19681969# Until then, take the setting from the config file1970unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1971{1972# Return "" to give no special treatment to any path1973return"";1974}else{1975# Alternatively, to have all files treated as if they are binary (which1976# is more like git itself), always return the "-kb" option1977return"-kb";1978}1979}19801981package GITCVS::log;19821983####1984#### Copyright The Open University UK - 2006.1985####1986#### Authors: Martyn Smith <martyn@catalyst.net.nz>1987#### Martin Langhoff <martin@catalyst.net.nz>1988####1989####19901991use strict;1992use warnings;19931994=head1 NAME19951996GITCVS::log19971998=head1 DESCRIPTION19992000This module provides very crude logging with a similar interface to2001Log::Log4perl20022003=head1 METHODS20042005=cut20062007=head2 new20082009Creates a new log object, optionally you can specify a filename here to2010indicate the file to log to. If no log file is specified, you can specify one2011later with method setfile, or indicate you no longer want logging with method2012nofile.20132014Until one of these methods is called, all log calls will buffer messages ready2015to write out.20162017=cut2018sub new2019{2020my$class=shift;2021my$filename=shift;20222023my$self= {};20242025bless$self,$class;20262027if(defined($filename) )2028{2029open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2030}20312032return$self;2033}20342035=head2 setfile20362037This methods takes a filename, and attempts to open that file as the log file.2038If successful, all buffered data is written out to the file, and any further2039logging is written directly to the file.20402041=cut2042sub setfile2043{2044my$self=shift;2045my$filename=shift;20462047if(defined($filename) )2048{2049open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2050}20512052return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20532054while(my$line=shift@{$self->{buffer}} )2055{2056print{$self->{fh}}$line;2057}2058}20592060=head2 nofile20612062This method indicates no logging is going to be used. It flushes any entries in2063the internal buffer, and sets a flag to ensure no further data is put there.20642065=cut2066sub nofile2067{2068my$self=shift;20692070$self->{nolog} =1;20712072return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20732074$self->{buffer} = [];2075}20762077=head2 _logopen20782079Internal method. Returns true if the log file is open, false otherwise.20802081=cut2082sub _logopen2083{2084my$self=shift;20852086return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2087return0;2088}20892090=head2 debug info warn fatal20912092These four methods are wrappers to _log. They provide the actual interface for2093logging data.20942095=cut2096sub debug {my$self=shift;$self->_log("debug",@_); }2097sub info {my$self=shift;$self->_log("info",@_); }2098subwarn{my$self=shift;$self->_log("warn",@_); }2099sub fatal {my$self=shift;$self->_log("fatal",@_); }21002101=head2 _log21022103This is an internal method called by the logging functions. It generates a2104timestamp and pushes the logged line either to file, or internal buffer.21052106=cut2107sub _log2108{2109my$self=shift;2110my$level=shift;21112112return if($self->{nolog} );21132114my@time=localtime;2115my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2116$time[5] +1900,2117$time[4] +1,2118$time[3],2119$time[2],2120$time[1],2121$time[0],2122uc$level,2123);21242125if($self->_logopen)2126{2127print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2128}else{2129push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2130}2131}21322133=head2 DESTROY21342135This method simply closes the file handle if one is open21362137=cut2138sub DESTROY2139{2140my$self=shift;21412142if($self->_logopen)2143{2144close$self->{fh};2145}2146}21472148package GITCVS::updater;21492150####2151#### Copyright The Open University UK - 2006.2152####2153#### Authors: Martyn Smith <martyn@catalyst.net.nz>2154#### Martin Langhoff <martin@catalyst.net.nz>2155####2156####21572158use strict;2159use warnings;2160use DBI;21612162=head1 METHODS21632164=cut21652166=head2 new21672168=cut2169sub new2170{2171my$class=shift;2172my$config=shift;2173my$module=shift;2174my$log=shift;21752176die"Need to specify a git repository"unless(defined($config)and-d $config);2177die"Need to specify a module"unless(defined($module) );21782179$class=ref($class) ||$class;21802181my$self= {};21822183bless$self,$class;21842185$self->{module} =$module;2186$self->{git_path} =$config."/";21872188$self->{log} =$log;21892190die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );21912192$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2193$cfg->{gitcvs}{dbdriver} ||"SQLite";2194$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2195$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2196$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2197$cfg->{gitcvs}{dbuser} ||"";2198$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2199$cfg->{gitcvs}{dbpass} ||"";2200my%mapping= ( m =>$module,2201 a =>$state->{method},2202 u =>getlogin||getpwuid($<) || $<,2203 G =>$self->{git_path},2204 g => mangle_dirname($self->{git_path}),2205);2206$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2207$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;22082209die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2210die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2211$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2212$self->{dbuser},2213$self->{dbpass});2214die"Error connecting to database\n"unlessdefined$self->{dbh};22152216$self->{tables} = {};2217foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2218{2219$self->{tables}{$table} =1;2220}22212222# Construct the revision table if required2223unless($self->{tables}{revision} )2224{2225$self->{dbh}->do("2226 CREATE TABLE revision (2227 name TEXT NOT NULL,2228 revision INTEGER NOT NULL,2229 filehash TEXT NOT NULL,2230 commithash TEXT NOT NULL,2231 author TEXT NOT NULL,2232 modified TEXT NOT NULL,2233 mode TEXT NOT NULL2234 )2235 ");2236$self->{dbh}->do("2237 CREATE INDEX revision_ix12238 ON revision (name,revision)2239 ");2240$self->{dbh}->do("2241 CREATE INDEX revision_ix22242 ON revision (name,commithash)2243 ");2244}22452246# Construct the head table if required2247unless($self->{tables}{head} )2248{2249$self->{dbh}->do("2250 CREATE TABLE head (2251 name TEXT NOT NULL,2252 revision INTEGER NOT NULL,2253 filehash TEXT NOT NULL,2254 commithash TEXT NOT NULL,2255 author TEXT NOT NULL,2256 modified TEXT NOT NULL,2257 mode TEXT NOT NULL2258 )2259 ");2260$self->{dbh}->do("2261 CREATE INDEX head_ix12262 ON head (name)2263 ");2264}22652266# Construct the properties table if required2267unless($self->{tables}{properties} )2268{2269$self->{dbh}->do("2270 CREATE TABLE properties (2271 key TEXT NOT NULL PRIMARY KEY,2272 value TEXT2273 )2274 ");2275}22762277# Construct the commitmsgs table if required2278unless($self->{tables}{commitmsgs} )2279{2280$self->{dbh}->do("2281 CREATE TABLE commitmsgs (2282 key TEXT NOT NULL PRIMARY KEY,2283 value TEXT2284 )2285 ");2286}22872288return$self;2289}22902291=head2 update22922293=cut2294sub update2295{2296my$self=shift;22972298# first lets get the commit list2299$ENV{GIT_DIR} =$self->{git_path};23002301my$commitsha1=`git rev-parse$self->{module}`;2302chomp$commitsha1;23032304my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2305unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2306{2307die("Invalid module '$self->{module}'");2308}230923102311my$git_log;2312my$lastcommit=$self->_get_prop("last_commit");23132314if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2315return1;2316}23172318# Start exclusive lock here...2319$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";23202321# TODO: log processing is memory bound2322# if we can parse into a 2nd file that is in reverse order2323# we can probably do something really efficient2324my@git_log_params= ('--pretty','--parents','--topo-order');23252326if(defined$lastcommit) {2327push@git_log_params,"$lastcommit..$self->{module}";2328}else{2329push@git_log_params,$self->{module};2330}2331# git-rev-list is the backend / plumbing version of git-log2332open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";23332334my@commits;23352336my%commit= ();23372338while( <GITLOG> )2339{2340chomp;2341if(m/^commit\s+(.*)$/) {2342# on ^commit lines put the just seen commit in the stack2343# and prime things for the next one2344if(keys%commit) {2345my%copy=%commit;2346unshift@commits, \%copy;2347%commit= ();2348}2349my@parents=split(m/\s+/,$1);2350$commit{hash} =shift@parents;2351$commit{parents} = \@parents;2352}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2353# on rfc822-like lines seen before we see any message,2354# lowercase the entry and put it in the hash as key-value2355$commit{lc($1)} =$2;2356}else{2357# message lines - skip initial empty line2358# and trim whitespace2359if(!exists($commit{message}) &&m/^\s*$/) {2360# define it to mark the end of headers2361$commit{message} ='';2362next;2363}2364s/^\s+//;s/\s+$//;# trim ws2365$commit{message} .=$_."\n";2366}2367}2368close GITLOG;23692370unshift@commits, \%commitif(keys%commit);23712372# Now all the commits are in the @commits bucket2373# ordered by time DESC. for each commit that needs processing,2374# determine whether it's following the last head we've seen or if2375# it's on its own branch, grab a file list, and add whatever's changed2376# NOTE: $lastcommit refers to the last commit from previous run2377# $lastpicked is the last commit we picked in this run2378my$lastpicked;2379my$head= {};2380if(defined$lastcommit) {2381$lastpicked=$lastcommit;2382}23832384my$committotal=scalar(@commits);2385my$commitcount=0;23862387# Load the head table into $head (for cached lookups during the update process)2388foreachmy$file( @{$self->gethead()} )2389{2390$head->{$file->{name}} =$file;2391}23922393foreachmy$commit(@commits)2394{2395$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2396if(defined$lastpicked)2397{2398if(!in_array($lastpicked, @{$commit->{parents}}))2399{2400# skip, we'll see this delta2401# as part of a merge later2402# warn "skipping off-track $commit->{hash}\n";2403next;2404}elsif(@{$commit->{parents}} >1) {2405# it is a merge commit, for each parent that is2406# not $lastpicked, see if we can get a log2407# from the merge-base to that parent to put it2408# in the message as a merge summary.2409my@parents= @{$commit->{parents}};2410foreachmy$parent(@parents) {2411# git-merge-base can potentially (but rarely) throw2412# several candidate merge bases. let's assume2413# that the first one is the best one.2414if($parenteq$lastpicked) {2415next;2416}2417open my$p,'git-merge-base '.$lastpicked.' '2418.$parent.'|';2419my@output= (<$p>);2420close$p;2421my$base=join('',@output);2422chomp$base;2423if($base) {2424my@merged;2425# print "want to log between $base $parent \n";2426open(GITLOG,'-|','git-log',"$base..$parent")2427or die"Cannot call git-log:$!";2428my$mergedhash;2429while(<GITLOG>) {2430chomp;2431if(!defined$mergedhash) {2432if(m/^commit\s+(.+)$/) {2433$mergedhash=$1;2434}else{2435next;2436}2437}else{2438# grab the first line that looks non-rfc8222439# aka has content after leading space2440if(m/^\s+(\S.*)$/) {2441my$title=$1;2442$title=substr($title,0,100);# truncate2443unshift@merged,"$mergedhash$title";2444undef$mergedhash;2445}2446}2447}2448close GITLOG;2449if(@merged) {2450$commit->{mergemsg} =$commit->{message};2451$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2452foreachmy$summary(@merged) {2453$commit->{mergemsg} .="\t$summary\n";2454}2455$commit->{mergemsg} .="\n\n";2456# print "Message for $commit->{hash} \n$commit->{mergemsg}";2457}2458}2459}2460}2461}24622463# convert the date to CVS-happy format2464$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24652466if(defined($lastpicked) )2467{2468my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2469local($/) ="\0";2470while( <FILELIST> )2471{2472chomp;2473unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2474{2475die("Couldn't process git-diff-tree line :$_");2476}2477my($mode,$hash,$change) = ($1,$2,$3);2478my$name= <FILELIST>;2479chomp($name);24802481# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24822483my$git_perms="";2484$git_perms.="r"if($mode&4);2485$git_perms.="w"if($mode&2);2486$git_perms.="x"if($mode&1);2487$git_perms="rw"if($git_permseq"");24882489if($changeeq"D")2490{2491#$log->debug("DELETE $name");2492$head->{$name} = {2493 name =>$name,2494 revision =>$head->{$name}{revision} +1,2495 filehash =>"deleted",2496 commithash =>$commit->{hash},2497 modified =>$commit->{date},2498 author =>$commit->{author},2499 mode =>$git_perms,2500};2501$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2502}2503elsif($changeeq"M")2504{2505#$log->debug("MODIFIED $name");2506$head->{$name} = {2507 name =>$name,2508 revision =>$head->{$name}{revision} +1,2509 filehash =>$hash,2510 commithash =>$commit->{hash},2511 modified =>$commit->{date},2512 author =>$commit->{author},2513 mode =>$git_perms,2514};2515$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2516}2517elsif($changeeq"A")2518{2519#$log->debug("ADDED $name");2520$head->{$name} = {2521 name =>$name,2522 revision =>1,2523 filehash =>$hash,2524 commithash =>$commit->{hash},2525 modified =>$commit->{date},2526 author =>$commit->{author},2527 mode =>$git_perms,2528};2529$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2530}2531else2532{2533$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2534die;2535}2536}2537close FILELIST;2538}else{2539# this is used to detect files removed from the repo2540my$seen_files= {};25412542my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2543local$/="\0";2544while( <FILELIST> )2545{2546chomp;2547unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2548{2549die("Couldn't process git-ls-tree line :$_");2550}25512552my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);25532554$seen_files->{$git_filename} =1;25552556my($oldhash,$oldrevision,$oldmode) = (2557$head->{$git_filename}{filehash},2558$head->{$git_filename}{revision},2559$head->{$git_filename}{mode}2560);25612562if($git_perms=~/^\d\d\d(\d)\d\d/o)2563{2564$git_perms="";2565$git_perms.="r"if($1&4);2566$git_perms.="w"if($1&2);2567$git_perms.="x"if($1&1);2568}else{2569$git_perms="rw";2570}25712572# unless the file exists with the same hash, we need to update it ...2573unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2574{2575my$newrevision= ($oldrevisionor0) +1;25762577$head->{$git_filename} = {2578 name =>$git_filename,2579 revision =>$newrevision,2580 filehash =>$git_hash,2581 commithash =>$commit->{hash},2582 modified =>$commit->{date},2583 author =>$commit->{author},2584 mode =>$git_perms,2585};258625872588$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2589}2590}2591close FILELIST;25922593# Detect deleted files2594foreachmy$file(keys%$head)2595{2596unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2597{2598$head->{$file}{revision}++;2599$head->{$file}{filehash} ="deleted";2600$head->{$file}{commithash} =$commit->{hash};2601$head->{$file}{modified} =$commit->{date};2602$head->{$file}{author} =$commit->{author};26032604$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2605}2606}2607# END : "Detect deleted files"2608}260926102611if(exists$commit->{mergemsg})2612{2613$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2614}26152616$lastpicked=$commit->{hash};26172618$self->_set_prop("last_commit",$commit->{hash});2619}26202621$self->delete_head();2622foreachmy$file(keys%$head)2623{2624$self->insert_head(2625$file,2626$head->{$file}{revision},2627$head->{$file}{filehash},2628$head->{$file}{commithash},2629$head->{$file}{modified},2630$head->{$file}{author},2631$head->{$file}{mode},2632);2633}2634# invalidate the gethead cache2635$self->{gethead_cache} =undef;263626372638# Ending exclusive lock here2639$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2640}26412642sub insert_rev2643{2644my$self=shift;2645my$name=shift;2646my$revision=shift;2647my$filehash=shift;2648my$commithash=shift;2649my$modified=shift;2650my$author=shift;2651my$mode=shift;26522653my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2654$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2655}26562657sub insert_mergelog2658{2659my$self=shift;2660my$key=shift;2661my$value=shift;26622663my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2664$insert_mergelog->execute($key,$value);2665}26662667sub delete_head2668{2669my$self=shift;26702671my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2672$delete_head->execute();2673}26742675sub insert_head2676{2677my$self=shift;2678my$name=shift;2679my$revision=shift;2680my$filehash=shift;2681my$commithash=shift;2682my$modified=shift;2683my$author=shift;2684my$mode=shift;26852686my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2687$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2688}26892690sub _headrev2691{2692my$self=shift;2693my$filename=shift;26942695my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2696$db_query->execute($filename);2697my($hash,$revision,$mode) =$db_query->fetchrow_array;26982699return($hash,$revision,$mode);2700}27012702sub _get_prop2703{2704my$self=shift;2705my$key=shift;27062707my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2708$db_query->execute($key);2709my($value) =$db_query->fetchrow_array;27102711return$value;2712}27132714sub _set_prop2715{2716my$self=shift;2717my$key=shift;2718my$value=shift;27192720my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2721$db_query->execute($value,$key);27222723unless($db_query->rows)2724{2725$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2726$db_query->execute($key,$value);2727}27282729return$value;2730}27312732=head2 gethead27332734=cut27352736sub gethead2737{2738my$self=shift;27392740return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );27412742my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2743$db_query->execute();27442745my$tree= [];2746while(my$file=$db_query->fetchrow_hashref)2747{2748push@$tree,$file;2749}27502751$self->{gethead_cache} =$tree;27522753return$tree;2754}27552756=head2 getlog27572758=cut27592760sub getlog2761{2762my$self=shift;2763my$filename=shift;27642765my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2766$db_query->execute($filename);27672768my$tree= [];2769while(my$file=$db_query->fetchrow_hashref)2770{2771push@$tree,$file;2772}27732774return$tree;2775}27762777=head2 getmeta27782779This function takes a filename (with path) argument and returns a hashref of2780metadata for that file.27812782=cut27832784sub getmeta2785{2786my$self=shift;2787my$filename=shift;2788my$revision=shift;27892790my$db_query;2791if(defined($revision)and$revision=~/^\d+$/)2792{2793$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2794$db_query->execute($filename,$revision);2795}2796elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2797{2798$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2799$db_query->execute($filename,$revision);2800}else{2801$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2802$db_query->execute($filename);2803}28042805return$db_query->fetchrow_hashref;2806}28072808=head2 commitmessage28092810this function takes a commithash and returns the commit message for that commit28112812=cut2813sub commitmessage2814{2815my$self=shift;2816my$commithash=shift;28172818die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);28192820my$db_query;2821$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2822$db_query->execute($commithash);28232824my($message) =$db_query->fetchrow_array;28252826if(defined($message) )2827{2828$message.=" "if($message=~/\n$/);2829return$message;2830}28312832my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2833shift@lineswhile($lines[0] =~/\S/);2834$message=join("",@lines);2835$message.=" "if($message=~/\n$/);2836return$message;2837}28382839=head2 gethistory28402841This function takes a filename (with path) argument and returns an arrayofarrays2842containing revision,filehash,commithash ordered by revision descending28432844=cut2845sub gethistory2846{2847my$self=shift;2848my$filename=shift;28492850my$db_query;2851$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2852$db_query->execute($filename);28532854return$db_query->fetchall_arrayref;2855}28562857=head2 gethistorydense28582859This function takes a filename (with path) argument and returns an arrayofarrays2860containing revision,filehash,commithash ordered by revision descending.28612862This version of gethistory skips deleted entries -- so it is useful for annotate.2863The 'dense' part is a reference to a '--dense' option available for git-rev-list2864and other git tools that depend on it.28652866=cut2867sub gethistorydense2868{2869my$self=shift;2870my$filename=shift;28712872my$db_query;2873$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2874$db_query->execute($filename);28752876return$db_query->fetchall_arrayref;2877}28782879=head2 in_array()28802881from Array::PAT - mimics the in_array() function2882found in PHP. Yuck but works for small arrays.28832884=cut2885sub in_array2886{2887my($check,@array) =@_;2888my$retval=0;2889foreachmy$test(@array){2890if($checkeq$test){2891$retval=1;2892}2893}2894return$retval;2895}28962897=head2 safe_pipe_capture28982899an alternative to `command` that allows input to be passed as an array2900to work around shell problems with weird characters in arguments29012902=cut2903sub safe_pipe_capture {29042905my@output;29062907if(my$pid=open my$child,'-|') {2908@output= (<$child>);2909close$childor die join(' ',@_).":$!$?";2910}else{2911exec(@_)or die"$!$?";# exec() can fail the executable can't be found2912}2913returnwantarray?@output:join('',@output);2914}29152916=head2 mangle_dirname29172918create a string from a directory name that is suitable to use as2919part of a filename, mainly by converting all chars except \w.- to _29202921=cut2922sub mangle_dirname {2923my$dirname=shift;2924return unlessdefined$dirname;29252926$dirname=~s/[^\w.-]/_/g;29272928return$dirname;2929}293029311;