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($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 99die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 100} 101my$request=$1; 102$line= <STDIN>;chomp$line; 103 req_Root('root',$line)# reuse Root 104or die"E Invalid root$line\n"; 105$line= <STDIN>;chomp$line; 106unless($lineeq'anonymous') { 107print"E Only anonymous user allowed via pserver\n"; 108print"I HATE YOU\n"; 109exit1; 110} 111$line= <STDIN>;chomp$line;# validate the password? 112$line= <STDIN>;chomp$line; 113unless($lineeq"END$requestREQUEST") { 114die"E Do not understand$line-- expecting END$requestREQUEST\n"; 115} 116print"I LOVE YOU\n"; 117exit if$requesteq'VERIFICATION';# cvs login 118# and now back to our regular programme... 119} 120 121# Keep going until the client closes the connection 122while(<STDIN>) 123{ 124chomp; 125 126# Check to see if we've seen this method, and call appropriate function. 127if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 128{ 129# use the $methods hash to call the appropriate sub for this command 130#$log->info("Method : $1"); 131&{$methods->{$1}}($1,$2); 132}else{ 133# log fatal because we don't understand this function. If this happens 134# we're fairly screwed because we don't know if the client is expecting 135# a response. If it is, the client will hang, we'll hang, and the whole 136# thing will be custard. 137$log->fatal("Don't understand command$_\n"); 138die("Unknown command$_"); 139} 140} 141 142$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 143$log->info("--------------- FINISH -----------------"); 144 145# Magic catchall method. 146# This is the method that will handle all commands we haven't yet 147# implemented. It simply sends a warning to the log file indicating a 148# command that hasn't been implemented has been invoked. 149sub req_CATCHALL 150{ 151my($cmd,$data) =@_; 152$log->warn("Unhandled command : req_$cmd:$data"); 153} 154 155 156# Root pathname \n 157# Response expected: no. Tell the server which CVSROOT to use. Note that 158# pathname is a local directory and not a fully qualified CVSROOT variable. 159# pathname must already exist; if creating a new root, use the init 160# request, not Root. pathname does not include the hostname of the server, 161# how to access the server, etc.; by the time the CVS protocol is in use, 162# connection, authentication, etc., are already taken care of. The Root 163# request must be sent only once, and it must be sent before any requests 164# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 165sub req_Root 166{ 167my($cmd,$data) =@_; 168$log->debug("req_Root :$data"); 169 170unless($data=~ m#^/#) { 171print"error 1 Root must be an absolute pathname\n"; 172return0; 173} 174 175if($state->{CVSROOT} 176&& ($state->{CVSROOT}ne$data)) { 177print"error 1 Conflicting roots specified\n"; 178return0; 179} 180 181$state->{CVSROOT} =$data; 182 183$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 184unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 185print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 186print"E\n"; 187print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 188return0; 189} 190 191my@gitvars=`git-config -l`; 192if($?) { 193print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 194print"E\n"; 195print"error 1 - problem executing git-config\n"; 196return0; 197} 198foreachmy$line(@gitvars) 199{ 200next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 201unless($2) { 202$cfg->{$1}{$3} =$4; 203}else{ 204$cfg->{$1}{$2}{$3} =$4; 205} 206} 207 208my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 209||$cfg->{gitcvs}{enabled}); 210unless($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i) { 211print"E GITCVS emulation needs to be enabled on this repo\n"; 212print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 213print"E\n"; 214print"error 1 GITCVS emulation disabled\n"; 215return0; 216} 217 218my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 219if($logfile) 220{ 221$log->setfile($logfile); 222}else{ 223$log->nofile(); 224} 225 226return1; 227} 228 229# Global_option option \n 230# Response expected: no. Transmit one of the global options `-q', `-Q', 231# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 232# variations (such as combining of options) are allowed. For graceful 233# handling of valid-requests, it is probably better to make new global 234# options separate requests, rather than trying to add them to this 235# request. 236sub req_Globaloption 237{ 238my($cmd,$data) =@_; 239$log->debug("req_Globaloption :$data"); 240$state->{globaloptions}{$data} =1; 241} 242 243# Valid-responses request-list \n 244# Response expected: no. Tell the server what responses the client will 245# accept. request-list is a space separated list of tokens. 246sub req_Validresponses 247{ 248my($cmd,$data) =@_; 249$log->debug("req_Validresponses :$data"); 250 251# TODO : re-enable this, currently it's not particularly useful 252#$state->{validresponses} = [ split /\s+/, $data ]; 253} 254 255# valid-requests \n 256# Response expected: yes. Ask the server to send back a Valid-requests 257# response. 258sub req_validrequests 259{ 260my($cmd,$data) =@_; 261 262$log->debug("req_validrequests"); 263 264$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 265$log->debug("SEND : ok"); 266 267print"Valid-requests ".join(" ",keys%$methods) ."\n"; 268print"ok\n"; 269} 270 271# Directory local-directory \n 272# Additional data: repository \n. Response expected: no. Tell the server 273# what directory to use. The repository should be a directory name from a 274# previous server response. Note that this both gives a default for Entry 275# and Modified and also for ci and the other commands; normal usage is to 276# send Directory for each directory in which there will be an Entry or 277# Modified, and then a final Directory for the original directory, then the 278# command. The local-directory is relative to the top level at which the 279# command is occurring (i.e. the last Directory which is sent before the 280# command); to indicate that top level, `.' should be sent for 281# local-directory. 282sub req_Directory 283{ 284my($cmd,$data) =@_; 285 286my$repository= <STDIN>; 287chomp$repository; 288 289 290$state->{localdir} =$data; 291$state->{repository} =$repository; 292$state->{path} =$repository; 293$state->{path} =~s/^$state->{CVSROOT}\///; 294$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 295$state->{path} .="/"if($state->{path} =~ /\S/ ); 296 297$state->{directory} =$state->{localdir}; 298$state->{directory} =""if($state->{directory}eq"."); 299$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 300 301if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 302{ 303$log->info("Setting prepend to '$state->{path}'"); 304$state->{prependdir} =$state->{path}; 305foreachmy$entry(keys%{$state->{entries}} ) 306{ 307$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 308delete$state->{entries}{$entry}; 309} 310} 311 312if(defined($state->{prependdir} ) ) 313{ 314$log->debug("Prepending '$state->{prependdir}' to state|directory"); 315$state->{directory} =$state->{prependdir} .$state->{directory} 316} 317$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 318} 319 320# Entry entry-line \n 321# Response expected: no. Tell the server what version of a file is on the 322# local machine. The name in entry-line is a name relative to the directory 323# most recently specified with Directory. If the user is operating on only 324# some files in a directory, Entry requests for only those files need be 325# included. If an Entry request is sent without Modified, Is-modified, or 326# Unchanged, it means the file is lost (does not exist in the working 327# directory). If both Entry and one of Modified, Is-modified, or Unchanged 328# are sent for the same file, Entry must be sent first. For a given file, 329# one can send Modified, Is-modified, or Unchanged, but not more than one 330# of these three. 331sub req_Entry 332{ 333my($cmd,$data) =@_; 334 335#$log->debug("req_Entry : $data"); 336 337my@data=split(/\//,$data); 338 339$state->{entries}{$state->{directory}.$data[1]} = { 340 revision =>$data[2], 341 conflict =>$data[3], 342 options =>$data[4], 343 tag_or_date =>$data[5], 344}; 345 346$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 347} 348 349# Questionable filename \n 350# Response expected: no. Additional data: no. Tell the server to check 351# whether filename should be ignored, and if not, next time the server 352# sends responses, send (in a M response) `?' followed by the directory and 353# filename. filename must not contain `/'; it needs to be a file in the 354# directory named by the most recent Directory request. 355sub req_Questionable 356{ 357my($cmd,$data) =@_; 358 359$log->debug("req_Questionable :$data"); 360$state->{entries}{$state->{directory}.$data}{questionable} =1; 361} 362 363# add \n 364# Response expected: yes. Add a file or directory. This uses any previous 365# Argument, Directory, Entry, or Modified requests, if they have been sent. 366# The last Directory sent specifies the working directory at the time of 367# the operation. To add a directory, send the directory to be added using 368# Directory and Argument requests. 369sub req_add 370{ 371my($cmd,$data) =@_; 372 373 argsplit("add"); 374 375my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 376$updater->update(); 377 378 argsfromdir($updater); 379 380my$addcount=0; 381 382foreachmy$filename( @{$state->{args}} ) 383{ 384$filename= filecleanup($filename); 385 386my$meta=$updater->getmeta($filename); 387my$wrev= revparse($filename); 388 389if($wrev&&$meta&& ($wrev<0)) 390{ 391# previously removed file, add back 392$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 393 394print"MT +updated\n"; 395print"MT text U\n"; 396print"MT fname$filename\n"; 397print"MT newline\n"; 398print"MT -updated\n"; 399 400unless($state->{globaloptions}{-n} ) 401{ 402my($filepart,$dirpart) = filenamesplit($filename,1); 403 404print"Created$dirpart\n"; 405print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 406 407# this is an "entries" line 408my$kopts= kopts_from_path($filepart); 409$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 410print"/$filepart/1.$meta->{revision}//$kopts/\n"; 411# permissions 412$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 413print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 414# transmit file 415 transmitfile($meta->{filehash}); 416} 417 418next; 419} 420 421unless(defined($state->{entries}{$filename}{modified_filename} ) ) 422{ 423print"E cvs add: nothing known about `$filename'\n"; 424next; 425} 426# TODO : check we're not squashing an already existing file 427if(defined($state->{entries}{$filename}{revision} ) ) 428{ 429print"E cvs add: `$filename' has already been entered\n"; 430next; 431} 432 433my($filepart,$dirpart) = filenamesplit($filename,1); 434 435print"E cvs add: scheduling file `$filename' for addition\n"; 436 437print"Checked-in$dirpart\n"; 438print"$filename\n"; 439my$kopts= kopts_from_path($filepart); 440print"/$filepart/0//$kopts/\n"; 441 442$addcount++; 443} 444 445if($addcount==1) 446{ 447print"E cvs add: use `cvs commit' to add this file permanently\n"; 448} 449elsif($addcount>1) 450{ 451print"E cvs add: use `cvs commit' to add these files permanently\n"; 452} 453 454print"ok\n"; 455} 456 457# remove \n 458# Response expected: yes. Remove a file. This uses any previous Argument, 459# Directory, Entry, or Modified requests, if they have been sent. The last 460# Directory sent specifies the working directory at the time of the 461# operation. Note that this request does not actually do anything to the 462# repository; the only effect of a successful remove request is to supply 463# the client with a new entries line containing `-' to indicate a removed 464# file. In fact, the client probably could perform this operation without 465# contacting the server, although using remove may cause the server to 466# perform a few more checks. The client sends a subsequent ci request to 467# actually record the removal in the repository. 468sub req_remove 469{ 470my($cmd,$data) =@_; 471 472 argsplit("remove"); 473 474# Grab a handle to the SQLite db and do any necessary updates 475my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 476$updater->update(); 477 478#$log->debug("add state : " . Dumper($state)); 479 480my$rmcount=0; 481 482foreachmy$filename( @{$state->{args}} ) 483{ 484$filename= filecleanup($filename); 485 486if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 487{ 488print"E cvs remove: file `$filename' still in working directory\n"; 489next; 490} 491 492my$meta=$updater->getmeta($filename); 493my$wrev= revparse($filename); 494 495unless(defined($wrev) ) 496{ 497print"E cvs remove: nothing known about `$filename'\n"; 498next; 499} 500 501if(defined($wrev)and$wrev<0) 502{ 503print"E cvs remove: file `$filename' already scheduled for removal\n"; 504next; 505} 506 507unless($wrev==$meta->{revision} ) 508{ 509# TODO : not sure if the format of this message is quite correct. 510print"E cvs remove: Up to date check failed for `$filename'\n"; 511next; 512} 513 514 515my($filepart,$dirpart) = filenamesplit($filename,1); 516 517print"E cvs remove: scheduling `$filename' for removal\n"; 518 519print"Checked-in$dirpart\n"; 520print"$filename\n"; 521my$kopts= kopts_from_path($filepart); 522print"/$filepart/-1.$wrev//$kopts/\n"; 523 524$rmcount++; 525} 526 527if($rmcount==1) 528{ 529print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 530} 531elsif($rmcount>1) 532{ 533print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 534} 535 536print"ok\n"; 537} 538 539# Modified filename \n 540# Response expected: no. Additional data: mode, \n, file transmission. Send 541# the server a copy of one locally modified file. filename is a file within 542# the most recent directory sent with Directory; it must not contain `/'. 543# If the user is operating on only some files in a directory, only those 544# files need to be included. This can also be sent without Entry, if there 545# is no entry for the file. 546sub req_Modified 547{ 548my($cmd,$data) =@_; 549 550my$mode= <STDIN>; 551chomp$mode; 552my$size= <STDIN>; 553chomp$size; 554 555# Grab config information 556my$blocksize=8192; 557my$bytesleft=$size; 558my$tmp; 559 560# Get a filehandle/name to write it to 561my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 562 563# Loop over file data writing out to temporary file. 564while($bytesleft) 565{ 566$blocksize=$bytesleftif($bytesleft<$blocksize); 567read STDIN,$tmp,$blocksize; 568print$fh $tmp; 569$bytesleft-=$blocksize; 570} 571 572close$fh; 573 574# Ensure we have something sensible for the file mode 575if($mode=~/u=(\w+)/) 576{ 577$mode=$1; 578}else{ 579$mode="rw"; 580} 581 582# Save the file data in $state 583$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 584$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 585$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 586$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 587 588 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 589} 590 591# Unchanged filename\n 592# Response expected: no. Tell the server that filename has not been 593# modified in the checked out directory. The filename is a file within the 594# most recent directory sent with Directory; it must not contain `/'. 595sub req_Unchanged 596{ 597 my ($cmd,$data) =@_; 598 599$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 600 601 #$log->debug("req_Unchanged :$data"); 602} 603 604# Argument text\n 605# Response expected: no. Save argument for use in a subsequent command. 606# Arguments accumulate until an argument-using command is given, at which 607# point they are forgotten. 608# Argumentx text\n 609# Response expected: no. Append\nfollowed by text to the current argument 610# being saved. 611sub req_Argument 612{ 613 my ($cmd,$data) =@_; 614 615 # Argumentx means: append to last Argument (with a newline in front) 616 617$log->debug("$cmd:$data"); 618 619 if ($cmdeq 'Argumentx') { 620 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 621 } else { 622 push @{$state->{arguments}},$data; 623 } 624} 625 626# expand-modules\n 627# Response expected: yes. Expand the modules which are specified in the 628# arguments. Returns the data in Module-expansion responses. Note that the 629# server can assume that this is checkout or export, not rtag or rdiff; the 630# latter do not access the working directory and thus have no need to 631# expand modules on the client side. Expand may not be the best word for 632# what this request does. It does not necessarily tell you all the files 633# contained in a module, for example. Basically it is a way of telling you 634# which working directories the server needs to know about in order to 635# handle a checkout of the specified modules. For example, suppose that the 636# server has a module defined by 637# aliasmodule -a 1dir 638# That is, one can check out aliasmodule and it will take 1dir in the 639# repository and check it out to 1dir in the working directory. Now suppose 640# the client already has this module checked out and is planning on using 641# the co request to update it. Without using expand-modules, the client 642# would have two bad choices: it could either send information about all 643# working directories under the current directory, which could be 644# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 645# stands for 1dir, and neglect to send information for 1dir, which would 646# lead to incorrect operation. With expand-modules, the client would first 647# ask for the module to be expanded: 648sub req_expandmodules 649{ 650 my ($cmd,$data) =@_; 651 652 argsplit(); 653 654$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 655 656 unless ( ref$state->{arguments} eq "ARRAY" ) 657 { 658 print "ok\n"; 659 return; 660 } 661 662 foreach my$module( @{$state->{arguments}} ) 663 { 664$log->debug("SEND : Module-expansion$module"); 665 print "Module-expansion$module\n"; 666 } 667 668 print "ok\n"; 669 statecleanup(); 670} 671 672# co\n 673# Response expected: yes. Get files from the repository. This uses any 674# previous Argument, Directory, Entry, or Modified requests, if they have 675# been sent. Arguments to this command are module names; the client cannot 676# know what directories they correspond to except by (1) just sending the 677# co request, and then seeing what directory names the server sends back in 678# its responses, and (2) the expand-modules request. 679sub req_co 680{ 681 my ($cmd,$data) =@_; 682 683 argsplit("co"); 684 685 my$module=$state->{args}[0]; 686 my$checkout_path=$module; 687 688 # use the user specified directory if we're given it 689$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 690 691$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 692 693$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 694 695$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 696 697# Grab a handle to the SQLite db and do any necessary updates 698my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 699$updater->update(); 700 701$checkout_path=~ s|/$||;# get rid of trailing slashes 702 703# Eclipse seems to need the Clear-sticky command 704# to prepare the 'Entries' file for the new directory. 705print"Clear-sticky$checkout_path/\n"; 706print$state->{CVSROOT} ."/$module/\n"; 707print"Clear-static-directory$checkout_path/\n"; 708print$state->{CVSROOT} ."/$module/\n"; 709print"Clear-sticky$checkout_path/\n";# yes, twice 710print$state->{CVSROOT} ."/$module/\n"; 711print"Template$checkout_path/\n"; 712print$state->{CVSROOT} ."/$module/\n"; 713print"0\n"; 714 715# instruct the client that we're checking out to $checkout_path 716print"E cvs checkout: Updating$checkout_path\n"; 717 718my%seendirs= (); 719my$lastdir=''; 720 721# recursive 722sub prepdir { 723my($dir,$repodir,$remotedir,$seendirs) =@_; 724my$parent= dirname($dir); 725$dir=~ s|/+$||; 726$repodir=~ s|/+$||; 727$remotedir=~ s|/+$||; 728$parent=~ s|/+$||; 729$log->debug("announcedir$dir,$repodir,$remotedir"); 730 731if($parenteq'.'||$parenteq'./') { 732$parent=''; 733} 734# recurse to announce unseen parents first 735if(length($parent) && !exists($seendirs->{$parent})) { 736 prepdir($parent,$repodir,$remotedir,$seendirs); 737} 738# Announce that we are going to modify at the parent level 739if($parent) { 740print"E cvs checkout: Updating$remotedir/$parent\n"; 741}else{ 742print"E cvs checkout: Updating$remotedir\n"; 743} 744print"Clear-sticky$remotedir/$parent/\n"; 745print"$repodir/$parent/\n"; 746 747print"Clear-static-directory$remotedir/$dir/\n"; 748print"$repodir/$dir/\n"; 749print"Clear-sticky$remotedir/$parent/\n";# yes, twice 750print"$repodir/$parent/\n"; 751print"Template$remotedir/$dir/\n"; 752print"$repodir/$dir/\n"; 753print"0\n"; 754 755$seendirs->{$dir} =1; 756} 757 758foreachmy$git( @{$updater->gethead} ) 759{ 760# Don't want to check out deleted files 761next if($git->{filehash}eq"deleted"); 762 763($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 764 765if(length($git->{dir}) &&$git->{dir}ne'./' 766&&$git->{dir}ne$lastdir) { 767unless(exists($seendirs{$git->{dir}})) { 768 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 769$checkout_path, \%seendirs); 770$lastdir=$git->{dir}; 771$seendirs{$git->{dir}} =1; 772} 773print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 774} 775 776# modification time of this file 777print"Mod-time$git->{modified}\n"; 778 779# print some information to the client 780if(defined($git->{dir} )and$git->{dir}ne"./") 781{ 782print"M U$checkout_path/$git->{dir}$git->{name}\n"; 783}else{ 784print"M U$checkout_path/$git->{name}\n"; 785} 786 787# instruct client we're sending a file to put in this path 788print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 789 790print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 791 792# this is an "entries" line 793my$kopts= kopts_from_path($git->{name}); 794print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 795# permissions 796print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 797 798# transmit file 799 transmitfile($git->{filehash}); 800} 801 802print"ok\n"; 803 804 statecleanup(); 805} 806 807# update \n 808# Response expected: yes. Actually do a cvs update command. This uses any 809# previous Argument, Directory, Entry, or Modified requests, if they have 810# been sent. The last Directory sent specifies the working directory at the 811# time of the operation. The -I option is not used--files which the client 812# can decide whether to ignore are not mentioned and the client sends the 813# Questionable request for others. 814sub req_update 815{ 816my($cmd,$data) =@_; 817 818$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 819 820 argsplit("update"); 821 822# 823# It may just be a client exploring the available heads/modules 824# in that case, list them as top level directories and leave it 825# at that. Eclipse uses this technique to offer you a list of 826# projects (heads in this case) to checkout. 827# 828if($state->{module}eq'') { 829print"E cvs update: Updating .\n"; 830opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 831while(my$head=readdir(HEADS)) { 832if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 833print"E cvs update: New directory `$head'\n"; 834} 835} 836closedir HEADS; 837print"ok\n"; 838return1; 839} 840 841 842# Grab a handle to the SQLite db and do any necessary updates 843my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 844 845$updater->update(); 846 847 argsfromdir($updater); 848 849#$log->debug("update state : " . Dumper($state)); 850 851# foreach file specified on the command line ... 852foreachmy$filename( @{$state->{args}} ) 853{ 854$filename= filecleanup($filename); 855 856$log->debug("Processing file$filename"); 857 858# if we have a -C we should pretend we never saw modified stuff 859if(exists($state->{opt}{C} ) ) 860{ 861delete$state->{entries}{$filename}{modified_hash}; 862delete$state->{entries}{$filename}{modified_filename}; 863$state->{entries}{$filename}{unchanged} =1; 864} 865 866my$meta; 867if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 868{ 869$meta=$updater->getmeta($filename,$1); 870}else{ 871$meta=$updater->getmeta($filename); 872} 873 874if( !defined$meta) 875{ 876$meta= { 877 name =>$filename, 878 revision =>0, 879 filehash =>'added' 880}; 881} 882 883my$oldmeta=$meta; 884 885my$wrev= revparse($filename); 886 887# If the working copy is an old revision, lets get that version too for comparison. 888if(defined($wrev)and$wrev!=$meta->{revision} ) 889{ 890$oldmeta=$updater->getmeta($filename,$wrev); 891} 892 893#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 894 895# Files are up to date if the working copy and repo copy have the same revision, 896# and the working copy is unmodified _and_ the user hasn't specified -C 897next if(defined($wrev) 898and defined($meta->{revision}) 899and$wrev==$meta->{revision} 900and$state->{entries}{$filename}{unchanged} 901and not exists($state->{opt}{C} ) ); 902 903# If the working copy and repo copy have the same revision, 904# but the working copy is modified, tell the client it's modified 905if(defined($wrev) 906and defined($meta->{revision}) 907and$wrev==$meta->{revision} 908and defined($state->{entries}{$filename}{modified_hash}) 909and not exists($state->{opt}{C} ) ) 910{ 911$log->info("Tell the client the file is modified"); 912print"MT text M\n"; 913print"MT fname$filename\n"; 914print"MT newline\n"; 915next; 916} 917 918if($meta->{filehash}eq"deleted") 919{ 920my($filepart,$dirpart) = filenamesplit($filename,1); 921 922$log->info("Removing '$filename' from working copy (no longer in the repo)"); 923 924print"E cvs update: `$filename' is no longer in the repository\n"; 925# Don't want to actually _DO_ the update if -n specified 926unless($state->{globaloptions}{-n} ) { 927print"Removed$dirpart\n"; 928print"$filepart\n"; 929} 930} 931elsif(not defined($state->{entries}{$filename}{modified_hash} ) 932or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 933or$meta->{filehash}eq'added') 934{ 935# normal update, just send the new revision (either U=Update, 936# or A=Add, or R=Remove) 937if(defined($wrev) &&$wrev<0) 938{ 939$log->info("Tell the client the file is scheduled for removal"); 940print"MT text R\n"; 941print"MT fname$filename\n"; 942print"MT newline\n"; 943next; 944} 945elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 946{ 947$log->info("Tell the client the file is scheduled for addition"); 948print"MT text A\n"; 949print"MT fname$filename\n"; 950print"MT newline\n"; 951next; 952 953} 954else{ 955$log->info("Updating '$filename' to ".$meta->{revision}); 956print"MT +updated\n"; 957print"MT text U\n"; 958print"MT fname$filename\n"; 959print"MT newline\n"; 960print"MT -updated\n"; 961} 962 963my($filepart,$dirpart) = filenamesplit($filename,1); 964 965# Don't want to actually _DO_ the update if -n specified 966unless($state->{globaloptions}{-n} ) 967{ 968if(defined($wrev) ) 969{ 970# instruct client we're sending a file to put in this path as a replacement 971print"Update-existing$dirpart\n"; 972$log->debug("Updating existing file 'Update-existing$dirpart'"); 973}else{ 974# instruct client we're sending a file to put in this path as a new file 975print"Clear-static-directory$dirpart\n"; 976print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 977print"Clear-sticky$dirpart\n"; 978print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 979 980$log->debug("Creating new file 'Created$dirpart'"); 981print"Created$dirpart\n"; 982} 983print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 984 985# this is an "entries" line 986my$kopts= kopts_from_path($filepart); 987$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 988print"/$filepart/1.$meta->{revision}//$kopts/\n"; 989 990# permissions 991$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 992print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 993 994# transmit file 995 transmitfile($meta->{filehash}); 996} 997}else{ 998$log->info("Updating '$filename'"); 999my($filepart,$dirpart) = filenamesplit($meta->{name},1);10001001my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/";10021003chdir$dir;1004my$file_local=$filepart.".mine";1005system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1006my$file_old=$filepart.".".$oldmeta->{revision};1007 transmitfile($oldmeta->{filehash},$file_old);1008my$file_new=$filepart.".".$meta->{revision};1009 transmitfile($meta->{filehash},$file_new);10101011# we need to merge with the local changes ( M=successful merge, C=conflict merge )1012$log->info("Merging$file_local,$file_old,$file_new");1013print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10141015$log->debug("Temporary directory for merge is$dir");10161017my$return=system("git","merge-file",$file_local,$file_old,$file_new);1018$return>>=8;10191020if($return==0)1021{1022$log->info("Merged successfully");1023print"M M$filename\n";1024$log->debug("Merged$dirpart");10251026# Don't want to actually _DO_ the update if -n specified1027unless($state->{globaloptions}{-n} )1028{1029print"Merged$dirpart\n";1030$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1031print$state->{CVSROOT} ."/$state->{module}/$filename\n";1032my$kopts= kopts_from_path($filepart);1033$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1034print"/$filepart/1.$meta->{revision}//$kopts/\n";1035}1036}1037elsif($return==1)1038{1039$log->info("Merged with conflicts");1040print"E cvs update: conflicts found in$filename\n";1041print"M C$filename\n";10421043# Don't want to actually _DO_ the update if -n specified1044unless($state->{globaloptions}{-n} )1045{1046print"Merged$dirpart\n";1047print$state->{CVSROOT} ."/$state->{module}/$filename\n";1048my$kopts= kopts_from_path($filepart);1049print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1050}1051}1052else1053{1054$log->warn("Merge failed");1055next;1056}10571058# Don't want to actually _DO_ the update if -n specified1059unless($state->{globaloptions}{-n} )1060{1061# permissions1062$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1063print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10641065# transmit file, format is single integer on a line by itself (file1066# size) followed by the file contents1067# TODO : we should copy files in blocks1068my$data=`cat$file_local`;1069$log->debug("File size : " . length($data));1070 print length($data) . "\n";1071 print$data;1072 }10731074 chdir "/";1075 }10761077 }10781079 print "ok\n";1080}10811082sub req_ci1083{1084 my ($cmd,$data) =@_;10851086 argsplit("ci");10871088 #$log->debug("State : " . Dumper($state));10891090$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10911092 if ($state->{method} eq 'pserver')1093 {1094 print "error 1 pserver access cannot commit\n";1095 exit;1096 }10971098 if ( -e$state->{CVSROOT} . "/index" )1099 {1100$log->warn("file 'index' already exists in the git repository");1101 print "error 1 Index already exists in git repo\n";1102 exit;1103 }11041105 # Grab a handle to the SQLite db and do any necessary updates1106 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1107$updater->update();11081109 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1110 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1111$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");11121113$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1114$ENV{GIT_INDEX_FILE} =$file_index;11151116 # Remember where the head was at the beginning.1117 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1118 chomp$parenthash;1119 if ($parenthash!~ /^[0-9a-f]{40}$/) {1120 print "error 1 pserver cannot find the current HEAD of module";1121 exit;1122 }11231124 chdir$tmpdir;11251126 # populate the temporary index based1127 system("git-read-tree",$parenthash);1128 unless ($?== 0)1129 {1130 die "Error running git-read-tree$state->{module}$file_index$!";1131 }1132$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");11331134 my@committedfiles= ();1135 my%oldmeta;11361137 # foreach file specified on the command line ...1138 foreach my$filename( @{$state->{args}} )1139 {1140 my$committedfile=$filename;1141$filename= filecleanup($filename);11421143 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );11441145 my$meta=$updater->getmeta($filename);1146$oldmeta{$filename} =$meta;11471148 my$wrev= revparse($filename);11491150 my ($filepart,$dirpart) = filenamesplit($filename);11511152 # do a checkout of the file if it part of this tree1153 if ($wrev) {1154 system('git-checkout-index', '-f', '-u',$filename);1155 unless ($?== 0) {1156 die "Error running git-checkout-index -f -u$filename:$!";1157 }1158 }11591160 my$addflag= 0;1161 my$rmflag= 0;1162$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1163$addflag= 1 unless ( -e$filename);11641165 # Do up to date checking1166 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1167 {1168 # fail everything if an up to date check fails1169 print "error 1 Up to date check failed for$filename\n";1170 chdir "/";1171 exit;1172 }11731174 push@committedfiles,$committedfile;1175$log->info("Committing$filename");11761177 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11781179 unless ($rmflag)1180 {1181$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1182 rename$state->{entries}{$filename}{modified_filename},$filename;11831184 # Calculate modes to remove1185 my$invmode= "";1186 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11871188$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1189 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1190 }11911192 if ($rmflag)1193 {1194$log->info("Removing file '$filename'");1195 unlink($filename);1196 system("git-update-index", "--remove",$filename);1197 }1198 elsif ($addflag)1199 {1200$log->info("Adding file '$filename'");1201 system("git-update-index", "--add",$filename);1202 } else {1203$log->info("Updating file '$filename'");1204 system("git-update-index",$filename);1205 }1206 }12071208 unless ( scalar(@committedfiles) > 0 )1209 {1210 print "E No files to commit\n";1211 print "ok\n";1212 chdir "/";1213 return;1214 }12151216 my$treehash= `git-write-tree`;1217 chomp$treehash;12181219$log->debug("Treehash :$treehash, Parenthash :$parenthash");12201221 # write our commit message out if we have one ...1222 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1223 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1224 print$msg_fh"\n\nvia git-CVS emulator\n";1225 close$msg_fh;12261227 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1228chomp($commithash);1229$log->info("Commit hash :$commithash");12301231unless($commithash=~/[a-zA-Z0-9]{40}/)1232{1233$log->warn("Commit failed (Invalid commit hash)");1234print"error 1 Commit failed (unknown reason)\n";1235chdir"/";1236exit;1237}12381239# Check that this is allowed, just as we would with a receive-pack1240my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1241$parenthash,$commithash);1242if( -x $cmd[0] ) {1243unless(system(@cmd) ==0)1244{1245$log->warn("Commit failed (update hook declined to update ref)");1246print"error 1 Commit failed (update hook declined)\n";1247chdir"/";1248exit;1249}1250}12511252if(system(qw(git update-ref -m),"cvsserver ci",1253"refs/heads/$state->{module}",$commithash,$parenthash)) {1254$log->warn("update-ref for$state->{module} failed.");1255print"error 1 Cannot commit -- update first\n";1256exit;1257}12581259$updater->update();12601261# foreach file specified on the command line ...1262foreachmy$filename(@committedfiles)1263{1264$filename= filecleanup($filename);12651266my$meta=$updater->getmeta($filename);1267unless(defined$meta->{revision}) {1268$meta->{revision} =1;1269}12701271my($filepart,$dirpart) = filenamesplit($filename,1);12721273$log->debug("Checked-in$dirpart:$filename");12741275print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1276if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1277{1278print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1279print"Remove-entry$dirpart\n";1280print"$filename\n";1281}else{1282if($meta->{revision} ==1) {1283print"M initial revision: 1.1\n";1284}else{1285print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1286}1287print"Checked-in$dirpart\n";1288print"$filename\n";1289my$kopts= kopts_from_path($filepart);1290print"/$filepart/1.$meta->{revision}//$kopts/\n";1291}1292}12931294chdir"/";1295print"ok\n";1296}12971298sub req_status1299{1300my($cmd,$data) =@_;13011302 argsplit("status");13031304$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1305#$log->debug("status state : " . Dumper($state));13061307# Grab a handle to the SQLite db and do any necessary updates1308my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1309$updater->update();13101311# if no files were specified, we need to work out what files we should be providing status on ...1312 argsfromdir($updater);13131314# foreach file specified on the command line ...1315foreachmy$filename( @{$state->{args}} )1316{1317$filename= filecleanup($filename);13181319my$meta=$updater->getmeta($filename);1320my$oldmeta=$meta;13211322my$wrev= revparse($filename);13231324# If the working copy is an old revision, lets get that version too for comparison.1325if(defined($wrev)and$wrev!=$meta->{revision} )1326{1327$oldmeta=$updater->getmeta($filename,$wrev);1328}13291330# TODO : All possible statuses aren't yet implemented1331my$status;1332# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1333$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1334and1335( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1336or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1337);13381339# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1340$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1341and1342($state->{entries}{$filename}{unchanged}1343or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1344);13451346# Need checkout if it exists in the repo but doesn't have a working copy1347$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );13481349# Locally modified if working copy and repo copy have the same revision but there are local changes1350$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );13511352# Needs Merge if working copy revision is less than repo copy and there are local changes1353$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );13541355$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1356$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1357$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1358$status||="File had conflicts on merge"if(0);13591360$status||="Unknown";13611362print"M ===================================================================\n";1363print"M File:$filename\tStatus:$status\n";1364if(defined($state->{entries}{$filename}{revision}) )1365{1366print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1367}else{1368print"M Working revision:\tNo entry for$filename\n";1369}1370if(defined($meta->{revision}) )1371{1372print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1373print"M Sticky Tag:\t\t(none)\n";1374print"M Sticky Date:\t\t(none)\n";1375print"M Sticky Options:\t\t(none)\n";1376}else{1377print"M Repository revision:\tNo revision control file\n";1378}1379print"M\n";1380}13811382print"ok\n";1383}13841385sub req_diff1386{1387my($cmd,$data) =@_;13881389 argsplit("diff");13901391$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1392#$log->debug("status state : " . Dumper($state));13931394my($revision1,$revision2);1395if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1396{1397$revision1=$state->{opt}{r}[0];1398$revision2=$state->{opt}{r}[1];1399}else{1400$revision1=$state->{opt}{r};1401}14021403$revision1=~s/^1\.//if(defined($revision1) );1404$revision2=~s/^1\.//if(defined($revision2) );14051406$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );14071408# Grab a handle to the SQLite db and do any necessary updates1409my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1410$updater->update();14111412# if no files were specified, we need to work out what files we should be providing status on ...1413 argsfromdir($updater);14141415# foreach file specified on the command line ...1416foreachmy$filename( @{$state->{args}} )1417{1418$filename= filecleanup($filename);14191420my($fh,$file1,$file2,$meta1,$meta2,$filediff);14211422my$wrev= revparse($filename);14231424# We need _something_ to diff against1425next unless(defined($wrev) );14261427# if we have a -r switch, use it1428if(defined($revision1) )1429{1430(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1431$meta1=$updater->getmeta($filename,$revision1);1432unless(defined($meta1)and$meta1->{filehash}ne"deleted")1433{1434print"E File$filenameat revision 1.$revision1doesn't exist\n";1435next;1436}1437 transmitfile($meta1->{filehash},$file1);1438}1439# otherwise we just use the working copy revision1440else1441{1442(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1443$meta1=$updater->getmeta($filename,$wrev);1444 transmitfile($meta1->{filehash},$file1);1445}14461447# if we have a second -r switch, use it too1448if(defined($revision2) )1449{1450(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1451$meta2=$updater->getmeta($filename,$revision2);14521453unless(defined($meta2)and$meta2->{filehash}ne"deleted")1454{1455print"E File$filenameat revision 1.$revision2doesn't exist\n";1456next;1457}14581459 transmitfile($meta2->{filehash},$file2);1460}1461# otherwise we just use the working copy1462else1463{1464$file2=$state->{entries}{$filename}{modified_filename};1465}14661467# if we have been given -r, and we don't have a $file2 yet, lets get one1468if(defined($revision1)and not defined($file2) )1469{1470(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1471$meta2=$updater->getmeta($filename,$wrev);1472 transmitfile($meta2->{filehash},$file2);1473}14741475# We need to have retrieved something useful1476next unless(defined($meta1) );14771478# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1479next if(not defined($meta2)and$wrev==$meta1->{revision}1480and1481( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1482or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1483);14841485# Apparently we only show diffs for locally modified files1486next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14871488print"M Index:$filename\n";1489print"M ===================================================================\n";1490print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1491print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1492print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1493print"M diff ";1494foreachmy$opt(keys%{$state->{opt}} )1495{1496if(ref$state->{opt}{$opt}eq"ARRAY")1497{1498foreachmy$value( @{$state->{opt}{$opt}} )1499{1500print"-$opt$value";1501}1502}else{1503print"-$opt";1504print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1505}1506}1507print"$filename\n";15081509$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));15101511($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15121513if(exists$state->{opt}{u} )1514{1515system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1516}else{1517system("diff$file1$file2>$filediff");1518}15191520while( <$fh> )1521{1522print"M$_";1523}1524close$fh;1525}15261527print"ok\n";1528}15291530sub req_log1531{1532my($cmd,$data) =@_;15331534 argsplit("log");15351536$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1537#$log->debug("log state : " . Dumper($state));15381539my($minrev,$maxrev);1540if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1541{1542my$control=$2;1543$minrev=$1;1544$maxrev=$3;1545$minrev=~s/^1\.//if(defined($minrev) );1546$maxrev=~s/^1\.//if(defined($maxrev) );1547$minrev++if(defined($minrev)and$controleq"::");1548}15491550# Grab a handle to the SQLite db and do any necessary updates1551my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1552$updater->update();15531554# if no files were specified, we need to work out what files we should be providing status on ...1555 argsfromdir($updater);15561557# foreach file specified on the command line ...1558foreachmy$filename( @{$state->{args}} )1559{1560$filename= filecleanup($filename);15611562my$headmeta=$updater->getmeta($filename);15631564my$revisions=$updater->getlog($filename);1565my$totalrevisions=scalar(@$revisions);15661567if(defined($minrev) )1568{1569$log->debug("Removing revisions less than$minrev");1570while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1571{1572pop@$revisions;1573}1574}1575if(defined($maxrev) )1576{1577$log->debug("Removing revisions greater than$maxrev");1578while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1579{1580shift@$revisions;1581}1582}15831584next unless(scalar(@$revisions) );15851586print"M\n";1587print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1588print"M Working file:$filename\n";1589print"M head: 1.$headmeta->{revision}\n";1590print"M branch:\n";1591print"M locks: strict\n";1592print"M access list:\n";1593print"M symbolic names:\n";1594print"M keyword substitution: kv\n";1595print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1596print"M description:\n";15971598foreachmy$revision(@$revisions)1599{1600print"M ----------------------------\n";1601print"M revision 1.$revision->{revision}\n";1602# reformat the date for log output1603$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}) );1604$revision->{author} =~s/\s+.*//;1605$revision->{author} =~s/^(.{8}).*/$1/;1606print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1607my$commitmessage=$updater->commitmessage($revision->{commithash});1608$commitmessage=~s/^/M /mg;1609print$commitmessage."\n";1610}1611print"M =============================================================================\n";1612}16131614print"ok\n";1615}16161617sub req_annotate1618{1619my($cmd,$data) =@_;16201621 argsplit("annotate");16221623$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1624#$log->debug("status state : " . Dumper($state));16251626# Grab a handle to the SQLite db and do any necessary updates1627my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1628$updater->update();16291630# if no files were specified, we need to work out what files we should be providing annotate on ...1631 argsfromdir($updater);16321633# we'll need a temporary checkout dir1634my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1635my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1636$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");16371638$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1639$ENV{GIT_INDEX_FILE} =$file_index;16401641chdir$tmpdir;16421643# foreach file specified on the command line ...1644foreachmy$filename( @{$state->{args}} )1645{1646$filename= filecleanup($filename);16471648my$meta=$updater->getmeta($filename);16491650next unless($meta->{revision} );16511652# get all the commits that this file was in1653# in dense format -- aka skip dead revisions1654my$revisions=$updater->gethistorydense($filename);1655my$lastseenin=$revisions->[0][2];16561657# populate the temporary index based on the latest commit were we saw1658# the file -- but do it cheaply without checking out any files1659# TODO: if we got a revision from the client, use that instead1660# to look up the commithash in sqlite (still good to default to1661# the current head as we do now)1662system("git-read-tree",$lastseenin);1663unless($?==0)1664{1665die"Error running git-read-tree$lastseenin$file_index$!";1666}1667$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16681669# do a checkout of the file1670system('git-checkout-index','-f','-u',$filename);1671unless($?==0) {1672die"Error running git-checkout-index -f -u$filename:$!";1673}16741675$log->info("Annotate$filename");16761677# Prepare a file with the commits from the linearized1678# history that annotate should know about. This prevents1679# git-jsannotate telling us about commits we are hiding1680# from the client.16811682open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1683for(my$i=0;$i<@$revisions;$i++)1684{1685print ANNOTATEHINTS $revisions->[$i][2];1686if($i+1<@$revisions) {# have we got a parent?1687print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1688}1689print ANNOTATEHINTS "\n";1690}16911692print ANNOTATEHINTS "\n";1693close ANNOTATEHINTS;16941695my$annotatecmd='git-annotate';1696open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1697or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1698my$metadata= {};1699print"E Annotations for$filename\n";1700print"E ***************\n";1701while( <ANNOTATE> )1702{1703if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1704{1705my$commithash=$1;1706my$data=$2;1707unless(defined($metadata->{$commithash} ) )1708{1709$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1710$metadata->{$commithash}{author} =~s/\s+.*//;1711$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1712$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1713}1714printf("M 1.%-5d (%-8s%10s):%s\n",1715$metadata->{$commithash}{revision},1716$metadata->{$commithash}{author},1717$metadata->{$commithash}{modified},1718$data1719);1720}else{1721$log->warn("Error in annotate output! LINE:$_");1722print"E Annotate error\n";1723next;1724}1725}1726close ANNOTATE;1727}17281729# done; get out of the tempdir1730chdir"/";17311732print"ok\n";17331734}17351736# This method takes the state->{arguments} array and produces two new arrays.1737# The first is $state->{args} which is everything before the '--' argument, and1738# the second is $state->{files} which is everything after it.1739sub argsplit1740{1741return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");17421743my$type=shift;17441745$state->{args} = [];1746$state->{files} = [];1747$state->{opt} = {};17481749if(defined($type) )1750{1751my$opt= {};1752$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");1753$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1754$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");1755$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1756$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1757$opt= { k =>1, m =>1}if($typeeq"add");1758$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1759$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");176017611762while(scalar( @{$state->{arguments}} ) >0)1763{1764my$arg=shift@{$state->{arguments}};17651766next if($argeq"--");1767next unless($arg=~/\S/);17681769# if the argument looks like a switch1770if($arg=~/^-(\w)(.*)/)1771{1772# if it's a switch that takes an argument1773if($opt->{$1} )1774{1775# If this switch has already been provided1776if($opt->{$1} >1and exists($state->{opt}{$1} ) )1777{1778$state->{opt}{$1} = [$state->{opt}{$1} ];1779if(length($2) >0)1780{1781push@{$state->{opt}{$1}},$2;1782}else{1783push@{$state->{opt}{$1}},shift@{$state->{arguments}};1784}1785}else{1786# if there's extra data in the arg, use that as the argument for the switch1787if(length($2) >0)1788{1789$state->{opt}{$1} =$2;1790}else{1791$state->{opt}{$1} =shift@{$state->{arguments}};1792}1793}1794}else{1795$state->{opt}{$1} =undef;1796}1797}1798else1799{1800push@{$state->{args}},$arg;1801}1802}1803}1804else1805{1806my$mode=0;18071808foreachmy$value( @{$state->{arguments}} )1809{1810if($valueeq"--")1811{1812$mode++;1813next;1814}1815push@{$state->{args}},$valueif($mode==0);1816push@{$state->{files}},$valueif($mode==1);1817}1818}1819}18201821# This method uses $state->{directory} to populate $state->{args} with a list of filenames1822sub argsfromdir1823{1824my$updater=shift;18251826$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");18271828return if(scalar( @{$state->{args}} ) >1);18291830my@gethead= @{$updater->gethead};18311832# push added files1833foreachmy$file(keys%{$state->{entries}}) {1834if(exists$state->{entries}{$file}{revision} &&1835$state->{entries}{$file}{revision} ==0)1836{1837push@gethead, { name =>$file, filehash =>'added'};1838}1839}18401841if(scalar(@{$state->{args}}) ==1)1842{1843my$arg=$state->{args}[0];1844$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );18451846$log->info("Only one arg specified, checking for directory expansion on '$arg'");18471848foreachmy$file(@gethead)1849{1850next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1851next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1852push@{$state->{args}},$file->{name};1853}18541855shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1856}else{1857$log->info("Only one arg specified, populating file list automatically");18581859$state->{args} = [];18601861foreachmy$file(@gethead)1862{1863next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1864next unless($file->{name} =~s/^$state->{prependdir}//);1865push@{$state->{args}},$file->{name};1866}1867}1868}18691870# This method cleans up the $state variable after a command that uses arguments has run1871sub statecleanup1872{1873$state->{files} = [];1874$state->{args} = [];1875$state->{arguments} = [];1876$state->{entries} = {};1877}18781879sub revparse1880{1881my$filename=shift;18821883returnundefunless(defined($state->{entries}{$filename}{revision} ) );18841885return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1886return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18871888returnundef;1889}18901891# This method takes a file hash and does a CVS "file transfer" which transmits the1892# size of the file, and then the file contents.1893# If a second argument $targetfile is given, the file is instead written out to1894# a file by the name of $targetfile1895sub transmitfile1896{1897my$filehash=shift;1898my$targetfile=shift;18991900if(defined($filehash)and$filehasheq"deleted")1901{1902$log->warn("filehash is 'deleted'");1903return;1904}19051906die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);19071908my$type=`git-cat-file -t$filehash`;1909 chomp$type;19101911 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );19121913 my$size= `git-cat-file -s $filehash`;1914chomp$size;19151916$log->debug("transmitfile($filehash) size=$size, type=$type");19171918if(open my$fh,'-|',"git-cat-file","blob",$filehash)1919{1920if(defined($targetfile) )1921{1922open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1923print NEWFILE $_while( <$fh> );1924close NEWFILE;1925}else{1926print"$size\n";1927printwhile( <$fh> );1928}1929close$fhor die("Couldn't close filehandle for transmitfile()");1930}else{1931die("Couldn't execute git-cat-file");1932}1933}19341935# This method takes a file name, and returns ( $dirpart, $filepart ) which1936# refers to the directory portion and the file portion of the filename1937# respectively1938sub filenamesplit1939{1940my$filename=shift;1941my$fixforlocaldir=shift;19421943my($filepart,$dirpart) = ($filename,".");1944($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1945$dirpart.="/";19461947if($fixforlocaldir)1948{1949$dirpart=~s/^$state->{prependdir}//;1950}19511952return($filepart,$dirpart);1953}19541955sub filecleanup1956{1957my$filename=shift;19581959returnundefunless(defined($filename));1960if($filename=~/^\// )1961{1962print"E absolute filenames '$filename' not supported by server\n";1963returnundef;1964}19651966$filename=~s/^\.\///g;1967$filename=$state->{prependdir} .$filename;1968return$filename;1969}19701971# Given a path, this function returns a string containing the kopts1972# that should go into that path's Entries line. For example, a binary1973# file should get -kb.1974sub kopts_from_path1975{1976my($path) =@_;19771978# Once it exists, the git attributes system should be used to look up1979# what attributes apply to this path.19801981# Until then, take the setting from the config file1982unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1983{1984# Return "" to give no special treatment to any path1985return"";1986}else{1987# Alternatively, to have all files treated as if they are binary (which1988# is more like git itself), always return the "-kb" option1989return"-kb";1990}1991}19921993package GITCVS::log;19941995####1996#### Copyright The Open University UK - 2006.1997####1998#### Authors: Martyn Smith <martyn@catalyst.net.nz>1999#### Martin Langhoff <martin@catalyst.net.nz>2000####2001####20022003use strict;2004use warnings;20052006=head1 NAME20072008GITCVS::log20092010=head1 DESCRIPTION20112012This module provides very crude logging with a similar interface to2013Log::Log4perl20142015=head1 METHODS20162017=cut20182019=head2 new20202021Creates a new log object, optionally you can specify a filename here to2022indicate the file to log to. If no log file is specified, you can specify one2023later with method setfile, or indicate you no longer want logging with method2024nofile.20252026Until one of these methods is called, all log calls will buffer messages ready2027to write out.20282029=cut2030sub new2031{2032my$class=shift;2033my$filename=shift;20342035my$self= {};20362037bless$self,$class;20382039if(defined($filename) )2040{2041open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2042}20432044return$self;2045}20462047=head2 setfile20482049This methods takes a filename, and attempts to open that file as the log file.2050If successful, all buffered data is written out to the file, and any further2051logging is written directly to the file.20522053=cut2054sub setfile2055{2056my$self=shift;2057my$filename=shift;20582059if(defined($filename) )2060{2061open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2062}20632064return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20652066while(my$line=shift@{$self->{buffer}} )2067{2068print{$self->{fh}}$line;2069}2070}20712072=head2 nofile20732074This method indicates no logging is going to be used. It flushes any entries in2075the internal buffer, and sets a flag to ensure no further data is put there.20762077=cut2078sub nofile2079{2080my$self=shift;20812082$self->{nolog} =1;20832084return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20852086$self->{buffer} = [];2087}20882089=head2 _logopen20902091Internal method. Returns true if the log file is open, false otherwise.20922093=cut2094sub _logopen2095{2096my$self=shift;20972098return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2099return0;2100}21012102=head2 debug info warn fatal21032104These four methods are wrappers to _log. They provide the actual interface for2105logging data.21062107=cut2108sub debug {my$self=shift;$self->_log("debug",@_); }2109sub info {my$self=shift;$self->_log("info",@_); }2110subwarn{my$self=shift;$self->_log("warn",@_); }2111sub fatal {my$self=shift;$self->_log("fatal",@_); }21122113=head2 _log21142115This is an internal method called by the logging functions. It generates a2116timestamp and pushes the logged line either to file, or internal buffer.21172118=cut2119sub _log2120{2121my$self=shift;2122my$level=shift;21232124return if($self->{nolog} );21252126my@time=localtime;2127my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2128$time[5] +1900,2129$time[4] +1,2130$time[3],2131$time[2],2132$time[1],2133$time[0],2134uc$level,2135);21362137if($self->_logopen)2138{2139print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2140}else{2141push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2142}2143}21442145=head2 DESTROY21462147This method simply closes the file handle if one is open21482149=cut2150sub DESTROY2151{2152my$self=shift;21532154if($self->_logopen)2155{2156close$self->{fh};2157}2158}21592160package GITCVS::updater;21612162####2163#### Copyright The Open University UK - 2006.2164####2165#### Authors: Martyn Smith <martyn@catalyst.net.nz>2166#### Martin Langhoff <martin@catalyst.net.nz>2167####2168####21692170use strict;2171use warnings;2172use DBI;21732174=head1 METHODS21752176=cut21772178=head2 new21792180=cut2181sub new2182{2183my$class=shift;2184my$config=shift;2185my$module=shift;2186my$log=shift;21872188die"Need to specify a git repository"unless(defined($config)and-d $config);2189die"Need to specify a module"unless(defined($module) );21902191$class=ref($class) ||$class;21922193my$self= {};21942195bless$self,$class;21962197$self->{module} =$module;2198$self->{git_path} =$config."/";21992200$self->{log} =$log;22012202die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );22032204$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2205$cfg->{gitcvs}{dbdriver} ||"SQLite";2206$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2207$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2208$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2209$cfg->{gitcvs}{dbuser} ||"";2210$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2211$cfg->{gitcvs}{dbpass} ||"";2212my%mapping= ( m =>$module,2213 a =>$state->{method},2214 u =>getlogin||getpwuid($<) || $<,2215 G =>$self->{git_path},2216 g => mangle_dirname($self->{git_path}),2217);2218$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2219$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;22202221die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2222die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2223$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2224$self->{dbuser},2225$self->{dbpass});2226die"Error connecting to database\n"unlessdefined$self->{dbh};22272228$self->{tables} = {};2229foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2230{2231$self->{tables}{$table} =1;2232}22332234# Construct the revision table if required2235unless($self->{tables}{revision} )2236{2237$self->{dbh}->do("2238 CREATE TABLE revision (2239 name TEXT NOT NULL,2240 revision INTEGER NOT NULL,2241 filehash TEXT NOT NULL,2242 commithash TEXT NOT NULL,2243 author TEXT NOT NULL,2244 modified TEXT NOT NULL,2245 mode TEXT NOT NULL2246 )2247 ");2248$self->{dbh}->do("2249 CREATE INDEX revision_ix12250 ON revision (name,revision)2251 ");2252$self->{dbh}->do("2253 CREATE INDEX revision_ix22254 ON revision (name,commithash)2255 ");2256}22572258# Construct the head table if required2259unless($self->{tables}{head} )2260{2261$self->{dbh}->do("2262 CREATE TABLE head (2263 name TEXT NOT NULL,2264 revision INTEGER NOT NULL,2265 filehash TEXT NOT NULL,2266 commithash TEXT NOT NULL,2267 author TEXT NOT NULL,2268 modified TEXT NOT NULL,2269 mode TEXT NOT NULL2270 )2271 ");2272$self->{dbh}->do("2273 CREATE INDEX head_ix12274 ON head (name)2275 ");2276}22772278# Construct the properties table if required2279unless($self->{tables}{properties} )2280{2281$self->{dbh}->do("2282 CREATE TABLE properties (2283 key TEXT NOT NULL PRIMARY KEY,2284 value TEXT2285 )2286 ");2287}22882289# Construct the commitmsgs table if required2290unless($self->{tables}{commitmsgs} )2291{2292$self->{dbh}->do("2293 CREATE TABLE commitmsgs (2294 key TEXT NOT NULL PRIMARY KEY,2295 value TEXT2296 )2297 ");2298}22992300return$self;2301}23022303=head2 update23042305=cut2306sub update2307{2308my$self=shift;23092310# first lets get the commit list2311$ENV{GIT_DIR} =$self->{git_path};23122313my$commitsha1=`git rev-parse$self->{module}`;2314chomp$commitsha1;23152316my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2317unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2318{2319die("Invalid module '$self->{module}'");2320}232123222323my$git_log;2324my$lastcommit=$self->_get_prop("last_commit");23252326if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2327return1;2328}23292330# Start exclusive lock here...2331$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";23322333# TODO: log processing is memory bound2334# if we can parse into a 2nd file that is in reverse order2335# we can probably do something really efficient2336my@git_log_params= ('--pretty','--parents','--topo-order');23372338if(defined$lastcommit) {2339push@git_log_params,"$lastcommit..$self->{module}";2340}else{2341push@git_log_params,$self->{module};2342}2343# git-rev-list is the backend / plumbing version of git-log2344open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";23452346my@commits;23472348my%commit= ();23492350while( <GITLOG> )2351{2352chomp;2353if(m/^commit\s+(.*)$/) {2354# on ^commit lines put the just seen commit in the stack2355# and prime things for the next one2356if(keys%commit) {2357my%copy=%commit;2358unshift@commits, \%copy;2359%commit= ();2360}2361my@parents=split(m/\s+/,$1);2362$commit{hash} =shift@parents;2363$commit{parents} = \@parents;2364}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2365# on rfc822-like lines seen before we see any message,2366# lowercase the entry and put it in the hash as key-value2367$commit{lc($1)} =$2;2368}else{2369# message lines - skip initial empty line2370# and trim whitespace2371if(!exists($commit{message}) &&m/^\s*$/) {2372# define it to mark the end of headers2373$commit{message} ='';2374next;2375}2376s/^\s+//;s/\s+$//;# trim ws2377$commit{message} .=$_."\n";2378}2379}2380close GITLOG;23812382unshift@commits, \%commitif(keys%commit);23832384# Now all the commits are in the @commits bucket2385# ordered by time DESC. for each commit that needs processing,2386# determine whether it's following the last head we've seen or if2387# it's on its own branch, grab a file list, and add whatever's changed2388# NOTE: $lastcommit refers to the last commit from previous run2389# $lastpicked is the last commit we picked in this run2390my$lastpicked;2391my$head= {};2392if(defined$lastcommit) {2393$lastpicked=$lastcommit;2394}23952396my$committotal=scalar(@commits);2397my$commitcount=0;23982399# Load the head table into $head (for cached lookups during the update process)2400foreachmy$file( @{$self->gethead()} )2401{2402$head->{$file->{name}} =$file;2403}24042405foreachmy$commit(@commits)2406{2407$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2408if(defined$lastpicked)2409{2410if(!in_array($lastpicked, @{$commit->{parents}}))2411{2412# skip, we'll see this delta2413# as part of a merge later2414# warn "skipping off-track $commit->{hash}\n";2415next;2416}elsif(@{$commit->{parents}} >1) {2417# it is a merge commit, for each parent that is2418# not $lastpicked, see if we can get a log2419# from the merge-base to that parent to put it2420# in the message as a merge summary.2421my@parents= @{$commit->{parents}};2422foreachmy$parent(@parents) {2423# git-merge-base can potentially (but rarely) throw2424# several candidate merge bases. let's assume2425# that the first one is the best one.2426if($parenteq$lastpicked) {2427next;2428}2429open my$p,'git-merge-base '.$lastpicked.' '2430.$parent.'|';2431my@output= (<$p>);2432close$p;2433my$base=join('',@output);2434chomp$base;2435if($base) {2436my@merged;2437# print "want to log between $base $parent \n";2438open(GITLOG,'-|','git-log',"$base..$parent")2439or die"Cannot call git-log:$!";2440my$mergedhash;2441while(<GITLOG>) {2442chomp;2443if(!defined$mergedhash) {2444if(m/^commit\s+(.+)$/) {2445$mergedhash=$1;2446}else{2447next;2448}2449}else{2450# grab the first line that looks non-rfc8222451# aka has content after leading space2452if(m/^\s+(\S.*)$/) {2453my$title=$1;2454$title=substr($title,0,100);# truncate2455unshift@merged,"$mergedhash$title";2456undef$mergedhash;2457}2458}2459}2460close GITLOG;2461if(@merged) {2462$commit->{mergemsg} =$commit->{message};2463$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2464foreachmy$summary(@merged) {2465$commit->{mergemsg} .="\t$summary\n";2466}2467$commit->{mergemsg} .="\n\n";2468# print "Message for $commit->{hash} \n$commit->{mergemsg}";2469}2470}2471}2472}2473}24742475# convert the date to CVS-happy format2476$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24772478if(defined($lastpicked) )2479{2480my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2481local($/) ="\0";2482while( <FILELIST> )2483{2484chomp;2485unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2486{2487die("Couldn't process git-diff-tree line :$_");2488}2489my($mode,$hash,$change) = ($1,$2,$3);2490my$name= <FILELIST>;2491chomp($name);24922493# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24942495my$git_perms="";2496$git_perms.="r"if($mode&4);2497$git_perms.="w"if($mode&2);2498$git_perms.="x"if($mode&1);2499$git_perms="rw"if($git_permseq"");25002501if($changeeq"D")2502{2503#$log->debug("DELETE $name");2504$head->{$name} = {2505 name =>$name,2506 revision =>$head->{$name}{revision} +1,2507 filehash =>"deleted",2508 commithash =>$commit->{hash},2509 modified =>$commit->{date},2510 author =>$commit->{author},2511 mode =>$git_perms,2512};2513$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2514}2515elsif($changeeq"M")2516{2517#$log->debug("MODIFIED $name");2518$head->{$name} = {2519 name =>$name,2520 revision =>$head->{$name}{revision} +1,2521 filehash =>$hash,2522 commithash =>$commit->{hash},2523 modified =>$commit->{date},2524 author =>$commit->{author},2525 mode =>$git_perms,2526};2527$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2528}2529elsif($changeeq"A")2530{2531#$log->debug("ADDED $name");2532$head->{$name} = {2533 name =>$name,2534 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2535 filehash =>$hash,2536 commithash =>$commit->{hash},2537 modified =>$commit->{date},2538 author =>$commit->{author},2539 mode =>$git_perms,2540};2541$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2542}2543else2544{2545$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2546die;2547}2548}2549close FILELIST;2550}else{2551# this is used to detect files removed from the repo2552my$seen_files= {};25532554my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2555local$/="\0";2556while( <FILELIST> )2557{2558chomp;2559unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2560{2561die("Couldn't process git-ls-tree line :$_");2562}25632564my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);25652566$seen_files->{$git_filename} =1;25672568my($oldhash,$oldrevision,$oldmode) = (2569$head->{$git_filename}{filehash},2570$head->{$git_filename}{revision},2571$head->{$git_filename}{mode}2572);25732574if($git_perms=~/^\d\d\d(\d)\d\d/o)2575{2576$git_perms="";2577$git_perms.="r"if($1&4);2578$git_perms.="w"if($1&2);2579$git_perms.="x"if($1&1);2580}else{2581$git_perms="rw";2582}25832584# unless the file exists with the same hash, we need to update it ...2585unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2586{2587my$newrevision= ($oldrevisionor0) +1;25882589$head->{$git_filename} = {2590 name =>$git_filename,2591 revision =>$newrevision,2592 filehash =>$git_hash,2593 commithash =>$commit->{hash},2594 modified =>$commit->{date},2595 author =>$commit->{author},2596 mode =>$git_perms,2597};259825992600$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2601}2602}2603close FILELIST;26042605# Detect deleted files2606foreachmy$file(keys%$head)2607{2608unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2609{2610$head->{$file}{revision}++;2611$head->{$file}{filehash} ="deleted";2612$head->{$file}{commithash} =$commit->{hash};2613$head->{$file}{modified} =$commit->{date};2614$head->{$file}{author} =$commit->{author};26152616$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2617}2618}2619# END : "Detect deleted files"2620}262126222623if(exists$commit->{mergemsg})2624{2625$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2626}26272628$lastpicked=$commit->{hash};26292630$self->_set_prop("last_commit",$commit->{hash});2631}26322633$self->delete_head();2634foreachmy$file(keys%$head)2635{2636$self->insert_head(2637$file,2638$head->{$file}{revision},2639$head->{$file}{filehash},2640$head->{$file}{commithash},2641$head->{$file}{modified},2642$head->{$file}{author},2643$head->{$file}{mode},2644);2645}2646# invalidate the gethead cache2647$self->{gethead_cache} =undef;264826492650# Ending exclusive lock here2651$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2652}26532654sub insert_rev2655{2656my$self=shift;2657my$name=shift;2658my$revision=shift;2659my$filehash=shift;2660my$commithash=shift;2661my$modified=shift;2662my$author=shift;2663my$mode=shift;26642665my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2666$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2667}26682669sub insert_mergelog2670{2671my$self=shift;2672my$key=shift;2673my$value=shift;26742675my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2676$insert_mergelog->execute($key,$value);2677}26782679sub delete_head2680{2681my$self=shift;26822683my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2684$delete_head->execute();2685}26862687sub insert_head2688{2689my$self=shift;2690my$name=shift;2691my$revision=shift;2692my$filehash=shift;2693my$commithash=shift;2694my$modified=shift;2695my$author=shift;2696my$mode=shift;26972698my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2699$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2700}27012702sub _headrev2703{2704my$self=shift;2705my$filename=shift;27062707my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2708$db_query->execute($filename);2709my($hash,$revision,$mode) =$db_query->fetchrow_array;27102711return($hash,$revision,$mode);2712}27132714sub _get_prop2715{2716my$self=shift;2717my$key=shift;27182719my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2720$db_query->execute($key);2721my($value) =$db_query->fetchrow_array;27222723return$value;2724}27252726sub _set_prop2727{2728my$self=shift;2729my$key=shift;2730my$value=shift;27312732my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2733$db_query->execute($value,$key);27342735unless($db_query->rows)2736{2737$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2738$db_query->execute($key,$value);2739}27402741return$value;2742}27432744=head2 gethead27452746=cut27472748sub gethead2749{2750my$self=shift;27512752return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );27532754my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2755$db_query->execute();27562757my$tree= [];2758while(my$file=$db_query->fetchrow_hashref)2759{2760push@$tree,$file;2761}27622763$self->{gethead_cache} =$tree;27642765return$tree;2766}27672768=head2 getlog27692770=cut27712772sub getlog2773{2774my$self=shift;2775my$filename=shift;27762777my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2778$db_query->execute($filename);27792780my$tree= [];2781while(my$file=$db_query->fetchrow_hashref)2782{2783push@$tree,$file;2784}27852786return$tree;2787}27882789=head2 getmeta27902791This function takes a filename (with path) argument and returns a hashref of2792metadata for that file.27932794=cut27952796sub getmeta2797{2798my$self=shift;2799my$filename=shift;2800my$revision=shift;28012802my$db_query;2803if(defined($revision)and$revision=~/^\d+$/)2804{2805$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2806$db_query->execute($filename,$revision);2807}2808elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2809{2810$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2811$db_query->execute($filename,$revision);2812}else{2813$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2814$db_query->execute($filename);2815}28162817return$db_query->fetchrow_hashref;2818}28192820=head2 commitmessage28212822this function takes a commithash and returns the commit message for that commit28232824=cut2825sub commitmessage2826{2827my$self=shift;2828my$commithash=shift;28292830die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);28312832my$db_query;2833$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2834$db_query->execute($commithash);28352836my($message) =$db_query->fetchrow_array;28372838if(defined($message) )2839{2840$message.=" "if($message=~/\n$/);2841return$message;2842}28432844my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2845shift@lineswhile($lines[0] =~/\S/);2846$message=join("",@lines);2847$message.=" "if($message=~/\n$/);2848return$message;2849}28502851=head2 gethistory28522853This function takes a filename (with path) argument and returns an arrayofarrays2854containing revision,filehash,commithash ordered by revision descending28552856=cut2857sub gethistory2858{2859my$self=shift;2860my$filename=shift;28612862my$db_query;2863$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2864$db_query->execute($filename);28652866return$db_query->fetchall_arrayref;2867}28682869=head2 gethistorydense28702871This function takes a filename (with path) argument and returns an arrayofarrays2872containing revision,filehash,commithash ordered by revision descending.28732874This version of gethistory skips deleted entries -- so it is useful for annotate.2875The 'dense' part is a reference to a '--dense' option available for git-rev-list2876and other git tools that depend on it.28772878=cut2879sub gethistorydense2880{2881my$self=shift;2882my$filename=shift;28832884my$db_query;2885$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2886$db_query->execute($filename);28872888return$db_query->fetchall_arrayref;2889}28902891=head2 in_array()28922893from Array::PAT - mimics the in_array() function2894found in PHP. Yuck but works for small arrays.28952896=cut2897sub in_array2898{2899my($check,@array) =@_;2900my$retval=0;2901foreachmy$test(@array){2902if($checkeq$test){2903$retval=1;2904}2905}2906return$retval;2907}29082909=head2 safe_pipe_capture29102911an alternative to `command` that allows input to be passed as an array2912to work around shell problems with weird characters in arguments29132914=cut2915sub safe_pipe_capture {29162917my@output;29182919if(my$pid=open my$child,'-|') {2920@output= (<$child>);2921close$childor die join(' ',@_).":$!$?";2922}else{2923exec(@_)or die"$!$?";# exec() can fail the executable can't be found2924}2925returnwantarray?@output:join('',@output);2926}29272928=head2 mangle_dirname29292930create a string from a directory name that is suitable to use as2931part of a filename, mainly by converting all chars except \w.- to _29322933=cut2934sub mangle_dirname {2935my$dirname=shift;2936return unlessdefined$dirname;29372938$dirname=~s/[^\w.-]/_/g;29392940return$dirname;2941}294229431;