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; 25use Getopt::Long qw(:config require_order no_ignore_case); 26 27my$VERSION='@@GIT_VERSION@@'; 28 29my$log= GITCVS::log->new(); 30my$cfg; 31 32my$DATE_LIST= { 33 Jan =>"01", 34 Feb =>"02", 35 Mar =>"03", 36 Apr =>"04", 37 May =>"05", 38 Jun =>"06", 39 Jul =>"07", 40 Aug =>"08", 41 Sep =>"09", 42 Oct =>"10", 43 Nov =>"11", 44 Dec =>"12", 45}; 46 47# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 48$| =1; 49 50#### Definition and mappings of functions #### 51 52my$methods= { 53'Root'=> \&req_Root, 54'Valid-responses'=> \&req_Validresponses, 55'valid-requests'=> \&req_validrequests, 56'Directory'=> \&req_Directory, 57'Entry'=> \&req_Entry, 58'Modified'=> \&req_Modified, 59'Unchanged'=> \&req_Unchanged, 60'Questionable'=> \&req_Questionable, 61'Argument'=> \&req_Argument, 62'Argumentx'=> \&req_Argument, 63'expand-modules'=> \&req_expandmodules, 64'add'=> \&req_add, 65'remove'=> \&req_remove, 66'co'=> \&req_co, 67'update'=> \&req_update, 68'ci'=> \&req_ci, 69'diff'=> \&req_diff, 70'log'=> \&req_log, 71'rlog'=> \&req_log, 72'tag'=> \&req_CATCHALL, 73'status'=> \&req_status, 74'admin'=> \&req_CATCHALL, 75'history'=> \&req_CATCHALL, 76'watchers'=> \&req_CATCHALL, 77'editors'=> \&req_CATCHALL, 78'annotate'=> \&req_annotate, 79'Global_option'=> \&req_Globaloption, 80#'annotate' => \&req_CATCHALL, 81}; 82 83############################################## 84 85 86# $state holds all the bits of information the clients sends us that could 87# potentially be useful when it comes to actually _doing_ something. 88my$state= { prependdir =>''}; 89$log->info("--------------- STARTING -----------------"); 90 91my$usage= 92"Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n". 93" --base-path <path> : Prepend to requested CVSROOT\n". 94" --strict-paths : Don't allow recursing into subdirectories\n". 95" --export-all : Don't check for gitcvs.enabled in config\n". 96" --version, -V : Print version information and exit\n". 97" --help, -h, -H : Print usage information and exit\n". 98"\n". 99"<directory> ... is a list of allowed directories. If no directories\n". 100"are given, all are allowed. This is an additional restriction, gitcvs\n". 101"access still needs to be enabled by the gitcvs.enabled config option.\n"; 102 103my@opts= ('help|h|H','version|V', 104'base-path=s','strict-paths','export-all'); 105GetOptions($state,@opts) 106or die$usage; 107 108if($state->{version}) { 109print"git-cvsserver version$VERSION\n"; 110exit; 111} 112if($state->{help}) { 113print$usage; 114exit; 115} 116 117my$TEMP_DIR= tempdir( CLEANUP =>1); 118$log->debug("Temporary directory is '$TEMP_DIR'"); 119 120$state->{method} ='ext'; 121if(@ARGV) { 122if($ARGV[0]eq'pserver') { 123$state->{method} ='pserver'; 124shift@ARGV; 125}elsif($ARGV[0]eq'server') { 126shift@ARGV; 127} 128} 129 130# everything else is a directory 131$state->{allowed_roots} = [@ARGV]; 132 133# if we are called with a pserver argument, 134# deal with the authentication cat before entering the 135# main loop 136if($state->{method}eq'pserver') { 137my$line= <STDIN>;chomp$line; 138unless($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 139die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 140} 141my$request=$1; 142$line= <STDIN>;chomp$line; 143 req_Root('root',$line)# reuse Root 144or die"E Invalid root$line\n"; 145$line= <STDIN>;chomp$line; 146unless($lineeq'anonymous') { 147print"E Only anonymous user allowed via pserver\n"; 148print"I HATE YOU\n"; 149exit1; 150} 151$line= <STDIN>;chomp$line;# validate the password? 152$line= <STDIN>;chomp$line; 153unless($lineeq"END$requestREQUEST") { 154die"E Do not understand$line-- expecting END$requestREQUEST\n"; 155} 156print"I LOVE YOU\n"; 157exit if$requesteq'VERIFICATION';# cvs login 158# and now back to our regular programme... 159} 160 161# Keep going until the client closes the connection 162while(<STDIN>) 163{ 164chomp; 165 166# Check to see if we've seen this method, and call appropriate function. 167if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 168{ 169# use the $methods hash to call the appropriate sub for this command 170#$log->info("Method : $1"); 171&{$methods->{$1}}($1,$2); 172}else{ 173# log fatal because we don't understand this function. If this happens 174# we're fairly screwed because we don't know if the client is expecting 175# a response. If it is, the client will hang, we'll hang, and the whole 176# thing will be custard. 177$log->fatal("Don't understand command$_\n"); 178die("Unknown command$_"); 179} 180} 181 182$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 183$log->info("--------------- FINISH -----------------"); 184 185# Magic catchall method. 186# This is the method that will handle all commands we haven't yet 187# implemented. It simply sends a warning to the log file indicating a 188# command that hasn't been implemented has been invoked. 189sub req_CATCHALL 190{ 191my($cmd,$data) =@_; 192$log->warn("Unhandled command : req_$cmd:$data"); 193} 194 195 196# Root pathname \n 197# Response expected: no. Tell the server which CVSROOT to use. Note that 198# pathname is a local directory and not a fully qualified CVSROOT variable. 199# pathname must already exist; if creating a new root, use the init 200# request, not Root. pathname does not include the hostname of the server, 201# how to access the server, etc.; by the time the CVS protocol is in use, 202# connection, authentication, etc., are already taken care of. The Root 203# request must be sent only once, and it must be sent before any requests 204# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 205sub req_Root 206{ 207my($cmd,$data) =@_; 208$log->debug("req_Root :$data"); 209 210unless($data=~ m#^/#) { 211print"error 1 Root must be an absolute pathname\n"; 212return0; 213} 214 215if($state->{CVSROOT} 216&& ($state->{CVSROOT}ne$data)) { 217print"error 1 Conflicting roots specified\n"; 218return0; 219} 220 221$state->{CVSROOT} =$state->{'base-path'} ||''; 222$state->{CVSROOT} =~ s#/+$##; 223$state->{CVSROOT} .=$data; 224 225$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 226 227if(@{$state->{allowed_roots}}) { 228my$allowed=0; 229foreachmy$dir(@{$state->{allowed_roots}}) { 230next unless$dir=~ m#^/#; 231$dir=~ s#/+$##; 232if($state->{'strict-paths'}) { 233if($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { 234$allowed=1; 235last; 236} 237}elsif($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { 238$allowed=1; 239last; 240} 241} 242 243unless($allowed) { 244print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 245print"E\n"; 246print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 247return0; 248} 249} 250 251unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 252print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 253print"E\n"; 254print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 255return0; 256} 257 258my@gitvars=`git-config -l`; 259if($?) { 260print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 261print"E\n"; 262print"error 1 - problem executing git-config\n"; 263return0; 264} 265foreachmy$line(@gitvars) 266{ 267next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 268unless($2) { 269$cfg->{$1}{$3} =$4; 270}else{ 271$cfg->{$1}{$2}{$3} =$4; 272} 273} 274 275my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 276||$cfg->{gitcvs}{enabled}); 277unless($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i) { 278print"E GITCVS emulation needs to be enabled on this repo\n"; 279print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 280print"E\n"; 281print"error 1 GITCVS emulation disabled\n"; 282return0; 283} 284 285my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 286if($logfile) 287{ 288$log->setfile($logfile); 289}else{ 290$log->nofile(); 291} 292 293return1; 294} 295 296# Global_option option \n 297# Response expected: no. Transmit one of the global options `-q', `-Q', 298# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 299# variations (such as combining of options) are allowed. For graceful 300# handling of valid-requests, it is probably better to make new global 301# options separate requests, rather than trying to add them to this 302# request. 303sub req_Globaloption 304{ 305my($cmd,$data) =@_; 306$log->debug("req_Globaloption :$data"); 307$state->{globaloptions}{$data} =1; 308} 309 310# Valid-responses request-list \n 311# Response expected: no. Tell the server what responses the client will 312# accept. request-list is a space separated list of tokens. 313sub req_Validresponses 314{ 315my($cmd,$data) =@_; 316$log->debug("req_Validresponses :$data"); 317 318# TODO : re-enable this, currently it's not particularly useful 319#$state->{validresponses} = [ split /\s+/, $data ]; 320} 321 322# valid-requests \n 323# Response expected: yes. Ask the server to send back a Valid-requests 324# response. 325sub req_validrequests 326{ 327my($cmd,$data) =@_; 328 329$log->debug("req_validrequests"); 330 331$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 332$log->debug("SEND : ok"); 333 334print"Valid-requests ".join(" ",keys%$methods) ."\n"; 335print"ok\n"; 336} 337 338# Directory local-directory \n 339# Additional data: repository \n. Response expected: no. Tell the server 340# what directory to use. The repository should be a directory name from a 341# previous server response. Note that this both gives a default for Entry 342# and Modified and also for ci and the other commands; normal usage is to 343# send Directory for each directory in which there will be an Entry or 344# Modified, and then a final Directory for the original directory, then the 345# command. The local-directory is relative to the top level at which the 346# command is occurring (i.e. the last Directory which is sent before the 347# command); to indicate that top level, `.' should be sent for 348# local-directory. 349sub req_Directory 350{ 351my($cmd,$data) =@_; 352 353my$repository= <STDIN>; 354chomp$repository; 355 356 357$state->{localdir} =$data; 358$state->{repository} =$repository; 359$state->{path} =$repository; 360$state->{path} =~s/^$state->{CVSROOT}\///; 361$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 362$state->{path} .="/"if($state->{path} =~ /\S/ ); 363 364$state->{directory} =$state->{localdir}; 365$state->{directory} =""if($state->{directory}eq"."); 366$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 367 368if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 369{ 370$log->info("Setting prepend to '$state->{path}'"); 371$state->{prependdir} =$state->{path}; 372foreachmy$entry(keys%{$state->{entries}} ) 373{ 374$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 375delete$state->{entries}{$entry}; 376} 377} 378 379if(defined($state->{prependdir} ) ) 380{ 381$log->debug("Prepending '$state->{prependdir}' to state|directory"); 382$state->{directory} =$state->{prependdir} .$state->{directory} 383} 384$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 385} 386 387# Entry entry-line \n 388# Response expected: no. Tell the server what version of a file is on the 389# local machine. The name in entry-line is a name relative to the directory 390# most recently specified with Directory. If the user is operating on only 391# some files in a directory, Entry requests for only those files need be 392# included. If an Entry request is sent without Modified, Is-modified, or 393# Unchanged, it means the file is lost (does not exist in the working 394# directory). If both Entry and one of Modified, Is-modified, or Unchanged 395# are sent for the same file, Entry must be sent first. For a given file, 396# one can send Modified, Is-modified, or Unchanged, but not more than one 397# of these three. 398sub req_Entry 399{ 400my($cmd,$data) =@_; 401 402#$log->debug("req_Entry : $data"); 403 404my@data=split(/\//,$data); 405 406$state->{entries}{$state->{directory}.$data[1]} = { 407 revision =>$data[2], 408 conflict =>$data[3], 409 options =>$data[4], 410 tag_or_date =>$data[5], 411}; 412 413$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 414} 415 416# Questionable filename \n 417# Response expected: no. Additional data: no. Tell the server to check 418# whether filename should be ignored, and if not, next time the server 419# sends responses, send (in a M response) `?' followed by the directory and 420# filename. filename must not contain `/'; it needs to be a file in the 421# directory named by the most recent Directory request. 422sub req_Questionable 423{ 424my($cmd,$data) =@_; 425 426$log->debug("req_Questionable :$data"); 427$state->{entries}{$state->{directory}.$data}{questionable} =1; 428} 429 430# add \n 431# Response expected: yes. Add a file or directory. This uses any previous 432# Argument, Directory, Entry, or Modified requests, if they have been sent. 433# The last Directory sent specifies the working directory at the time of 434# the operation. To add a directory, send the directory to be added using 435# Directory and Argument requests. 436sub req_add 437{ 438my($cmd,$data) =@_; 439 440 argsplit("add"); 441 442my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 443$updater->update(); 444 445 argsfromdir($updater); 446 447my$addcount=0; 448 449foreachmy$filename( @{$state->{args}} ) 450{ 451$filename= filecleanup($filename); 452 453my$meta=$updater->getmeta($filename); 454my$wrev= revparse($filename); 455 456if($wrev&&$meta&& ($wrev<0)) 457{ 458# previously removed file, add back 459$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 460 461print"MT +updated\n"; 462print"MT text U\n"; 463print"MT fname$filename\n"; 464print"MT newline\n"; 465print"MT -updated\n"; 466 467unless($state->{globaloptions}{-n} ) 468{ 469my($filepart,$dirpart) = filenamesplit($filename,1); 470 471print"Created$dirpart\n"; 472print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 473 474# this is an "entries" line 475my$kopts= kopts_from_path($filepart); 476$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 477print"/$filepart/1.$meta->{revision}//$kopts/\n"; 478# permissions 479$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 480print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 481# transmit file 482 transmitfile($meta->{filehash}); 483} 484 485next; 486} 487 488unless(defined($state->{entries}{$filename}{modified_filename} ) ) 489{ 490print"E cvs add: nothing known about `$filename'\n"; 491next; 492} 493# TODO : check we're not squashing an already existing file 494if(defined($state->{entries}{$filename}{revision} ) ) 495{ 496print"E cvs add: `$filename' has already been entered\n"; 497next; 498} 499 500my($filepart,$dirpart) = filenamesplit($filename,1); 501 502print"E cvs add: scheduling file `$filename' for addition\n"; 503 504print"Checked-in$dirpart\n"; 505print"$filename\n"; 506my$kopts= kopts_from_path($filepart); 507print"/$filepart/0//$kopts/\n"; 508 509$addcount++; 510} 511 512if($addcount==1) 513{ 514print"E cvs add: use `cvs commit' to add this file permanently\n"; 515} 516elsif($addcount>1) 517{ 518print"E cvs add: use `cvs commit' to add these files permanently\n"; 519} 520 521print"ok\n"; 522} 523 524# remove \n 525# Response expected: yes. Remove a file. This uses any previous Argument, 526# Directory, Entry, or Modified requests, if they have been sent. The last 527# Directory sent specifies the working directory at the time of the 528# operation. Note that this request does not actually do anything to the 529# repository; the only effect of a successful remove request is to supply 530# the client with a new entries line containing `-' to indicate a removed 531# file. In fact, the client probably could perform this operation without 532# contacting the server, although using remove may cause the server to 533# perform a few more checks. The client sends a subsequent ci request to 534# actually record the removal in the repository. 535sub req_remove 536{ 537my($cmd,$data) =@_; 538 539 argsplit("remove"); 540 541# Grab a handle to the SQLite db and do any necessary updates 542my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 543$updater->update(); 544 545#$log->debug("add state : " . Dumper($state)); 546 547my$rmcount=0; 548 549foreachmy$filename( @{$state->{args}} ) 550{ 551$filename= filecleanup($filename); 552 553if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 554{ 555print"E cvs remove: file `$filename' still in working directory\n"; 556next; 557} 558 559my$meta=$updater->getmeta($filename); 560my$wrev= revparse($filename); 561 562unless(defined($wrev) ) 563{ 564print"E cvs remove: nothing known about `$filename'\n"; 565next; 566} 567 568if(defined($wrev)and$wrev<0) 569{ 570print"E cvs remove: file `$filename' already scheduled for removal\n"; 571next; 572} 573 574unless($wrev==$meta->{revision} ) 575{ 576# TODO : not sure if the format of this message is quite correct. 577print"E cvs remove: Up to date check failed for `$filename'\n"; 578next; 579} 580 581 582my($filepart,$dirpart) = filenamesplit($filename,1); 583 584print"E cvs remove: scheduling `$filename' for removal\n"; 585 586print"Checked-in$dirpart\n"; 587print"$filename\n"; 588my$kopts= kopts_from_path($filepart); 589print"/$filepart/-1.$wrev//$kopts/\n"; 590 591$rmcount++; 592} 593 594if($rmcount==1) 595{ 596print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 597} 598elsif($rmcount>1) 599{ 600print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 601} 602 603print"ok\n"; 604} 605 606# Modified filename \n 607# Response expected: no. Additional data: mode, \n, file transmission. Send 608# the server a copy of one locally modified file. filename is a file within 609# the most recent directory sent with Directory; it must not contain `/'. 610# If the user is operating on only some files in a directory, only those 611# files need to be included. This can also be sent without Entry, if there 612# is no entry for the file. 613sub req_Modified 614{ 615my($cmd,$data) =@_; 616 617my$mode= <STDIN>; 618chomp$mode; 619my$size= <STDIN>; 620chomp$size; 621 622# Grab config information 623my$blocksize=8192; 624my$bytesleft=$size; 625my$tmp; 626 627# Get a filehandle/name to write it to 628my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 629 630# Loop over file data writing out to temporary file. 631while($bytesleft) 632{ 633$blocksize=$bytesleftif($bytesleft<$blocksize); 634read STDIN,$tmp,$blocksize; 635print$fh $tmp; 636$bytesleft-=$blocksize; 637} 638 639close$fh; 640 641# Ensure we have something sensible for the file mode 642if($mode=~/u=(\w+)/) 643{ 644$mode=$1; 645}else{ 646$mode="rw"; 647} 648 649# Save the file data in $state 650$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 651$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 652$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 653$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 654 655 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 656} 657 658# Unchanged filename\n 659# Response expected: no. Tell the server that filename has not been 660# modified in the checked out directory. The filename is a file within the 661# most recent directory sent with Directory; it must not contain `/'. 662sub req_Unchanged 663{ 664 my ($cmd,$data) =@_; 665 666$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 667 668 #$log->debug("req_Unchanged :$data"); 669} 670 671# Argument text\n 672# Response expected: no. Save argument for use in a subsequent command. 673# Arguments accumulate until an argument-using command is given, at which 674# point they are forgotten. 675# Argumentx text\n 676# Response expected: no. Append\nfollowed by text to the current argument 677# being saved. 678sub req_Argument 679{ 680 my ($cmd,$data) =@_; 681 682 # Argumentx means: append to last Argument (with a newline in front) 683 684$log->debug("$cmd:$data"); 685 686 if ($cmdeq 'Argumentx') { 687 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 688 } else { 689 push @{$state->{arguments}},$data; 690 } 691} 692 693# expand-modules\n 694# Response expected: yes. Expand the modules which are specified in the 695# arguments. Returns the data in Module-expansion responses. Note that the 696# server can assume that this is checkout or export, not rtag or rdiff; the 697# latter do not access the working directory and thus have no need to 698# expand modules on the client side. Expand may not be the best word for 699# what this request does. It does not necessarily tell you all the files 700# contained in a module, for example. Basically it is a way of telling you 701# which working directories the server needs to know about in order to 702# handle a checkout of the specified modules. For example, suppose that the 703# server has a module defined by 704# aliasmodule -a 1dir 705# That is, one can check out aliasmodule and it will take 1dir in the 706# repository and check it out to 1dir in the working directory. Now suppose 707# the client already has this module checked out and is planning on using 708# the co request to update it. Without using expand-modules, the client 709# would have two bad choices: it could either send information about all 710# working directories under the current directory, which could be 711# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 712# stands for 1dir, and neglect to send information for 1dir, which would 713# lead to incorrect operation. With expand-modules, the client would first 714# ask for the module to be expanded: 715sub req_expandmodules 716{ 717 my ($cmd,$data) =@_; 718 719 argsplit(); 720 721$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 722 723 unless ( ref$state->{arguments} eq "ARRAY" ) 724 { 725 print "ok\n"; 726 return; 727 } 728 729 foreach my$module( @{$state->{arguments}} ) 730 { 731$log->debug("SEND : Module-expansion$module"); 732 print "Module-expansion$module\n"; 733 } 734 735 print "ok\n"; 736 statecleanup(); 737} 738 739# co\n 740# Response expected: yes. Get files from the repository. This uses any 741# previous Argument, Directory, Entry, or Modified requests, if they have 742# been sent. Arguments to this command are module names; the client cannot 743# know what directories they correspond to except by (1) just sending the 744# co request, and then seeing what directory names the server sends back in 745# its responses, and (2) the expand-modules request. 746sub req_co 747{ 748 my ($cmd,$data) =@_; 749 750 argsplit("co"); 751 752 my$module=$state->{args}[0]; 753 my$checkout_path=$module; 754 755 # use the user specified directory if we're given it 756$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 757 758$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 759 760$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 761 762$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 763 764# Grab a handle to the SQLite db and do any necessary updates 765my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 766$updater->update(); 767 768$checkout_path=~ s|/$||;# get rid of trailing slashes 769 770# Eclipse seems to need the Clear-sticky command 771# to prepare the 'Entries' file for the new directory. 772print"Clear-sticky$checkout_path/\n"; 773print$state->{CVSROOT} ."/$module/\n"; 774print"Clear-static-directory$checkout_path/\n"; 775print$state->{CVSROOT} ."/$module/\n"; 776print"Clear-sticky$checkout_path/\n";# yes, twice 777print$state->{CVSROOT} ."/$module/\n"; 778print"Template$checkout_path/\n"; 779print$state->{CVSROOT} ."/$module/\n"; 780print"0\n"; 781 782# instruct the client that we're checking out to $checkout_path 783print"E cvs checkout: Updating$checkout_path\n"; 784 785my%seendirs= (); 786my$lastdir=''; 787 788# recursive 789sub prepdir { 790my($dir,$repodir,$remotedir,$seendirs) =@_; 791my$parent= dirname($dir); 792$dir=~ s|/+$||; 793$repodir=~ s|/+$||; 794$remotedir=~ s|/+$||; 795$parent=~ s|/+$||; 796$log->debug("announcedir$dir,$repodir,$remotedir"); 797 798if($parenteq'.'||$parenteq'./') { 799$parent=''; 800} 801# recurse to announce unseen parents first 802if(length($parent) && !exists($seendirs->{$parent})) { 803 prepdir($parent,$repodir,$remotedir,$seendirs); 804} 805# Announce that we are going to modify at the parent level 806if($parent) { 807print"E cvs checkout: Updating$remotedir/$parent\n"; 808}else{ 809print"E cvs checkout: Updating$remotedir\n"; 810} 811print"Clear-sticky$remotedir/$parent/\n"; 812print"$repodir/$parent/\n"; 813 814print"Clear-static-directory$remotedir/$dir/\n"; 815print"$repodir/$dir/\n"; 816print"Clear-sticky$remotedir/$parent/\n";# yes, twice 817print"$repodir/$parent/\n"; 818print"Template$remotedir/$dir/\n"; 819print"$repodir/$dir/\n"; 820print"0\n"; 821 822$seendirs->{$dir} =1; 823} 824 825foreachmy$git( @{$updater->gethead} ) 826{ 827# Don't want to check out deleted files 828next if($git->{filehash}eq"deleted"); 829 830($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 831 832if(length($git->{dir}) &&$git->{dir}ne'./' 833&&$git->{dir}ne$lastdir) { 834unless(exists($seendirs{$git->{dir}})) { 835 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 836$checkout_path, \%seendirs); 837$lastdir=$git->{dir}; 838$seendirs{$git->{dir}} =1; 839} 840print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 841} 842 843# modification time of this file 844print"Mod-time$git->{modified}\n"; 845 846# print some information to the client 847if(defined($git->{dir} )and$git->{dir}ne"./") 848{ 849print"M U$checkout_path/$git->{dir}$git->{name}\n"; 850}else{ 851print"M U$checkout_path/$git->{name}\n"; 852} 853 854# instruct client we're sending a file to put in this path 855print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 856 857print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 858 859# this is an "entries" line 860my$kopts= kopts_from_path($git->{name}); 861print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 862# permissions 863print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 864 865# transmit file 866 transmitfile($git->{filehash}); 867} 868 869print"ok\n"; 870 871 statecleanup(); 872} 873 874# update \n 875# Response expected: yes. Actually do a cvs update command. This uses any 876# previous Argument, Directory, Entry, or Modified requests, if they have 877# been sent. The last Directory sent specifies the working directory at the 878# time of the operation. The -I option is not used--files which the client 879# can decide whether to ignore are not mentioned and the client sends the 880# Questionable request for others. 881sub req_update 882{ 883my($cmd,$data) =@_; 884 885$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 886 887 argsplit("update"); 888 889# 890# It may just be a client exploring the available heads/modules 891# in that case, list them as top level directories and leave it 892# at that. Eclipse uses this technique to offer you a list of 893# projects (heads in this case) to checkout. 894# 895if($state->{module}eq'') { 896print"E cvs update: Updating .\n"; 897opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 898while(my$head=readdir(HEADS)) { 899if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 900print"E cvs update: New directory `$head'\n"; 901} 902} 903closedir HEADS; 904print"ok\n"; 905return1; 906} 907 908 909# Grab a handle to the SQLite db and do any necessary updates 910my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 911 912$updater->update(); 913 914 argsfromdir($updater); 915 916#$log->debug("update state : " . Dumper($state)); 917 918# foreach file specified on the command line ... 919foreachmy$filename( @{$state->{args}} ) 920{ 921$filename= filecleanup($filename); 922 923$log->debug("Processing file$filename"); 924 925# if we have a -C we should pretend we never saw modified stuff 926if(exists($state->{opt}{C} ) ) 927{ 928delete$state->{entries}{$filename}{modified_hash}; 929delete$state->{entries}{$filename}{modified_filename}; 930$state->{entries}{$filename}{unchanged} =1; 931} 932 933my$meta; 934if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 935{ 936$meta=$updater->getmeta($filename,$1); 937}else{ 938$meta=$updater->getmeta($filename); 939} 940 941if( !defined$meta) 942{ 943$meta= { 944 name =>$filename, 945 revision =>0, 946 filehash =>'added' 947}; 948} 949 950my$oldmeta=$meta; 951 952my$wrev= revparse($filename); 953 954# If the working copy is an old revision, lets get that version too for comparison. 955if(defined($wrev)and$wrev!=$meta->{revision} ) 956{ 957$oldmeta=$updater->getmeta($filename,$wrev); 958} 959 960#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 961 962# Files are up to date if the working copy and repo copy have the same revision, 963# and the working copy is unmodified _and_ the user hasn't specified -C 964next if(defined($wrev) 965and defined($meta->{revision}) 966and$wrev==$meta->{revision} 967and$state->{entries}{$filename}{unchanged} 968and not exists($state->{opt}{C} ) ); 969 970# If the working copy and repo copy have the same revision, 971# but the working copy is modified, tell the client it's modified 972if(defined($wrev) 973and defined($meta->{revision}) 974and$wrev==$meta->{revision} 975and defined($state->{entries}{$filename}{modified_hash}) 976and not exists($state->{opt}{C} ) ) 977{ 978$log->info("Tell the client the file is modified"); 979print"MT text M\n"; 980print"MT fname$filename\n"; 981print"MT newline\n"; 982next; 983} 984 985if($meta->{filehash}eq"deleted") 986{ 987my($filepart,$dirpart) = filenamesplit($filename,1); 988 989$log->info("Removing '$filename' from working copy (no longer in the repo)"); 990 991print"E cvs update: `$filename' is no longer in the repository\n"; 992# Don't want to actually _DO_ the update if -n specified 993unless($state->{globaloptions}{-n} ) { 994print"Removed$dirpart\n"; 995print"$filepart\n"; 996} 997} 998elsif(not defined($state->{entries}{$filename}{modified_hash} ) 999or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1000or$meta->{filehash}eq'added')1001{1002# normal update, just send the new revision (either U=Update,1003# or A=Add, or R=Remove)1004if(defined($wrev) &&$wrev<0)1005{1006$log->info("Tell the client the file is scheduled for removal");1007print"MT text R\n";1008print"MT fname$filename\n";1009print"MT newline\n";1010next;1011}1012elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) )1013{1014$log->info("Tell the client the file is scheduled for addition");1015print"MT text A\n";1016print"MT fname$filename\n";1017print"MT newline\n";1018next;10191020}1021else{1022$log->info("Updating '$filename' to ".$meta->{revision});1023print"MT +updated\n";1024print"MT text U\n";1025print"MT fname$filename\n";1026print"MT newline\n";1027print"MT -updated\n";1028}10291030my($filepart,$dirpart) = filenamesplit($filename,1);10311032# Don't want to actually _DO_ the update if -n specified1033unless($state->{globaloptions}{-n} )1034{1035if(defined($wrev) )1036{1037# instruct client we're sending a file to put in this path as a replacement1038print"Update-existing$dirpart\n";1039$log->debug("Updating existing file 'Update-existing$dirpart'");1040}else{1041# instruct client we're sending a file to put in this path as a new file1042print"Clear-static-directory$dirpart\n";1043print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1044print"Clear-sticky$dirpart\n";1045print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";10461047$log->debug("Creating new file 'Created$dirpart'");1048print"Created$dirpart\n";1049}1050print$state->{CVSROOT} ."/$state->{module}/$filename\n";10511052# this is an "entries" line1053my$kopts= kopts_from_path($filepart);1054$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1055print"/$filepart/1.$meta->{revision}//$kopts/\n";10561057# permissions1058$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1059print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10601061# transmit file1062 transmitfile($meta->{filehash});1063}1064}else{1065$log->info("Updating '$filename'");1066my($filepart,$dirpart) = filenamesplit($meta->{name},1);10671068my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/";10691070chdir$dir;1071my$file_local=$filepart.".mine";1072system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1073my$file_old=$filepart.".".$oldmeta->{revision};1074 transmitfile($oldmeta->{filehash},$file_old);1075my$file_new=$filepart.".".$meta->{revision};1076 transmitfile($meta->{filehash},$file_new);10771078# we need to merge with the local changes ( M=successful merge, C=conflict merge )1079$log->info("Merging$file_local,$file_old,$file_new");1080print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";10811082$log->debug("Temporary directory for merge is$dir");10831084my$return=system("git","merge-file",$file_local,$file_old,$file_new);1085$return>>=8;10861087if($return==0)1088{1089$log->info("Merged successfully");1090print"M M$filename\n";1091$log->debug("Merged$dirpart");10921093# Don't want to actually _DO_ the update if -n specified1094unless($state->{globaloptions}{-n} )1095{1096print"Merged$dirpart\n";1097$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1098print$state->{CVSROOT} ."/$state->{module}/$filename\n";1099my$kopts= kopts_from_path($filepart);1100$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1101print"/$filepart/1.$meta->{revision}//$kopts/\n";1102}1103}1104elsif($return==1)1105{1106$log->info("Merged with conflicts");1107print"E cvs update: conflicts found in$filename\n";1108print"M C$filename\n";11091110# Don't want to actually _DO_ the update if -n specified1111unless($state->{globaloptions}{-n} )1112{1113print"Merged$dirpart\n";1114print$state->{CVSROOT} ."/$state->{module}/$filename\n";1115my$kopts= kopts_from_path($filepart);1116print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1117}1118}1119else1120{1121$log->warn("Merge failed");1122next;1123}11241125# Don't want to actually _DO_ the update if -n specified1126unless($state->{globaloptions}{-n} )1127{1128# permissions1129$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1130print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";11311132# transmit file, format is single integer on a line by itself (file1133# size) followed by the file contents1134# TODO : we should copy files in blocks1135my$data=`cat$file_local`;1136$log->debug("File size : " . length($data));1137 print length($data) . "\n";1138 print$data;1139 }11401141 chdir "/";1142 }11431144 }11451146 print "ok\n";1147}11481149sub req_ci1150{1151 my ($cmd,$data) =@_;11521153 argsplit("ci");11541155 #$log->debug("State : " . Dumper($state));11561157$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));11581159 if ($state->{method} eq 'pserver')1160 {1161 print "error 1 pserver access cannot commit\n";1162 exit;1163 }11641165 if ( -e$state->{CVSROOT} . "/index" )1166 {1167$log->warn("file 'index' already exists in the git repository");1168 print "error 1 Index already exists in git repo\n";1169 exit;1170 }11711172 # Grab a handle to the SQLite db and do any necessary updates1173 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1174$updater->update();11751176 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1177 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1178$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");11791180$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1181$ENV{GIT_INDEX_FILE} =$file_index;11821183 # Remember where the head was at the beginning.1184 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1185 chomp$parenthash;1186 if ($parenthash!~ /^[0-9a-f]{40}$/) {1187 print "error 1 pserver cannot find the current HEAD of module";1188 exit;1189 }11901191 chdir$tmpdir;11921193 # populate the temporary index based1194 system("git-read-tree",$parenthash);1195 unless ($?== 0)1196 {1197 die "Error running git-read-tree$state->{module}$file_index$!";1198 }1199$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");12001201 my@committedfiles= ();1202 my%oldmeta;12031204 # foreach file specified on the command line ...1205 foreach my$filename( @{$state->{args}} )1206 {1207 my$committedfile=$filename;1208$filename= filecleanup($filename);12091210 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );12111212 my$meta=$updater->getmeta($filename);1213$oldmeta{$filename} =$meta;12141215 my$wrev= revparse($filename);12161217 my ($filepart,$dirpart) = filenamesplit($filename);12181219 # do a checkout of the file if it part of this tree1220 if ($wrev) {1221 system('git-checkout-index', '-f', '-u',$filename);1222 unless ($?== 0) {1223 die "Error running git-checkout-index -f -u$filename:$!";1224 }1225 }12261227 my$addflag= 0;1228 my$rmflag= 0;1229$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1230$addflag= 1 unless ( -e$filename);12311232 # Do up to date checking1233 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1234 {1235 # fail everything if an up to date check fails1236 print "error 1 Up to date check failed for$filename\n";1237 chdir "/";1238 exit;1239 }12401241 push@committedfiles,$committedfile;1242$log->info("Committing$filename");12431244 system("mkdir","-p",$dirpart) unless ( -d$dirpart);12451246 unless ($rmflag)1247 {1248$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1249 rename$state->{entries}{$filename}{modified_filename},$filename;12501251 # Calculate modes to remove1252 my$invmode= "";1253 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }12541255$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1256 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1257 }12581259 if ($rmflag)1260 {1261$log->info("Removing file '$filename'");1262 unlink($filename);1263 system("git-update-index", "--remove",$filename);1264 }1265 elsif ($addflag)1266 {1267$log->info("Adding file '$filename'");1268 system("git-update-index", "--add",$filename);1269 } else {1270$log->info("Updating file '$filename'");1271 system("git-update-index",$filename);1272 }1273 }12741275 unless ( scalar(@committedfiles) > 0 )1276 {1277 print "E No files to commit\n";1278 print "ok\n";1279 chdir "/";1280 return;1281 }12821283 my$treehash= `git-write-tree`;1284 chomp$treehash;12851286$log->debug("Treehash :$treehash, Parenthash :$parenthash");12871288 # write our commit message out if we have one ...1289 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1290 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1291 print$msg_fh"\n\nvia git-CVS emulator\n";1292 close$msg_fh;12931294 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1295chomp($commithash);1296$log->info("Commit hash :$commithash");12971298unless($commithash=~/[a-zA-Z0-9]{40}/)1299{1300$log->warn("Commit failed (Invalid commit hash)");1301print"error 1 Commit failed (unknown reason)\n";1302chdir"/";1303exit;1304}13051306# Check that this is allowed, just as we would with a receive-pack1307my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1308$parenthash,$commithash);1309if( -x $cmd[0] ) {1310unless(system(@cmd) ==0)1311{1312$log->warn("Commit failed (update hook declined to update ref)");1313print"error 1 Commit failed (update hook declined)\n";1314chdir"/";1315exit;1316}1317}13181319if(system(qw(git update-ref -m),"cvsserver ci",1320"refs/heads/$state->{module}",$commithash,$parenthash)) {1321$log->warn("update-ref for$state->{module} failed.");1322print"error 1 Cannot commit -- update first\n";1323exit;1324}13251326$updater->update();13271328# foreach file specified on the command line ...1329foreachmy$filename(@committedfiles)1330{1331$filename= filecleanup($filename);13321333my$meta=$updater->getmeta($filename);1334unless(defined$meta->{revision}) {1335$meta->{revision} =1;1336}13371338my($filepart,$dirpart) = filenamesplit($filename,1);13391340$log->debug("Checked-in$dirpart:$filename");13411342print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1343if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1344{1345print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1346print"Remove-entry$dirpart\n";1347print"$filename\n";1348}else{1349if($meta->{revision} ==1) {1350print"M initial revision: 1.1\n";1351}else{1352print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1353}1354print"Checked-in$dirpart\n";1355print"$filename\n";1356my$kopts= kopts_from_path($filepart);1357print"/$filepart/1.$meta->{revision}//$kopts/\n";1358}1359}13601361chdir"/";1362print"ok\n";1363}13641365sub req_status1366{1367my($cmd,$data) =@_;13681369 argsplit("status");13701371$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1372#$log->debug("status state : " . Dumper($state));13731374# Grab a handle to the SQLite db and do any necessary updates1375my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1376$updater->update();13771378# if no files were specified, we need to work out what files we should be providing status on ...1379 argsfromdir($updater);13801381# foreach file specified on the command line ...1382foreachmy$filename( @{$state->{args}} )1383{1384$filename= filecleanup($filename);13851386my$meta=$updater->getmeta($filename);1387my$oldmeta=$meta;13881389my$wrev= revparse($filename);13901391# If the working copy is an old revision, lets get that version too for comparison.1392if(defined($wrev)and$wrev!=$meta->{revision} )1393{1394$oldmeta=$updater->getmeta($filename,$wrev);1395}13961397# TODO : All possible statuses aren't yet implemented1398my$status;1399# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1400$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1401and1402( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1403or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1404);14051406# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1407$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1408and1409($state->{entries}{$filename}{unchanged}1410or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1411);14121413# Need checkout if it exists in the repo but doesn't have a working copy1414$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );14151416# Locally modified if working copy and repo copy have the same revision but there are local changes1417$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );14181419# Needs Merge if working copy revision is less than repo copy and there are local changes1420$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );14211422$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1423$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1424$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1425$status||="File had conflicts on merge"if(0);14261427$status||="Unknown";14281429print"M ===================================================================\n";1430print"M File:$filename\tStatus:$status\n";1431if(defined($state->{entries}{$filename}{revision}) )1432{1433print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1434}else{1435print"M Working revision:\tNo entry for$filename\n";1436}1437if(defined($meta->{revision}) )1438{1439print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1440print"M Sticky Tag:\t\t(none)\n";1441print"M Sticky Date:\t\t(none)\n";1442print"M Sticky Options:\t\t(none)\n";1443}else{1444print"M Repository revision:\tNo revision control file\n";1445}1446print"M\n";1447}14481449print"ok\n";1450}14511452sub req_diff1453{1454my($cmd,$data) =@_;14551456 argsplit("diff");14571458$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1459#$log->debug("status state : " . Dumper($state));14601461my($revision1,$revision2);1462if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1463{1464$revision1=$state->{opt}{r}[0];1465$revision2=$state->{opt}{r}[1];1466}else{1467$revision1=$state->{opt}{r};1468}14691470$revision1=~s/^1\.//if(defined($revision1) );1471$revision2=~s/^1\.//if(defined($revision2) );14721473$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );14741475# Grab a handle to the SQLite db and do any necessary updates1476my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1477$updater->update();14781479# if no files were specified, we need to work out what files we should be providing status on ...1480 argsfromdir($updater);14811482# foreach file specified on the command line ...1483foreachmy$filename( @{$state->{args}} )1484{1485$filename= filecleanup($filename);14861487my($fh,$file1,$file2,$meta1,$meta2,$filediff);14881489my$wrev= revparse($filename);14901491# We need _something_ to diff against1492next unless(defined($wrev) );14931494# if we have a -r switch, use it1495if(defined($revision1) )1496{1497(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1498$meta1=$updater->getmeta($filename,$revision1);1499unless(defined($meta1)and$meta1->{filehash}ne"deleted")1500{1501print"E File$filenameat revision 1.$revision1doesn't exist\n";1502next;1503}1504 transmitfile($meta1->{filehash},$file1);1505}1506# otherwise we just use the working copy revision1507else1508{1509(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1510$meta1=$updater->getmeta($filename,$wrev);1511 transmitfile($meta1->{filehash},$file1);1512}15131514# if we have a second -r switch, use it too1515if(defined($revision2) )1516{1517(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1518$meta2=$updater->getmeta($filename,$revision2);15191520unless(defined($meta2)and$meta2->{filehash}ne"deleted")1521{1522print"E File$filenameat revision 1.$revision2doesn't exist\n";1523next;1524}15251526 transmitfile($meta2->{filehash},$file2);1527}1528# otherwise we just use the working copy1529else1530{1531$file2=$state->{entries}{$filename}{modified_filename};1532}15331534# if we have been given -r, and we don't have a $file2 yet, lets get one1535if(defined($revision1)and not defined($file2) )1536{1537(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1538$meta2=$updater->getmeta($filename,$wrev);1539 transmitfile($meta2->{filehash},$file2);1540}15411542# We need to have retrieved something useful1543next unless(defined($meta1) );15441545# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1546next if(not defined($meta2)and$wrev==$meta1->{revision}1547and1548( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1549or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1550);15511552# Apparently we only show diffs for locally modified files1553next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );15541555print"M Index:$filename\n";1556print"M ===================================================================\n";1557print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1558print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1559print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1560print"M diff ";1561foreachmy$opt(keys%{$state->{opt}} )1562{1563if(ref$state->{opt}{$opt}eq"ARRAY")1564{1565foreachmy$value( @{$state->{opt}{$opt}} )1566{1567print"-$opt$value";1568}1569}else{1570print"-$opt";1571print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1572}1573}1574print"$filename\n";15751576$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));15771578($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);15791580if(exists$state->{opt}{u} )1581{1582system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1583}else{1584system("diff$file1$file2>$filediff");1585}15861587while( <$fh> )1588{1589print"M$_";1590}1591close$fh;1592}15931594print"ok\n";1595}15961597sub req_log1598{1599my($cmd,$data) =@_;16001601 argsplit("log");16021603$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1604#$log->debug("log state : " . Dumper($state));16051606my($minrev,$maxrev);1607if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1608{1609my$control=$2;1610$minrev=$1;1611$maxrev=$3;1612$minrev=~s/^1\.//if(defined($minrev) );1613$maxrev=~s/^1\.//if(defined($maxrev) );1614$minrev++if(defined($minrev)and$controleq"::");1615}16161617# Grab a handle to the SQLite db and do any necessary updates1618my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1619$updater->update();16201621# if no files were specified, we need to work out what files we should be providing status on ...1622 argsfromdir($updater);16231624# foreach file specified on the command line ...1625foreachmy$filename( @{$state->{args}} )1626{1627$filename= filecleanup($filename);16281629my$headmeta=$updater->getmeta($filename);16301631my$revisions=$updater->getlog($filename);1632my$totalrevisions=scalar(@$revisions);16331634if(defined($minrev) )1635{1636$log->debug("Removing revisions less than$minrev");1637while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1638{1639pop@$revisions;1640}1641}1642if(defined($maxrev) )1643{1644$log->debug("Removing revisions greater than$maxrev");1645while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1646{1647shift@$revisions;1648}1649}16501651next unless(scalar(@$revisions) );16521653print"M\n";1654print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1655print"M Working file:$filename\n";1656print"M head: 1.$headmeta->{revision}\n";1657print"M branch:\n";1658print"M locks: strict\n";1659print"M access list:\n";1660print"M symbolic names:\n";1661print"M keyword substitution: kv\n";1662print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1663print"M description:\n";16641665foreachmy$revision(@$revisions)1666{1667print"M ----------------------------\n";1668print"M revision 1.$revision->{revision}\n";1669# reformat the date for log output1670$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}) );1671$revision->{author} =~s/\s+.*//;1672$revision->{author} =~s/^(.{8}).*/$1/;1673print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1674my$commitmessage=$updater->commitmessage($revision->{commithash});1675$commitmessage=~s/^/M /mg;1676print$commitmessage."\n";1677}1678print"M =============================================================================\n";1679}16801681print"ok\n";1682}16831684sub req_annotate1685{1686my($cmd,$data) =@_;16871688 argsplit("annotate");16891690$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1691#$log->debug("status state : " . Dumper($state));16921693# Grab a handle to the SQLite db and do any necessary updates1694my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1695$updater->update();16961697# if no files were specified, we need to work out what files we should be providing annotate on ...1698 argsfromdir($updater);16991700# we'll need a temporary checkout dir1701my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1702my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1703$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");17041705$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1706$ENV{GIT_INDEX_FILE} =$file_index;17071708chdir$tmpdir;17091710# foreach file specified on the command line ...1711foreachmy$filename( @{$state->{args}} )1712{1713$filename= filecleanup($filename);17141715my$meta=$updater->getmeta($filename);17161717next unless($meta->{revision} );17181719# get all the commits that this file was in1720# in dense format -- aka skip dead revisions1721my$revisions=$updater->gethistorydense($filename);1722my$lastseenin=$revisions->[0][2];17231724# populate the temporary index based on the latest commit were we saw1725# the file -- but do it cheaply without checking out any files1726# TODO: if we got a revision from the client, use that instead1727# to look up the commithash in sqlite (still good to default to1728# the current head as we do now)1729system("git-read-tree",$lastseenin);1730unless($?==0)1731{1732die"Error running git-read-tree$lastseenin$file_index$!";1733}1734$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");17351736# do a checkout of the file1737system('git-checkout-index','-f','-u',$filename);1738unless($?==0) {1739die"Error running git-checkout-index -f -u$filename:$!";1740}17411742$log->info("Annotate$filename");17431744# Prepare a file with the commits from the linearized1745# history that annotate should know about. This prevents1746# git-jsannotate telling us about commits we are hiding1747# from the client.17481749open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1750for(my$i=0;$i<@$revisions;$i++)1751{1752print ANNOTATEHINTS $revisions->[$i][2];1753if($i+1<@$revisions) {# have we got a parent?1754print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1755}1756print ANNOTATEHINTS "\n";1757}17581759print ANNOTATEHINTS "\n";1760close ANNOTATEHINTS;17611762my$annotatecmd='git-annotate';1763open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1764or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1765my$metadata= {};1766print"E Annotations for$filename\n";1767print"E ***************\n";1768while( <ANNOTATE> )1769{1770if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1771{1772my$commithash=$1;1773my$data=$2;1774unless(defined($metadata->{$commithash} ) )1775{1776$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1777$metadata->{$commithash}{author} =~s/\s+.*//;1778$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1779$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1780}1781printf("M 1.%-5d (%-8s%10s):%s\n",1782$metadata->{$commithash}{revision},1783$metadata->{$commithash}{author},1784$metadata->{$commithash}{modified},1785$data1786);1787}else{1788$log->warn("Error in annotate output! LINE:$_");1789print"E Annotate error\n";1790next;1791}1792}1793close ANNOTATE;1794}17951796# done; get out of the tempdir1797chdir"/";17981799print"ok\n";18001801}18021803# This method takes the state->{arguments} array and produces two new arrays.1804# The first is $state->{args} which is everything before the '--' argument, and1805# the second is $state->{files} which is everything after it.1806sub argsplit1807{1808return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");18091810my$type=shift;18111812$state->{args} = [];1813$state->{files} = [];1814$state->{opt} = {};18151816if(defined($type) )1817{1818my$opt= {};1819$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");1820$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1821$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");1822$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1823$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1824$opt= { k =>1, m =>1}if($typeeq"add");1825$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1826$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");182718281829while(scalar( @{$state->{arguments}} ) >0)1830{1831my$arg=shift@{$state->{arguments}};18321833next if($argeq"--");1834next unless($arg=~/\S/);18351836# if the argument looks like a switch1837if($arg=~/^-(\w)(.*)/)1838{1839# if it's a switch that takes an argument1840if($opt->{$1} )1841{1842# If this switch has already been provided1843if($opt->{$1} >1and exists($state->{opt}{$1} ) )1844{1845$state->{opt}{$1} = [$state->{opt}{$1} ];1846if(length($2) >0)1847{1848push@{$state->{opt}{$1}},$2;1849}else{1850push@{$state->{opt}{$1}},shift@{$state->{arguments}};1851}1852}else{1853# if there's extra data in the arg, use that as the argument for the switch1854if(length($2) >0)1855{1856$state->{opt}{$1} =$2;1857}else{1858$state->{opt}{$1} =shift@{$state->{arguments}};1859}1860}1861}else{1862$state->{opt}{$1} =undef;1863}1864}1865else1866{1867push@{$state->{args}},$arg;1868}1869}1870}1871else1872{1873my$mode=0;18741875foreachmy$value( @{$state->{arguments}} )1876{1877if($valueeq"--")1878{1879$mode++;1880next;1881}1882push@{$state->{args}},$valueif($mode==0);1883push@{$state->{files}},$valueif($mode==1);1884}1885}1886}18871888# This method uses $state->{directory} to populate $state->{args} with a list of filenames1889sub argsfromdir1890{1891my$updater=shift;18921893$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");18941895return if(scalar( @{$state->{args}} ) >1);18961897my@gethead= @{$updater->gethead};18981899# push added files1900foreachmy$file(keys%{$state->{entries}}) {1901if(exists$state->{entries}{$file}{revision} &&1902$state->{entries}{$file}{revision} ==0)1903{1904push@gethead, { name =>$file, filehash =>'added'};1905}1906}19071908if(scalar(@{$state->{args}}) ==1)1909{1910my$arg=$state->{args}[0];1911$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );19121913$log->info("Only one arg specified, checking for directory expansion on '$arg'");19141915foreachmy$file(@gethead)1916{1917next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1918next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1919push@{$state->{args}},$file->{name};1920}19211922shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1923}else{1924$log->info("Only one arg specified, populating file list automatically");19251926$state->{args} = [];19271928foreachmy$file(@gethead)1929{1930next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1931next unless($file->{name} =~s/^$state->{prependdir}//);1932push@{$state->{args}},$file->{name};1933}1934}1935}19361937# This method cleans up the $state variable after a command that uses arguments has run1938sub statecleanup1939{1940$state->{files} = [];1941$state->{args} = [];1942$state->{arguments} = [];1943$state->{entries} = {};1944}19451946sub revparse1947{1948my$filename=shift;19491950returnundefunless(defined($state->{entries}{$filename}{revision} ) );19511952return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1953return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);19541955returnundef;1956}19571958# This method takes a file hash and does a CVS "file transfer" which transmits the1959# size of the file, and then the file contents.1960# If a second argument $targetfile is given, the file is instead written out to1961# a file by the name of $targetfile1962sub transmitfile1963{1964my$filehash=shift;1965my$targetfile=shift;19661967if(defined($filehash)and$filehasheq"deleted")1968{1969$log->warn("filehash is 'deleted'");1970return;1971}19721973die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);19741975my$type=`git-cat-file -t$filehash`;1976 chomp$type;19771978 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );19791980 my$size= `git-cat-file -s $filehash`;1981chomp$size;19821983$log->debug("transmitfile($filehash) size=$size, type=$type");19841985if(open my$fh,'-|',"git-cat-file","blob",$filehash)1986{1987if(defined($targetfile) )1988{1989open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1990print NEWFILE $_while( <$fh> );1991close NEWFILE;1992}else{1993print"$size\n";1994printwhile( <$fh> );1995}1996close$fhor die("Couldn't close filehandle for transmitfile()");1997}else{1998die("Couldn't execute git-cat-file");1999}2000}20012002# This method takes a file name, and returns ( $dirpart, $filepart ) which2003# refers to the directory portion and the file portion of the filename2004# respectively2005sub filenamesplit2006{2007my$filename=shift;2008my$fixforlocaldir=shift;20092010my($filepart,$dirpart) = ($filename,".");2011($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2012$dirpart.="/";20132014if($fixforlocaldir)2015{2016$dirpart=~s/^$state->{prependdir}//;2017}20182019return($filepart,$dirpart);2020}20212022sub filecleanup2023{2024my$filename=shift;20252026returnundefunless(defined($filename));2027if($filename=~/^\// )2028{2029print"E absolute filenames '$filename' not supported by server\n";2030returnundef;2031}20322033$filename=~s/^\.\///g;2034$filename=$state->{prependdir} .$filename;2035return$filename;2036}20372038# Given a path, this function returns a string containing the kopts2039# that should go into that path's Entries line. For example, a binary2040# file should get -kb.2041sub kopts_from_path2042{2043my($path) =@_;20442045# Once it exists, the git attributes system should be used to look up2046# what attributes apply to this path.20472048# Until then, take the setting from the config file2049unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)2050{2051# Return "" to give no special treatment to any path2052return"";2053}else{2054# Alternatively, to have all files treated as if they are binary (which2055# is more like git itself), always return the "-kb" option2056return"-kb";2057}2058}20592060package GITCVS::log;20612062####2063#### Copyright The Open University UK - 2006.2064####2065#### Authors: Martyn Smith <martyn@catalyst.net.nz>2066#### Martin Langhoff <martin@catalyst.net.nz>2067####2068####20692070use strict;2071use warnings;20722073=head1 NAME20742075GITCVS::log20762077=head1 DESCRIPTION20782079This module provides very crude logging with a similar interface to2080Log::Log4perl20812082=head1 METHODS20832084=cut20852086=head2 new20872088Creates a new log object, optionally you can specify a filename here to2089indicate the file to log to. If no log file is specified, you can specify one2090later with method setfile, or indicate you no longer want logging with method2091nofile.20922093Until one of these methods is called, all log calls will buffer messages ready2094to write out.20952096=cut2097sub new2098{2099my$class=shift;2100my$filename=shift;21012102my$self= {};21032104bless$self,$class;21052106if(defined($filename) )2107{2108open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2109}21102111return$self;2112}21132114=head2 setfile21152116This methods takes a filename, and attempts to open that file as the log file.2117If successful, all buffered data is written out to the file, and any further2118logging is written directly to the file.21192120=cut2121sub setfile2122{2123my$self=shift;2124my$filename=shift;21252126if(defined($filename) )2127{2128open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2129}21302131return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21322133while(my$line=shift@{$self->{buffer}} )2134{2135print{$self->{fh}}$line;2136}2137}21382139=head2 nofile21402141This method indicates no logging is going to be used. It flushes any entries in2142the internal buffer, and sets a flag to ensure no further data is put there.21432144=cut2145sub nofile2146{2147my$self=shift;21482149$self->{nolog} =1;21502151return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");21522153$self->{buffer} = [];2154}21552156=head2 _logopen21572158Internal method. Returns true if the log file is open, false otherwise.21592160=cut2161sub _logopen2162{2163my$self=shift;21642165return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2166return0;2167}21682169=head2 debug info warn fatal21702171These four methods are wrappers to _log. They provide the actual interface for2172logging data.21732174=cut2175sub debug {my$self=shift;$self->_log("debug",@_); }2176sub info {my$self=shift;$self->_log("info",@_); }2177subwarn{my$self=shift;$self->_log("warn",@_); }2178sub fatal {my$self=shift;$self->_log("fatal",@_); }21792180=head2 _log21812182This is an internal method called by the logging functions. It generates a2183timestamp and pushes the logged line either to file, or internal buffer.21842185=cut2186sub _log2187{2188my$self=shift;2189my$level=shift;21902191return if($self->{nolog} );21922193my@time=localtime;2194my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2195$time[5] +1900,2196$time[4] +1,2197$time[3],2198$time[2],2199$time[1],2200$time[0],2201uc$level,2202);22032204if($self->_logopen)2205{2206print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2207}else{2208push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2209}2210}22112212=head2 DESTROY22132214This method simply closes the file handle if one is open22152216=cut2217sub DESTROY2218{2219my$self=shift;22202221if($self->_logopen)2222{2223close$self->{fh};2224}2225}22262227package GITCVS::updater;22282229####2230#### Copyright The Open University UK - 2006.2231####2232#### Authors: Martyn Smith <martyn@catalyst.net.nz>2233#### Martin Langhoff <martin@catalyst.net.nz>2234####2235####22362237use strict;2238use warnings;2239use DBI;22402241=head1 METHODS22422243=cut22442245=head2 new22462247=cut2248sub new2249{2250my$class=shift;2251my$config=shift;2252my$module=shift;2253my$log=shift;22542255die"Need to specify a git repository"unless(defined($config)and-d $config);2256die"Need to specify a module"unless(defined($module) );22572258$class=ref($class) ||$class;22592260my$self= {};22612262bless$self,$class;22632264$self->{module} =$module;2265$self->{git_path} =$config."/";22662267$self->{log} =$log;22682269die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );22702271$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2272$cfg->{gitcvs}{dbdriver} ||"SQLite";2273$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2274$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2275$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2276$cfg->{gitcvs}{dbuser} ||"";2277$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2278$cfg->{gitcvs}{dbpass} ||"";2279my%mapping= ( m =>$module,2280 a =>$state->{method},2281 u =>getlogin||getpwuid($<) || $<,2282 G =>$self->{git_path},2283 g => mangle_dirname($self->{git_path}),2284);2285$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2286$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;22872288die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2289die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2290$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2291$self->{dbuser},2292$self->{dbpass});2293die"Error connecting to database\n"unlessdefined$self->{dbh};22942295$self->{tables} = {};2296foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2297{2298$self->{tables}{$table} =1;2299}23002301# Construct the revision table if required2302unless($self->{tables}{revision} )2303{2304$self->{dbh}->do("2305 CREATE TABLE revision (2306 name TEXT NOT NULL,2307 revision INTEGER NOT NULL,2308 filehash TEXT NOT NULL,2309 commithash TEXT NOT NULL,2310 author TEXT NOT NULL,2311 modified TEXT NOT NULL,2312 mode TEXT NOT NULL2313 )2314 ");2315$self->{dbh}->do("2316 CREATE INDEX revision_ix12317 ON revision (name,revision)2318 ");2319$self->{dbh}->do("2320 CREATE INDEX revision_ix22321 ON revision (name,commithash)2322 ");2323}23242325# Construct the head table if required2326unless($self->{tables}{head} )2327{2328$self->{dbh}->do("2329 CREATE TABLE head (2330 name TEXT NOT NULL,2331 revision INTEGER NOT NULL,2332 filehash TEXT NOT NULL,2333 commithash TEXT NOT NULL,2334 author TEXT NOT NULL,2335 modified TEXT NOT NULL,2336 mode TEXT NOT NULL2337 )2338 ");2339$self->{dbh}->do("2340 CREATE INDEX head_ix12341 ON head (name)2342 ");2343}23442345# Construct the properties table if required2346unless($self->{tables}{properties} )2347{2348$self->{dbh}->do("2349 CREATE TABLE properties (2350 key TEXT NOT NULL PRIMARY KEY,2351 value TEXT2352 )2353 ");2354}23552356# Construct the commitmsgs table if required2357unless($self->{tables}{commitmsgs} )2358{2359$self->{dbh}->do("2360 CREATE TABLE commitmsgs (2361 key TEXT NOT NULL PRIMARY KEY,2362 value TEXT2363 )2364 ");2365}23662367return$self;2368}23692370=head2 update23712372=cut2373sub update2374{2375my$self=shift;23762377# first lets get the commit list2378$ENV{GIT_DIR} =$self->{git_path};23792380my$commitsha1=`git rev-parse$self->{module}`;2381chomp$commitsha1;23822383my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2384unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2385{2386die("Invalid module '$self->{module}'");2387}238823892390my$git_log;2391my$lastcommit=$self->_get_prop("last_commit");23922393if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2394return1;2395}23962397# Start exclusive lock here...2398$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";23992400# TODO: log processing is memory bound2401# if we can parse into a 2nd file that is in reverse order2402# we can probably do something really efficient2403my@git_log_params= ('--pretty','--parents','--topo-order');24042405if(defined$lastcommit) {2406push@git_log_params,"$lastcommit..$self->{module}";2407}else{2408push@git_log_params,$self->{module};2409}2410# git-rev-list is the backend / plumbing version of git-log2411open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";24122413my@commits;24142415my%commit= ();24162417while( <GITLOG> )2418{2419chomp;2420if(m/^commit\s+(.*)$/) {2421# on ^commit lines put the just seen commit in the stack2422# and prime things for the next one2423if(keys%commit) {2424my%copy=%commit;2425unshift@commits, \%copy;2426%commit= ();2427}2428my@parents=split(m/\s+/,$1);2429$commit{hash} =shift@parents;2430$commit{parents} = \@parents;2431}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2432# on rfc822-like lines seen before we see any message,2433# lowercase the entry and put it in the hash as key-value2434$commit{lc($1)} =$2;2435}else{2436# message lines - skip initial empty line2437# and trim whitespace2438if(!exists($commit{message}) &&m/^\s*$/) {2439# define it to mark the end of headers2440$commit{message} ='';2441next;2442}2443s/^\s+//;s/\s+$//;# trim ws2444$commit{message} .=$_."\n";2445}2446}2447close GITLOG;24482449unshift@commits, \%commitif(keys%commit);24502451# Now all the commits are in the @commits bucket2452# ordered by time DESC. for each commit that needs processing,2453# determine whether it's following the last head we've seen or if2454# it's on its own branch, grab a file list, and add whatever's changed2455# NOTE: $lastcommit refers to the last commit from previous run2456# $lastpicked is the last commit we picked in this run2457my$lastpicked;2458my$head= {};2459if(defined$lastcommit) {2460$lastpicked=$lastcommit;2461}24622463my$committotal=scalar(@commits);2464my$commitcount=0;24652466# Load the head table into $head (for cached lookups during the update process)2467foreachmy$file( @{$self->gethead()} )2468{2469$head->{$file->{name}} =$file;2470}24712472foreachmy$commit(@commits)2473{2474$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2475if(defined$lastpicked)2476{2477if(!in_array($lastpicked, @{$commit->{parents}}))2478{2479# skip, we'll see this delta2480# as part of a merge later2481# warn "skipping off-track $commit->{hash}\n";2482next;2483}elsif(@{$commit->{parents}} >1) {2484# it is a merge commit, for each parent that is2485# not $lastpicked, see if we can get a log2486# from the merge-base to that parent to put it2487# in the message as a merge summary.2488my@parents= @{$commit->{parents}};2489foreachmy$parent(@parents) {2490# git-merge-base can potentially (but rarely) throw2491# several candidate merge bases. let's assume2492# that the first one is the best one.2493if($parenteq$lastpicked) {2494next;2495}2496open my$p,'git-merge-base '.$lastpicked.' '2497.$parent.'|';2498my@output= (<$p>);2499close$p;2500my$base=join('',@output);2501chomp$base;2502if($base) {2503my@merged;2504# print "want to log between $base $parent \n";2505open(GITLOG,'-|','git-log',"$base..$parent")2506or die"Cannot call git-log:$!";2507my$mergedhash;2508while(<GITLOG>) {2509chomp;2510if(!defined$mergedhash) {2511if(m/^commit\s+(.+)$/) {2512$mergedhash=$1;2513}else{2514next;2515}2516}else{2517# grab the first line that looks non-rfc8222518# aka has content after leading space2519if(m/^\s+(\S.*)$/) {2520my$title=$1;2521$title=substr($title,0,100);# truncate2522unshift@merged,"$mergedhash$title";2523undef$mergedhash;2524}2525}2526}2527close GITLOG;2528if(@merged) {2529$commit->{mergemsg} =$commit->{message};2530$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2531foreachmy$summary(@merged) {2532$commit->{mergemsg} .="\t$summary\n";2533}2534$commit->{mergemsg} .="\n\n";2535# print "Message for $commit->{hash} \n$commit->{mergemsg}";2536}2537}2538}2539}2540}25412542# convert the date to CVS-happy format2543$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);25442545if(defined($lastpicked) )2546{2547my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2548local($/) ="\0";2549while( <FILELIST> )2550{2551chomp;2552unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2553{2554die("Couldn't process git-diff-tree line :$_");2555}2556my($mode,$hash,$change) = ($1,$2,$3);2557my$name= <FILELIST>;2558chomp($name);25592560# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");25612562my$git_perms="";2563$git_perms.="r"if($mode&4);2564$git_perms.="w"if($mode&2);2565$git_perms.="x"if($mode&1);2566$git_perms="rw"if($git_permseq"");25672568if($changeeq"D")2569{2570#$log->debug("DELETE $name");2571$head->{$name} = {2572 name =>$name,2573 revision =>$head->{$name}{revision} +1,2574 filehash =>"deleted",2575 commithash =>$commit->{hash},2576 modified =>$commit->{date},2577 author =>$commit->{author},2578 mode =>$git_perms,2579};2580$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2581}2582elsif($changeeq"M")2583{2584#$log->debug("MODIFIED $name");2585$head->{$name} = {2586 name =>$name,2587 revision =>$head->{$name}{revision} +1,2588 filehash =>$hash,2589 commithash =>$commit->{hash},2590 modified =>$commit->{date},2591 author =>$commit->{author},2592 mode =>$git_perms,2593};2594$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2595}2596elsif($changeeq"A")2597{2598#$log->debug("ADDED $name");2599$head->{$name} = {2600 name =>$name,2601 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,2602 filehash =>$hash,2603 commithash =>$commit->{hash},2604 modified =>$commit->{date},2605 author =>$commit->{author},2606 mode =>$git_perms,2607};2608$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2609}2610else2611{2612$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2613die;2614}2615}2616close FILELIST;2617}else{2618# this is used to detect files removed from the repo2619my$seen_files= {};26202621my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2622local$/="\0";2623while( <FILELIST> )2624{2625chomp;2626unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2627{2628die("Couldn't process git-ls-tree line :$_");2629}26302631my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);26322633$seen_files->{$git_filename} =1;26342635my($oldhash,$oldrevision,$oldmode) = (2636$head->{$git_filename}{filehash},2637$head->{$git_filename}{revision},2638$head->{$git_filename}{mode}2639);26402641if($git_perms=~/^\d\d\d(\d)\d\d/o)2642{2643$git_perms="";2644$git_perms.="r"if($1&4);2645$git_perms.="w"if($1&2);2646$git_perms.="x"if($1&1);2647}else{2648$git_perms="rw";2649}26502651# unless the file exists with the same hash, we need to update it ...2652unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2653{2654my$newrevision= ($oldrevisionor0) +1;26552656$head->{$git_filename} = {2657 name =>$git_filename,2658 revision =>$newrevision,2659 filehash =>$git_hash,2660 commithash =>$commit->{hash},2661 modified =>$commit->{date},2662 author =>$commit->{author},2663 mode =>$git_perms,2664};266526662667$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2668}2669}2670close FILELIST;26712672# Detect deleted files2673foreachmy$file(keys%$head)2674{2675unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2676{2677$head->{$file}{revision}++;2678$head->{$file}{filehash} ="deleted";2679$head->{$file}{commithash} =$commit->{hash};2680$head->{$file}{modified} =$commit->{date};2681$head->{$file}{author} =$commit->{author};26822683$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2684}2685}2686# END : "Detect deleted files"2687}268826892690if(exists$commit->{mergemsg})2691{2692$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2693}26942695$lastpicked=$commit->{hash};26962697$self->_set_prop("last_commit",$commit->{hash});2698}26992700$self->delete_head();2701foreachmy$file(keys%$head)2702{2703$self->insert_head(2704$file,2705$head->{$file}{revision},2706$head->{$file}{filehash},2707$head->{$file}{commithash},2708$head->{$file}{modified},2709$head->{$file}{author},2710$head->{$file}{mode},2711);2712}2713# invalidate the gethead cache2714$self->{gethead_cache} =undef;271527162717# Ending exclusive lock here2718$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2719}27202721sub insert_rev2722{2723my$self=shift;2724my$name=shift;2725my$revision=shift;2726my$filehash=shift;2727my$commithash=shift;2728my$modified=shift;2729my$author=shift;2730my$mode=shift;27312732my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2733$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2734}27352736sub insert_mergelog2737{2738my$self=shift;2739my$key=shift;2740my$value=shift;27412742my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2743$insert_mergelog->execute($key,$value);2744}27452746sub delete_head2747{2748my$self=shift;27492750my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2751$delete_head->execute();2752}27532754sub insert_head2755{2756my$self=shift;2757my$name=shift;2758my$revision=shift;2759my$filehash=shift;2760my$commithash=shift;2761my$modified=shift;2762my$author=shift;2763my$mode=shift;27642765my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2766$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2767}27682769sub _headrev2770{2771my$self=shift;2772my$filename=shift;27732774my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2775$db_query->execute($filename);2776my($hash,$revision,$mode) =$db_query->fetchrow_array;27772778return($hash,$revision,$mode);2779}27802781sub _get_prop2782{2783my$self=shift;2784my$key=shift;27852786my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2787$db_query->execute($key);2788my($value) =$db_query->fetchrow_array;27892790return$value;2791}27922793sub _set_prop2794{2795my$self=shift;2796my$key=shift;2797my$value=shift;27982799my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2800$db_query->execute($value,$key);28012802unless($db_query->rows)2803{2804$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2805$db_query->execute($key,$value);2806}28072808return$value;2809}28102811=head2 gethead28122813=cut28142815sub gethead2816{2817my$self=shift;28182819return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );28202821my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2822$db_query->execute();28232824my$tree= [];2825while(my$file=$db_query->fetchrow_hashref)2826{2827push@$tree,$file;2828}28292830$self->{gethead_cache} =$tree;28312832return$tree;2833}28342835=head2 getlog28362837=cut28382839sub getlog2840{2841my$self=shift;2842my$filename=shift;28432844my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2845$db_query->execute($filename);28462847my$tree= [];2848while(my$file=$db_query->fetchrow_hashref)2849{2850push@$tree,$file;2851}28522853return$tree;2854}28552856=head2 getmeta28572858This function takes a filename (with path) argument and returns a hashref of2859metadata for that file.28602861=cut28622863sub getmeta2864{2865my$self=shift;2866my$filename=shift;2867my$revision=shift;28682869my$db_query;2870if(defined($revision)and$revision=~/^\d+$/)2871{2872$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2873$db_query->execute($filename,$revision);2874}2875elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2876{2877$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2878$db_query->execute($filename,$revision);2879}else{2880$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2881$db_query->execute($filename);2882}28832884return$db_query->fetchrow_hashref;2885}28862887=head2 commitmessage28882889this function takes a commithash and returns the commit message for that commit28902891=cut2892sub commitmessage2893{2894my$self=shift;2895my$commithash=shift;28962897die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);28982899my$db_query;2900$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2901$db_query->execute($commithash);29022903my($message) =$db_query->fetchrow_array;29042905if(defined($message) )2906{2907$message.=" "if($message=~/\n$/);2908return$message;2909}29102911my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2912shift@lineswhile($lines[0] =~/\S/);2913$message=join("",@lines);2914$message.=" "if($message=~/\n$/);2915return$message;2916}29172918=head2 gethistory29192920This function takes a filename (with path) argument and returns an arrayofarrays2921containing revision,filehash,commithash ordered by revision descending29222923=cut2924sub gethistory2925{2926my$self=shift;2927my$filename=shift;29282929my$db_query;2930$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2931$db_query->execute($filename);29322933return$db_query->fetchall_arrayref;2934}29352936=head2 gethistorydense29372938This function takes a filename (with path) argument and returns an arrayofarrays2939containing revision,filehash,commithash ordered by revision descending.29402941This version of gethistory skips deleted entries -- so it is useful for annotate.2942The 'dense' part is a reference to a '--dense' option available for git-rev-list2943and other git tools that depend on it.29442945=cut2946sub gethistorydense2947{2948my$self=shift;2949my$filename=shift;29502951my$db_query;2952$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2953$db_query->execute($filename);29542955return$db_query->fetchall_arrayref;2956}29572958=head2 in_array()29592960from Array::PAT - mimics the in_array() function2961found in PHP. Yuck but works for small arrays.29622963=cut2964sub in_array2965{2966my($check,@array) =@_;2967my$retval=0;2968foreachmy$test(@array){2969if($checkeq$test){2970$retval=1;2971}2972}2973return$retval;2974}29752976=head2 safe_pipe_capture29772978an alternative to `command` that allows input to be passed as an array2979to work around shell problems with weird characters in arguments29802981=cut2982sub safe_pipe_capture {29832984my@output;29852986if(my$pid=open my$child,'-|') {2987@output= (<$child>);2988close$childor die join(' ',@_).":$!$?";2989}else{2990exec(@_)or die"$!$?";# exec() can fail the executable can't be found2991}2992returnwantarray?@output:join('',@output);2993}29942995=head2 mangle_dirname29962997create a string from a directory name that is suitable to use as2998part of a filename, mainly by converting all chars except \w.- to _29993000=cut3001sub mangle_dirname {3002my$dirname=shift;3003return unlessdefined$dirname;30043005$dirname=~s/[^\w.-]/_/g;30063007return$dirname;3008}300930101;