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@laptop.org> 12#### 13#### 14#### Released under the GNU Public License, version 2. 15#### 16#### 17 18use5.008; 19use strict; 20use warnings; 21use bytes; 22 23use Fcntl; 24use File::Temp qw/tempdir tempfile/; 25use File::Path qw/rmtree/; 26use File::Basename; 27use Getopt::Long qw(:config require_order no_ignore_case); 28 29my$VERSION='@@GIT_VERSION@@'; 30 31my$log= GITCVS::log->new(); 32my$cfg; 33 34my$DATE_LIST= { 35 Jan =>"01", 36 Feb =>"02", 37 Mar =>"03", 38 Apr =>"04", 39 May =>"05", 40 Jun =>"06", 41 Jul =>"07", 42 Aug =>"08", 43 Sep =>"09", 44 Oct =>"10", 45 Nov =>"11", 46 Dec =>"12", 47}; 48 49# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 50$| =1; 51 52#### Definition and mappings of functions #### 53 54# NOTE: Despite the existence of req_CATCHALL and req_EMPTY unimplemented 55# requests, this list is incomplete. It is missing many rarer/optional 56# requests. Perhaps some clients require a claim of support for 57# these specific requests for main functionality to work? 58my$methods= { 59'Root'=> \&req_Root, 60'Valid-responses'=> \&req_Validresponses, 61'valid-requests'=> \&req_validrequests, 62'Directory'=> \&req_Directory, 63'Entry'=> \&req_Entry, 64'Modified'=> \&req_Modified, 65'Unchanged'=> \&req_Unchanged, 66'Questionable'=> \&req_Questionable, 67'Argument'=> \&req_Argument, 68'Argumentx'=> \&req_Argument, 69'expand-modules'=> \&req_expandmodules, 70'add'=> \&req_add, 71'remove'=> \&req_remove, 72'co'=> \&req_co, 73'update'=> \&req_update, 74'ci'=> \&req_ci, 75'diff'=> \&req_diff, 76'log'=> \&req_log, 77'rlog'=> \&req_log, 78'tag'=> \&req_CATCHALL, 79'status'=> \&req_status, 80'admin'=> \&req_CATCHALL, 81'history'=> \&req_CATCHALL, 82'watchers'=> \&req_EMPTY, 83'editors'=> \&req_EMPTY, 84'noop'=> \&req_EMPTY, 85'annotate'=> \&req_annotate, 86'Global_option'=> \&req_Globaloption, 87}; 88 89############################################## 90 91 92# $state holds all the bits of information the clients sends us that could 93# potentially be useful when it comes to actually _doing_ something. 94my$state= { prependdir =>''}; 95 96# Work is for managing temporary working directory 97my$work= 98{ 99state=>undef,# undef, 1 (empty), 2 (with stuff) 100 workDir =>undef, 101index=>undef, 102 emptyDir =>undef, 103 tmpDir =>undef 104}; 105 106$log->info("--------------- STARTING -----------------"); 107 108my$usage= 109"Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n". 110" --base-path <path> : Prepend to requested CVSROOT\n". 111" Can be read from GIT_CVSSERVER_BASE_PATH\n". 112" --strict-paths : Don't allow recursing into subdirectories\n". 113" --export-all : Don't check for gitcvs.enabled in config\n". 114" --version, -V : Print version information and exit\n". 115" -h, -H : Print usage information and exit\n". 116"\n". 117"<directory> ... is a list of allowed directories. If no directories\n". 118"are given, all are allowed. This is an additional restriction, gitcvs\n". 119"access still needs to be enabled by the gitcvs.enabled config option.\n". 120"Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n"; 121 122my@opts= ('h|H','version|V', 123'base-path=s','strict-paths','export-all'); 124GetOptions($state,@opts) 125or die$usage; 126 127if($state->{version}) { 128print"git-cvsserver version$VERSION\n"; 129exit; 130} 131if($state->{help}) { 132print$usage; 133exit; 134} 135 136my$TEMP_DIR= tempdir( CLEANUP =>1); 137$log->debug("Temporary directory is '$TEMP_DIR'"); 138 139$state->{method} ='ext'; 140if(@ARGV) { 141if($ARGV[0]eq'pserver') { 142$state->{method} ='pserver'; 143shift@ARGV; 144}elsif($ARGV[0]eq'server') { 145shift@ARGV; 146} 147} 148 149# everything else is a directory 150$state->{allowed_roots} = [@ARGV]; 151 152# don't export the whole system unless the users requests it 153if($state->{'export-all'} && !@{$state->{allowed_roots}}) { 154die"--export-all can only be used together with an explicit whitelist\n"; 155} 156 157# Environment handling for running under git-shell 158if(exists$ENV{GIT_CVSSERVER_BASE_PATH}) { 159if($state->{'base-path'}) { 160die"Cannot specify base path both ways.\n"; 161} 162my$base_path=$ENV{GIT_CVSSERVER_BASE_PATH}; 163$state->{'base-path'} =$base_path; 164$log->debug("Picked up base path '$base_path' from environment.\n"); 165} 166if(exists$ENV{GIT_CVSSERVER_ROOT}) { 167if(@{$state->{allowed_roots}}) { 168die"Cannot specify roots both ways:@ARGV\n"; 169} 170my$allowed_root=$ENV{GIT_CVSSERVER_ROOT}; 171$state->{allowed_roots} = [$allowed_root]; 172$log->debug("Picked up allowed root '$allowed_root' from environment.\n"); 173} 174 175# if we are called with a pserver argument, 176# deal with the authentication cat before entering the 177# main loop 178if($state->{method}eq'pserver') { 179my$line= <STDIN>;chomp$line; 180unless($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 181die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 182} 183my$request=$1; 184$line= <STDIN>;chomp$line; 185unless(req_Root('root',$line)) {# reuse Root 186print"E Invalid root$line\n"; 187exit1; 188} 189$line= <STDIN>;chomp$line; 190my$user=$line; 191$line= <STDIN>;chomp$line; 192my$password=$line; 193 194if($usereq'anonymous') { 195# "A" will be 1 byte, use length instead in case the 196# encryption method ever changes (yeah, right!) 197if(length($password) >1) { 198print"E Don't supply a password for the `anonymous' user\n"; 199print"I HATE YOU\n"; 200exit1; 201} 202 203# Fall through to LOVE 204}else{ 205# Trying to authenticate a user 206if(not exists$cfg->{gitcvs}->{authdb}) { 207print"E the repo config file needs a [gitcvs] section with an 'authdb' parameter set to the filename of the authentication database\n"; 208print"I HATE YOU\n"; 209exit1; 210} 211 212my$authdb=$cfg->{gitcvs}->{authdb}; 213 214unless(-e $authdb) { 215print"E The authentication database specified in [gitcvs.authdb] does not exist\n"; 216print"I HATE YOU\n"; 217exit1; 218} 219 220my$auth_ok; 221open my$passwd,"<",$authdbor die$!; 222while(<$passwd>) { 223if(m{^\Q$user\E:(.*)}) { 224if(crypt($user, descramble($password))eq$1) { 225$auth_ok=1; 226} 227}; 228} 229close$passwd; 230 231unless($auth_ok) { 232print"I HATE YOU\n"; 233exit1; 234} 235 236# Fall through to LOVE 237} 238 239# For checking whether the user is anonymous on commit 240$state->{user} =$user; 241 242$line= <STDIN>;chomp$line; 243unless($lineeq"END$requestREQUEST") { 244die"E Do not understand$line-- expecting END$requestREQUEST\n"; 245} 246print"I LOVE YOU\n"; 247exit if$requesteq'VERIFICATION';# cvs login 248# and now back to our regular programme... 249} 250 251# Keep going until the client closes the connection 252while(<STDIN>) 253{ 254chomp; 255 256# Check to see if we've seen this method, and call appropriate function. 257if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 258{ 259# use the $methods hash to call the appropriate sub for this command 260#$log->info("Method : $1"); 261&{$methods->{$1}}($1,$2); 262}else{ 263# log fatal because we don't understand this function. If this happens 264# we're fairly screwed because we don't know if the client is expecting 265# a response. If it is, the client will hang, we'll hang, and the whole 266# thing will be custard. 267$log->fatal("Don't understand command$_\n"); 268die("Unknown command$_"); 269} 270} 271 272$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 273$log->info("--------------- FINISH -----------------"); 274 275chdir'/'; 276exit0; 277 278# Magic catchall method. 279# This is the method that will handle all commands we haven't yet 280# implemented. It simply sends a warning to the log file indicating a 281# command that hasn't been implemented has been invoked. 282sub req_CATCHALL 283{ 284my($cmd,$data) =@_; 285$log->warn("Unhandled command : req_$cmd:$data"); 286} 287 288# This method invariably succeeds with an empty response. 289sub req_EMPTY 290{ 291print"ok\n"; 292} 293 294# Root pathname \n 295# Response expected: no. Tell the server which CVSROOT to use. Note that 296# pathname is a local directory and not a fully qualified CVSROOT variable. 297# pathname must already exist; if creating a new root, use the init 298# request, not Root. pathname does not include the hostname of the server, 299# how to access the server, etc.; by the time the CVS protocol is in use, 300# connection, authentication, etc., are already taken care of. The Root 301# request must be sent only once, and it must be sent before any requests 302# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 303sub req_Root 304{ 305my($cmd,$data) =@_; 306$log->debug("req_Root :$data"); 307 308unless($data=~ m#^/#) { 309print"error 1 Root must be an absolute pathname\n"; 310return0; 311} 312 313my$cvsroot=$state->{'base-path'} ||''; 314$cvsroot=~ s#/+$##; 315$cvsroot.=$data; 316 317if($state->{CVSROOT} 318&& ($state->{CVSROOT}ne$cvsroot)) { 319print"error 1 Conflicting roots specified\n"; 320return0; 321} 322 323$state->{CVSROOT} =$cvsroot; 324 325$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 326 327if(@{$state->{allowed_roots}}) { 328my$allowed=0; 329foreachmy$dir(@{$state->{allowed_roots}}) { 330next unless$dir=~ m#^/#; 331$dir=~ s#/+$##; 332if($state->{'strict-paths'}) { 333if($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { 334$allowed=1; 335last; 336} 337}elsif($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { 338$allowed=1; 339last; 340} 341} 342 343unless($allowed) { 344print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 345print"E\n"; 346print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 347return0; 348} 349} 350 351unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 352print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 353print"E\n"; 354print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 355return0; 356} 357 358my@gitvars=`git config -l`; 359if($?) { 360print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 361print"E\n"; 362print"error 1 - problem executing git-config\n"; 363return0; 364} 365foreachmy$line(@gitvars) 366{ 367next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 368unless($2) { 369$cfg->{$1}{$3} =$4; 370}else{ 371$cfg->{$1}{$2}{$3} =$4; 372} 373} 374 375my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 376||$cfg->{gitcvs}{enabled}); 377unless($state->{'export-all'} || 378($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i)) { 379print"E GITCVS emulation needs to be enabled on this repo\n"; 380print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 381print"E\n"; 382print"error 1 GITCVS emulation disabled\n"; 383return0; 384} 385 386my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 387if($logfile) 388{ 389$log->setfile($logfile); 390}else{ 391$log->nofile(); 392} 393 394return1; 395} 396 397# Global_option option \n 398# Response expected: no. Transmit one of the global options `-q', `-Q', 399# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 400# variations (such as combining of options) are allowed. For graceful 401# handling of valid-requests, it is probably better to make new global 402# options separate requests, rather than trying to add them to this 403# request. 404sub req_Globaloption 405{ 406my($cmd,$data) =@_; 407$log->debug("req_Globaloption :$data"); 408$state->{globaloptions}{$data} =1; 409} 410 411# Valid-responses request-list \n 412# Response expected: no. Tell the server what responses the client will 413# accept. request-list is a space separated list of tokens. 414sub req_Validresponses 415{ 416my($cmd,$data) =@_; 417$log->debug("req_Validresponses :$data"); 418 419# TODO : re-enable this, currently it's not particularly useful 420#$state->{validresponses} = [ split /\s+/, $data ]; 421} 422 423# valid-requests \n 424# Response expected: yes. Ask the server to send back a Valid-requests 425# response. 426sub req_validrequests 427{ 428my($cmd,$data) =@_; 429 430$log->debug("req_validrequests"); 431 432$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 433$log->debug("SEND : ok"); 434 435print"Valid-requests ".join(" ",keys%$methods) ."\n"; 436print"ok\n"; 437} 438 439# Directory local-directory \n 440# Additional data: repository \n. Response expected: no. Tell the server 441# what directory to use. The repository should be a directory name from a 442# previous server response. Note that this both gives a default for Entry 443# and Modified and also for ci and the other commands; normal usage is to 444# send Directory for each directory in which there will be an Entry or 445# Modified, and then a final Directory for the original directory, then the 446# command. The local-directory is relative to the top level at which the 447# command is occurring (i.e. the last Directory which is sent before the 448# command); to indicate that top level, `.' should be sent for 449# local-directory. 450sub req_Directory 451{ 452my($cmd,$data) =@_; 453 454my$repository= <STDIN>; 455chomp$repository; 456 457 458$state->{localdir} =$data; 459$state->{repository} =$repository; 460$state->{path} =$repository; 461$state->{path} =~s/^\Q$state->{CVSROOT}\E\///; 462$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 463$state->{path} .="/"if($state->{path} =~ /\S/ ); 464 465$state->{directory} =$state->{localdir}; 466$state->{directory} =""if($state->{directory}eq"."); 467$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 468 469if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 470{ 471$log->info("Setting prepend to '$state->{path}'"); 472$state->{prependdir} =$state->{path}; 473foreachmy$entry(keys%{$state->{entries}} ) 474{ 475$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 476delete$state->{entries}{$entry}; 477} 478} 479 480if(defined($state->{prependdir} ) ) 481{ 482$log->debug("Prepending '$state->{prependdir}' to state|directory"); 483$state->{directory} =$state->{prependdir} .$state->{directory} 484} 485$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 486} 487 488# Entry entry-line \n 489# Response expected: no. Tell the server what version of a file is on the 490# local machine. The name in entry-line is a name relative to the directory 491# most recently specified with Directory. If the user is operating on only 492# some files in a directory, Entry requests for only those files need be 493# included. If an Entry request is sent without Modified, Is-modified, or 494# Unchanged, it means the file is lost (does not exist in the working 495# directory). If both Entry and one of Modified, Is-modified, or Unchanged 496# are sent for the same file, Entry must be sent first. For a given file, 497# one can send Modified, Is-modified, or Unchanged, but not more than one 498# of these three. 499sub req_Entry 500{ 501my($cmd,$data) =@_; 502 503#$log->debug("req_Entry : $data"); 504 505my@data=split(/\//,$data, -1); 506 507$state->{entries}{$state->{directory}.$data[1]} = { 508 revision =>$data[2], 509 conflict =>$data[3], 510 options =>$data[4], 511 tag_or_date =>$data[5], 512}; 513 514$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 515} 516 517# Questionable filename \n 518# Response expected: no. Additional data: no. Tell the server to check 519# whether filename should be ignored, and if not, next time the server 520# sends responses, send (in a M response) `?' followed by the directory and 521# filename. filename must not contain `/'; it needs to be a file in the 522# directory named by the most recent Directory request. 523sub req_Questionable 524{ 525my($cmd,$data) =@_; 526 527$log->debug("req_Questionable :$data"); 528$state->{entries}{$state->{directory}.$data}{questionable} =1; 529} 530 531# add \n 532# Response expected: yes. Add a file or directory. This uses any previous 533# Argument, Directory, Entry, or Modified requests, if they have been sent. 534# The last Directory sent specifies the working directory at the time of 535# the operation. To add a directory, send the directory to be added using 536# Directory and Argument requests. 537sub req_add 538{ 539my($cmd,$data) =@_; 540 541 argsplit("add"); 542 543my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 544$updater->update(); 545 546my$addcount=0; 547 548foreachmy$filename( @{$state->{args}} ) 549{ 550$filename= filecleanup($filename); 551 552my$meta=$updater->getmeta($filename); 553my$wrev= revparse($filename); 554 555if($wrev&&$meta&& ($wrev=~/^-/)) 556{ 557# previously removed file, add back 558$log->info("added file$filenamewas previously removed, send$meta->{revision}"); 559 560print"MT +updated\n"; 561print"MT text U\n"; 562print"MT fname$filename\n"; 563print"MT newline\n"; 564print"MT -updated\n"; 565 566unless($state->{globaloptions}{-n} ) 567{ 568my($filepart,$dirpart) = filenamesplit($filename,1); 569 570print"Created$dirpart\n"; 571print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 572 573# this is an "entries" line 574my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 575$log->debug("/$filepart/$meta->{revision}//$kopts/"); 576print"/$filepart/$meta->{revision}//$kopts/\n"; 577# permissions 578$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 579print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 580# transmit file 581 transmitfile($meta->{filehash}); 582} 583 584next; 585} 586 587unless(defined($state->{entries}{$filename}{modified_filename} ) ) 588{ 589print"E cvs add: nothing known about `$filename'\n"; 590next; 591} 592# TODO : check we're not squashing an already existing file 593if(defined($state->{entries}{$filename}{revision} ) ) 594{ 595print"E cvs add: `$filename' has already been entered\n"; 596next; 597} 598 599my($filepart,$dirpart) = filenamesplit($filename,1); 600 601print"E cvs add: scheduling file `$filename' for addition\n"; 602 603print"Checked-in$dirpart\n"; 604print"$filename\n"; 605my$kopts= kopts_from_path($filename,"file", 606$state->{entries}{$filename}{modified_filename}); 607print"/$filepart/0//$kopts/\n"; 608 609my$requestedKopts=$state->{opt}{k}; 610if(defined($requestedKopts)) 611{ 612$requestedKopts="-k$requestedKopts"; 613} 614else 615{ 616$requestedKopts=""; 617} 618if($koptsne$requestedKopts) 619{ 620$log->warn("Ignoring requested -k='$requestedKopts'" 621." for '$filename'; detected -k='$kopts' instead"); 622#TODO: Also have option to send warning to user? 623} 624 625$addcount++; 626} 627 628if($addcount==1) 629{ 630print"E cvs add: use `cvs commit' to add this file permanently\n"; 631} 632elsif($addcount>1) 633{ 634print"E cvs add: use `cvs commit' to add these files permanently\n"; 635} 636 637print"ok\n"; 638} 639 640# remove \n 641# Response expected: yes. Remove a file. This uses any previous Argument, 642# Directory, Entry, or Modified requests, if they have been sent. The last 643# Directory sent specifies the working directory at the time of the 644# operation. Note that this request does not actually do anything to the 645# repository; the only effect of a successful remove request is to supply 646# the client with a new entries line containing `-' to indicate a removed 647# file. In fact, the client probably could perform this operation without 648# contacting the server, although using remove may cause the server to 649# perform a few more checks. The client sends a subsequent ci request to 650# actually record the removal in the repository. 651sub req_remove 652{ 653my($cmd,$data) =@_; 654 655 argsplit("remove"); 656 657# Grab a handle to the SQLite db and do any necessary updates 658my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 659$updater->update(); 660 661#$log->debug("add state : " . Dumper($state)); 662 663my$rmcount=0; 664 665foreachmy$filename( @{$state->{args}} ) 666{ 667$filename= filecleanup($filename); 668 669if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 670{ 671print"E cvs remove: file `$filename' still in working directory\n"; 672next; 673} 674 675my$meta=$updater->getmeta($filename); 676my$wrev= revparse($filename); 677 678unless(defined($wrev) ) 679{ 680print"E cvs remove: nothing known about `$filename'\n"; 681next; 682} 683 684if(defined($wrev)and($wrev=~/^-/) ) 685{ 686print"E cvs remove: file `$filename' already scheduled for removal\n"; 687next; 688} 689 690unless($wreveq$meta->{revision} ) 691{ 692# TODO : not sure if the format of this message is quite correct. 693print"E cvs remove: Up to date check failed for `$filename'\n"; 694next; 695} 696 697 698my($filepart,$dirpart) = filenamesplit($filename,1); 699 700print"E cvs remove: scheduling `$filename' for removal\n"; 701 702print"Checked-in$dirpart\n"; 703print"$filename\n"; 704my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 705print"/$filepart/-$wrev//$kopts/\n"; 706 707$rmcount++; 708} 709 710if($rmcount==1) 711{ 712print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 713} 714elsif($rmcount>1) 715{ 716print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 717} 718 719print"ok\n"; 720} 721 722# Modified filename \n 723# Response expected: no. Additional data: mode, \n, file transmission. Send 724# the server a copy of one locally modified file. filename is a file within 725# the most recent directory sent with Directory; it must not contain `/'. 726# If the user is operating on only some files in a directory, only those 727# files need to be included. This can also be sent without Entry, if there 728# is no entry for the file. 729sub req_Modified 730{ 731my($cmd,$data) =@_; 732 733my$mode= <STDIN>; 734defined$mode 735or(print"E end of file reading mode for$data\n"),return; 736chomp$mode; 737my$size= <STDIN>; 738defined$size 739or(print"E end of file reading size of$data\n"),return; 740chomp$size; 741 742# Grab config information 743my$blocksize=8192; 744my$bytesleft=$size; 745my$tmp; 746 747# Get a filehandle/name to write it to 748my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 749 750# Loop over file data writing out to temporary file. 751while($bytesleft) 752{ 753$blocksize=$bytesleftif($bytesleft<$blocksize); 754read STDIN,$tmp,$blocksize; 755print$fh $tmp; 756$bytesleft-=$blocksize; 757} 758 759close$fh 760or(print"E failed to write temporary,$filename:$!\n"),return; 761 762# Ensure we have something sensible for the file mode 763if($mode=~/u=(\w+)/) 764{ 765$mode=$1; 766}else{ 767$mode="rw"; 768} 769 770# Save the file data in $state 771$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 772$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 773$state->{entries}{$state->{directory}.$data}{modified_hash} =`git hash-object$filename`; 774$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 775 776 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 777} 778 779# Unchanged filename\n 780# Response expected: no. Tell the server that filename has not been 781# modified in the checked out directory. The filename is a file within the 782# most recent directory sent with Directory; it must not contain `/'. 783sub req_Unchanged 784{ 785 my ($cmd,$data) =@_; 786 787$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 788 789 #$log->debug("req_Unchanged :$data"); 790} 791 792# Argument text\n 793# Response expected: no. Save argument for use in a subsequent command. 794# Arguments accumulate until an argument-using command is given, at which 795# point they are forgotten. 796# Argumentx text\n 797# Response expected: no. Append\nfollowed by text to the current argument 798# being saved. 799sub req_Argument 800{ 801 my ($cmd,$data) =@_; 802 803 # Argumentx means: append to last Argument (with a newline in front) 804 805$log->debug("$cmd:$data"); 806 807 if ($cmdeq 'Argumentx') { 808 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 809 } else { 810 push @{$state->{arguments}},$data; 811 } 812} 813 814# expand-modules\n 815# Response expected: yes. Expand the modules which are specified in the 816# arguments. Returns the data in Module-expansion responses. Note that the 817# server can assume that this is checkout or export, not rtag or rdiff; the 818# latter do not access the working directory and thus have no need to 819# expand modules on the client side. Expand may not be the best word for 820# what this request does. It does not necessarily tell you all the files 821# contained in a module, for example. Basically it is a way of telling you 822# which working directories the server needs to know about in order to 823# handle a checkout of the specified modules. For example, suppose that the 824# server has a module defined by 825# aliasmodule -a 1dir 826# That is, one can check out aliasmodule and it will take 1dir in the 827# repository and check it out to 1dir in the working directory. Now suppose 828# the client already has this module checked out and is planning on using 829# the co request to update it. Without using expand-modules, the client 830# would have two bad choices: it could either send information about all 831# working directories under the current directory, which could be 832# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 833# stands for 1dir, and neglect to send information for 1dir, which would 834# lead to incorrect operation. With expand-modules, the client would first 835# ask for the module to be expanded: 836sub req_expandmodules 837{ 838 my ($cmd,$data) =@_; 839 840 argsplit(); 841 842$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 843 844 unless ( ref$state->{arguments} eq "ARRAY" ) 845 { 846 print "ok\n"; 847 return; 848 } 849 850 foreach my$module( @{$state->{arguments}} ) 851 { 852$log->debug("SEND : Module-expansion$module"); 853 print "Module-expansion$module\n"; 854 } 855 856 print "ok\n"; 857 statecleanup(); 858} 859 860# co\n 861# Response expected: yes. Get files from the repository. This uses any 862# previous Argument, Directory, Entry, or Modified requests, if they have 863# been sent. Arguments to this command are module names; the client cannot 864# know what directories they correspond to except by (1) just sending the 865# co request, and then seeing what directory names the server sends back in 866# its responses, and (2) the expand-modules request. 867sub req_co 868{ 869 my ($cmd,$data) =@_; 870 871 argsplit("co"); 872 873 # Provide list of modules, if -c was used. 874 if (exists$state->{opt}{c}) { 875 my$showref= `git show-ref --heads`; 876 for my$line(split '\n',$showref) { 877 if ($line=~ m% refs/heads/(.*)$%) { 878 print "M$1\t$1\n"; 879 } 880 } 881 print "ok\n"; 882 return 1; 883 } 884 885 my$module=$state->{args}[0]; 886$state->{module} =$module; 887 my$checkout_path=$module; 888 889 # use the user specified directory if we're given it 890$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 891 892$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 893 894$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 895 896$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 897 898# Grab a handle to the SQLite db and do any necessary updates 899my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 900$updater->update(); 901 902$checkout_path=~ s|/$||;# get rid of trailing slashes 903 904# Eclipse seems to need the Clear-sticky command 905# to prepare the 'Entries' file for the new directory. 906print"Clear-sticky$checkout_path/\n"; 907print$state->{CVSROOT} ."/$module/\n"; 908print"Clear-static-directory$checkout_path/\n"; 909print$state->{CVSROOT} ."/$module/\n"; 910print"Clear-sticky$checkout_path/\n";# yes, twice 911print$state->{CVSROOT} ."/$module/\n"; 912print"Template$checkout_path/\n"; 913print$state->{CVSROOT} ."/$module/\n"; 914print"0\n"; 915 916# instruct the client that we're checking out to $checkout_path 917print"E cvs checkout: Updating$checkout_path\n"; 918 919my%seendirs= (); 920my$lastdir=''; 921 922# recursive 923sub prepdir { 924my($dir,$repodir,$remotedir,$seendirs) =@_; 925my$parent= dirname($dir); 926$dir=~ s|/+$||; 927$repodir=~ s|/+$||; 928$remotedir=~ s|/+$||; 929$parent=~ s|/+$||; 930$log->debug("announcedir$dir,$repodir,$remotedir"); 931 932if($parenteq'.'||$parenteq'./') { 933$parent=''; 934} 935# recurse to announce unseen parents first 936if(length($parent) && !exists($seendirs->{$parent})) { 937 prepdir($parent,$repodir,$remotedir,$seendirs); 938} 939# Announce that we are going to modify at the parent level 940if($parent) { 941print"E cvs checkout: Updating$remotedir/$parent\n"; 942}else{ 943print"E cvs checkout: Updating$remotedir\n"; 944} 945print"Clear-sticky$remotedir/$parent/\n"; 946print"$repodir/$parent/\n"; 947 948print"Clear-static-directory$remotedir/$dir/\n"; 949print"$repodir/$dir/\n"; 950print"Clear-sticky$remotedir/$parent/\n";# yes, twice 951print"$repodir/$parent/\n"; 952print"Template$remotedir/$dir/\n"; 953print"$repodir/$dir/\n"; 954print"0\n"; 955 956$seendirs->{$dir} =1; 957} 958 959foreachmy$git( @{$updater->gethead} ) 960{ 961# Don't want to check out deleted files 962next if($git->{filehash}eq"deleted"); 963 964my$fullName=$git->{name}; 965($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 966 967if(length($git->{dir}) &&$git->{dir}ne'./' 968&&$git->{dir}ne$lastdir) { 969unless(exists($seendirs{$git->{dir}})) { 970 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 971$checkout_path, \%seendirs); 972$lastdir=$git->{dir}; 973$seendirs{$git->{dir}} =1; 974} 975print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 976} 977 978# modification time of this file 979print"Mod-time$git->{modified}\n"; 980 981# print some information to the client 982if(defined($git->{dir} )and$git->{dir}ne"./") 983{ 984print"M U$checkout_path/$git->{dir}$git->{name}\n"; 985}else{ 986print"M U$checkout_path/$git->{name}\n"; 987} 988 989# instruct client we're sending a file to put in this path 990print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 991 992print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 993 994# this is an "entries" line 995my$kopts= kopts_from_path($fullName,"sha1",$git->{filehash}); 996print"/$git->{name}/$git->{revision}//$kopts/\n"; 997# permissions 998print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 9991000# transmit file1001 transmitfile($git->{filehash});1002}10031004print"ok\n";10051006 statecleanup();1007}10081009# update \n1010# Response expected: yes. Actually do a cvs update command. This uses any1011# previous Argument, Directory, Entry, or Modified requests, if they have1012# been sent. The last Directory sent specifies the working directory at the1013# time of the operation. The -I option is not used--files which the client1014# can decide whether to ignore are not mentioned and the client sends the1015# Questionable request for others.1016sub req_update1017{1018my($cmd,$data) =@_;10191020$log->debug("req_update : ". (defined($data) ?$data:"[NULL]"));10211022 argsplit("update");10231024#1025# It may just be a client exploring the available heads/modules1026# in that case, list them as top level directories and leave it1027# at that. Eclipse uses this technique to offer you a list of1028# projects (heads in this case) to checkout.1029#1030if($state->{module}eq'') {1031my$showref=`git show-ref --heads`;1032print"E cvs update: Updating .\n";1033formy$line(split'\n',$showref) {1034if($line=~ m% refs/heads/(.*)$%) {1035print"E cvs update: New directory `$1'\n";1036}1037}1038print"ok\n";1039return1;1040}104110421043# Grab a handle to the SQLite db and do any necessary updates1044my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);10451046$updater->update();10471048 argsfromdir($updater);10491050#$log->debug("update state : " . Dumper($state));10511052my$last_dirname="///";10531054# foreach file specified on the command line ...1055foreachmy$filename( @{$state->{args}} )1056{1057$filename= filecleanup($filename);10581059$log->debug("Processing file$filename");10601061unless($state->{globaloptions}{-Q} ||$state->{globaloptions}{-q} )1062{1063my$cur_dirname= dirname($filename);1064if($cur_dirnamene$last_dirname)1065{1066$last_dirname=$cur_dirname;1067if($cur_dirnameeq"")1068{1069$cur_dirname=".";1070}1071print"E cvs update: Updating$cur_dirname\n";1072}1073}10741075# if we have a -C we should pretend we never saw modified stuff1076if(exists($state->{opt}{C} ) )1077{1078delete$state->{entries}{$filename}{modified_hash};1079delete$state->{entries}{$filename}{modified_filename};1080$state->{entries}{$filename}{unchanged} =1;1081}10821083my$meta;1084if(defined($state->{opt}{r})and$state->{opt}{r} =~/^(1\.\d+)$/)1085{1086$meta=$updater->getmeta($filename,$1);1087}else{1088$meta=$updater->getmeta($filename);1089}10901091# If -p was given, "print" the contents of the requested revision.1092if(exists($state->{opt}{p} ) ) {1093if(defined($meta->{revision} ) ) {1094$log->info("Printing '$filename' revision ".$meta->{revision});10951096 transmitfile($meta->{filehash}, {print=>1});1097}10981099next;1100}11011102if( !defined$meta)1103{1104$meta= {1105 name =>$filename,1106 revision =>'0',1107 filehash =>'added'1108};1109}11101111my$oldmeta=$meta;11121113my$wrev= revparse($filename);11141115# If the working copy is an old revision, lets get that version too for comparison.1116if(defined($wrev)and$wrevne$meta->{revision} )1117{1118$oldmeta=$updater->getmeta($filename,$wrev);1119}11201121#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");11221123# Files are up to date if the working copy and repo copy have the same revision,1124# and the working copy is unmodified _and_ the user hasn't specified -C1125next if(defined($wrev)1126and defined($meta->{revision})1127and$wreveq$meta->{revision}1128and$state->{entries}{$filename}{unchanged}1129and not exists($state->{opt}{C} ) );11301131# If the working copy and repo copy have the same revision,1132# but the working copy is modified, tell the client it's modified1133if(defined($wrev)1134and defined($meta->{revision})1135and$wreveq$meta->{revision}1136and defined($state->{entries}{$filename}{modified_hash})1137and not exists($state->{opt}{C} ) )1138{1139$log->info("Tell the client the file is modified");1140print"MT text M\n";1141print"MT fname$filename\n";1142print"MT newline\n";1143next;1144}11451146if($meta->{filehash}eq"deleted")1147{1148# TODO: If it has been modified in the sandbox, error out1149# with the appropriate message, rather than deleting a modified1150# file.11511152my($filepart,$dirpart) = filenamesplit($filename,1);11531154$log->info("Removing '$filename' from working copy (no longer in the repo)");11551156print"E cvs update: `$filename' is no longer in the repository\n";1157# Don't want to actually _DO_ the update if -n specified1158unless($state->{globaloptions}{-n} ) {1159print"Removed$dirpart\n";1160print"$filepart\n";1161}1162}1163elsif(not defined($state->{entries}{$filename}{modified_hash} )1164or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1165or$meta->{filehash}eq'added')1166{1167# normal update, just send the new revision (either U=Update,1168# or A=Add, or R=Remove)1169if(defined($wrev) && ($wrev=~/^-/) )1170{1171$log->info("Tell the client the file is scheduled for removal");1172print"MT text R\n";1173print"MT fname$filename\n";1174print"MT newline\n";1175next;1176}1177elsif( (!defined($wrev) ||$wreveq'0') &&1178(!defined($meta->{revision}) ||$meta->{revision}eq'0') )1179{1180$log->info("Tell the client the file is scheduled for addition");1181print"MT text A\n";1182print"MT fname$filename\n";1183print"MT newline\n";1184next;11851186}1187else{1188$log->info("UpdatingX3 '$filename' to ".$meta->{revision});1189print"MT +updated\n";1190print"MT text U\n";1191print"MT fname$filename\n";1192print"MT newline\n";1193print"MT -updated\n";1194}11951196my($filepart,$dirpart) = filenamesplit($filename,1);11971198# Don't want to actually _DO_ the update if -n specified1199unless($state->{globaloptions}{-n} )1200{1201if(defined($wrev) )1202{1203# instruct client we're sending a file to put in this path as a replacement1204print"Update-existing$dirpart\n";1205$log->debug("Updating existing file 'Update-existing$dirpart'");1206}else{1207# instruct client we're sending a file to put in this path as a new file1208print"Clear-static-directory$dirpart\n";1209print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1210print"Clear-sticky$dirpart\n";1211print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";12121213$log->debug("Creating new file 'Created$dirpart'");1214print"Created$dirpart\n";1215}1216print$state->{CVSROOT} ."/$state->{module}/$filename\n";12171218# this is an "entries" line1219my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1220$log->debug("/$filepart/$meta->{revision}//$kopts/");1221print"/$filepart/$meta->{revision}//$kopts/\n";12221223# permissions1224$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1225print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";12261227# transmit file1228 transmitfile($meta->{filehash});1229}1230}else{1231my($filepart,$dirpart) = filenamesplit($meta->{name},1);12321233my$mergeDir= setupTmpDir();12341235my$file_local=$filepart.".mine";1236my$mergedFile="$mergeDir/$file_local";1237system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1238my$file_old=$filepart.".".$oldmeta->{revision};1239 transmitfile($oldmeta->{filehash}, { targetfile =>$file_old});1240my$file_new=$filepart.".".$meta->{revision};1241 transmitfile($meta->{filehash}, { targetfile =>$file_new});12421243# we need to merge with the local changes ( M=successful merge, C=conflict merge )1244$log->info("Merging$file_local,$file_old,$file_new");1245print"M Merging differences between$oldmeta->{revision} and$meta->{revision} into$filename\n";12461247$log->debug("Temporary directory for merge is$mergeDir");12481249my$return=system("git","merge-file",$file_local,$file_old,$file_new);1250$return>>=8;12511252 cleanupTmpDir();12531254if($return==0)1255{1256$log->info("Merged successfully");1257print"M M$filename\n";1258$log->debug("Merged$dirpart");12591260# Don't want to actually _DO_ the update if -n specified1261unless($state->{globaloptions}{-n} )1262{1263print"Merged$dirpart\n";1264$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1265print$state->{CVSROOT} ."/$state->{module}/$filename\n";1266my$kopts= kopts_from_path("$dirpart/$filepart",1267"file",$mergedFile);1268$log->debug("/$filepart/$meta->{revision}//$kopts/");1269print"/$filepart/$meta->{revision}//$kopts/\n";1270}1271}1272elsif($return==1)1273{1274$log->info("Merged with conflicts");1275print"E cvs update: conflicts found in$filename\n";1276print"M C$filename\n";12771278# Don't want to actually _DO_ the update if -n specified1279unless($state->{globaloptions}{-n} )1280{1281print"Merged$dirpart\n";1282print$state->{CVSROOT} ."/$state->{module}/$filename\n";1283my$kopts= kopts_from_path("$dirpart/$filepart",1284"file",$mergedFile);1285print"/$filepart/$meta->{revision}/+/$kopts/\n";1286}1287}1288else1289{1290$log->warn("Merge failed");1291next;1292}12931294# Don't want to actually _DO_ the update if -n specified1295unless($state->{globaloptions}{-n} )1296{1297# permissions1298$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1299print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";13001301# transmit file, format is single integer on a line by itself (file1302# size) followed by the file contents1303# TODO : we should copy files in blocks1304my$data=`cat$mergedFile`;1305$log->debug("File size : " . length($data));1306 print length($data) . "\n";1307 print$data;1308 }1309 }13101311 }13121313 print "ok\n";1314}13151316sub req_ci1317{1318 my ($cmd,$data) =@_;13191320 argsplit("ci");13211322 #$log->debug("State : " . Dumper($state));13231324$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));13251326 if ($state->{method} eq 'pserver' and$state->{user} eq 'anonymous' )1327 {1328 print "error 1 anonymous user cannot commit via pserver\n";1329 cleanupWorkTree();1330 exit;1331 }13321333 if ( -e$state->{CVSROOT} . "/index" )1334 {1335$log->warn("file 'index' already exists in the git repository");1336 print "error 1 Index already exists in git repo\n";1337 cleanupWorkTree();1338 exit;1339 }13401341 # Grab a handle to the SQLite db and do any necessary updates1342 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1343$updater->update();13441345 # Remember where the head was at the beginning.1346 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1347 chomp$parenthash;1348 if ($parenthash!~ /^[0-9a-f]{40}$/) {1349 print "error 1 pserver cannot find the current HEAD of module";1350 cleanupWorkTree();1351 exit;1352 }13531354 setupWorkTree($parenthash);13551356$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");13571358$log->info("Created index '$work->{index}' for head$state->{module} - exit status$?");13591360 my@committedfiles= ();1361 my%oldmeta;13621363 # foreach file specified on the command line ...1364 foreach my$filename( @{$state->{args}} )1365 {1366 my$committedfile=$filename;1367$filename= filecleanup($filename);13681369 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );13701371 my$meta=$updater->getmeta($filename);1372$oldmeta{$filename} =$meta;13731374 my$wrev= revparse($filename);13751376 my ($filepart,$dirpart) = filenamesplit($filename);13771378 # do a checkout of the file if it is part of this tree1379 if ($wrev) {1380 system('git', 'checkout-index', '-f', '-u',$filename);1381 unless ($?== 0) {1382 die "Error running git-checkout-index -f -u$filename:$!";1383 }1384 }13851386 my$addflag= 0;1387 my$rmflag= 0;1388$rmflag= 1 if ( defined($wrev) and ($wrev=~/^-/) );1389$addflag= 1 unless ( -e$filename);13901391 # Do up to date checking1392 unless ($addflagor$wreveq$meta->{revision} or1393 ($rmflagand$wreveq "-$meta->{revision}" ) )1394 {1395 # fail everything if an up to date check fails1396 print "error 1 Up to date check failed for$filename\n";1397 cleanupWorkTree();1398 exit;1399 }14001401 push@committedfiles,$committedfile;1402$log->info("Committing$filename");14031404 system("mkdir","-p",$dirpart) unless ( -d$dirpart);14051406 unless ($rmflag)1407 {1408$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1409 rename$state->{entries}{$filename}{modified_filename},$filename;14101411 # Calculate modes to remove1412 my$invmode= "";1413 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }14141415$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1416 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1417 }14181419 if ($rmflag)1420 {1421$log->info("Removing file '$filename'");1422 unlink($filename);1423 system("git", "update-index", "--remove",$filename);1424 }1425 elsif ($addflag)1426 {1427$log->info("Adding file '$filename'");1428 system("git", "update-index", "--add",$filename);1429 } else {1430$log->info("UpdatingX2 file '$filename'");1431 system("git", "update-index",$filename);1432 }1433 }14341435 unless ( scalar(@committedfiles) > 0 )1436 {1437 print "E No files to commit\n";1438 print "ok\n";1439 cleanupWorkTree();1440 return;1441 }14421443 my$treehash= `git write-tree`;1444 chomp$treehash;14451446$log->debug("Treehash :$treehash, Parenthash :$parenthash");14471448 # write our commit message out if we have one ...1449 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1450 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1451 if ( defined ($cfg->{gitcvs}{commitmsgannotation} ) ) {1452 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/) {1453 print$msg_fh"\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"1454 }1455 } else {1456 print$msg_fh"\n\nvia git-CVS emulator\n";1457 }1458 close$msg_fh;14591460 my$commithash= `git commit-tree $treehash-p $parenthash<$msg_filename`;1461chomp($commithash);1462$log->info("Commit hash :$commithash");14631464unless($commithash=~/[a-zA-Z0-9]{40}/)1465{1466$log->warn("Commit failed (Invalid commit hash)");1467print"error 1 Commit failed (unknown reason)\n";1468 cleanupWorkTree();1469exit;1470}14711472### Emulate git-receive-pack by running hooks/update1473my@hook= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1474$parenthash,$commithash);1475if( -x $hook[0] ) {1476unless(system(@hook) ==0)1477{1478$log->warn("Commit failed (update hook declined to update ref)");1479print"error 1 Commit failed (update hook declined)\n";1480 cleanupWorkTree();1481exit;1482}1483}14841485### Update the ref1486if(system(qw(git update-ref -m),"cvsserver ci",1487"refs/heads/$state->{module}",$commithash,$parenthash)) {1488$log->warn("update-ref for$state->{module} failed.");1489print"error 1 Cannot commit -- update first\n";1490 cleanupWorkTree();1491exit;1492}14931494### Emulate git-receive-pack by running hooks/post-receive1495my$hook=$ENV{GIT_DIR}.'hooks/post-receive';1496if( -x $hook) {1497open(my$pipe,"|$hook") ||die"can't fork$!";14981499local$SIG{PIPE} =sub{die'pipe broke'};15001501print$pipe"$parenthash$commithashrefs/heads/$state->{module}\n";15021503close$pipe||die"bad pipe:$!$?";1504}15051506$updater->update();15071508### Then hooks/post-update1509$hook=$ENV{GIT_DIR}.'hooks/post-update';1510if(-x $hook) {1511system($hook,"refs/heads/$state->{module}");1512}15131514# foreach file specified on the command line ...1515foreachmy$filename(@committedfiles)1516{1517$filename= filecleanup($filename);15181519my$meta=$updater->getmeta($filename);1520unless(defined$meta->{revision}) {1521$meta->{revision} ="1.1";1522}15231524my($filepart,$dirpart) = filenamesplit($filename,1);15251526$log->debug("Checked-in$dirpart:$filename");15271528print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1529if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1530{1531print"M new revision: delete; previous revision:$oldmeta{$filename}{revision}\n";1532print"Remove-entry$dirpart\n";1533print"$filename\n";1534}else{1535if($meta->{revision}eq"1.1") {1536print"M initial revision: 1.1\n";1537}else{1538print"M new revision:$meta->{revision}; previous revision:$oldmeta{$filename}{revision}\n";1539}1540print"Checked-in$dirpart\n";1541print"$filename\n";1542my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1543print"/$filepart/$meta->{revision}//$kopts/\n";1544}1545}15461547 cleanupWorkTree();1548print"ok\n";1549}15501551sub req_status1552{1553my($cmd,$data) =@_;15541555 argsplit("status");15561557$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1558#$log->debug("status state : " . Dumper($state));15591560# Grab a handle to the SQLite db and do any necessary updates1561my$updater;1562$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1563$updater->update();15641565# if no files were specified, we need to work out what files we should1566# be providing status on ...1567 argsfromdir($updater);15681569# foreach file specified on the command line ...1570foreachmy$filename( @{$state->{args}} )1571{1572$filename= filecleanup($filename);15731574if(exists($state->{opt}{l}) &&1575index($filename,'/',length($state->{prependdir})) >=0)1576{1577next;1578}15791580my$meta=$updater->getmeta($filename);1581my$oldmeta=$meta;15821583my$wrev= revparse($filename);15841585# If the working copy is an old revision, lets get that1586# version too for comparison.1587if(defined($wrev)and$wrevne$meta->{revision} )1588{1589$oldmeta=$updater->getmeta($filename,$wrev);1590}15911592# TODO : All possible statuses aren't yet implemented1593my$status;1594# Files are up to date if the working copy and repo copy have1595# the same revision, and the working copy is unmodified1596if(defined($wrev)and defined($meta->{revision})and1597$wreveq$meta->{revision}and1598( ($state->{entries}{$filename}{unchanged}and1599(not defined($state->{entries}{$filename}{conflict} )or1600$state->{entries}{$filename}{conflict} !~/^\+=/) )or1601(defined($state->{entries}{$filename}{modified_hash})and1602$state->{entries}{$filename}{modified_hash}eq1603$meta->{filehash} ) ) )1604{1605$status="Up-to-date"1606}16071608# Need checkout if the working copy has a different (usually1609# older) revision than the repo copy, and the working copy is1610# unmodified1611if(defined($wrev)and defined($meta->{revision} )and1612$meta->{revision}ne$wrevand1613($state->{entries}{$filename}{unchanged}or1614(defined($state->{entries}{$filename}{modified_hash})and1615$state->{entries}{$filename}{modified_hash}eq1616$oldmeta->{filehash} ) ) )1617{1618$status||="Needs Checkout";1619}16201621# Need checkout if it exists in the repo but doesn't have a working1622# copy1623if(not defined($wrev)and defined($meta->{revision} ) )1624{1625$status||="Needs Checkout";1626}16271628# Locally modified if working copy and repo copy have the1629# same revision but there are local changes1630if(defined($wrev)and defined($meta->{revision})and1631$wreveq$meta->{revision}and1632$state->{entries}{$filename}{modified_filename} )1633{1634$status||="Locally Modified";1635}16361637# Needs Merge if working copy revision is different1638# (usually older) than repo copy and there are local changes1639if(defined($wrev)and defined($meta->{revision} )and1640$meta->{revision}ne$wrevand1641$state->{entries}{$filename}{modified_filename} )1642{1643$status||="Needs Merge";1644}16451646if(defined($state->{entries}{$filename}{revision} )and1647not defined($meta->{revision} ) )1648{1649$status||="Locally Added";1650}1651if(defined($wrev)and defined($meta->{revision} )and1652$wreveq"-$meta->{revision}")1653{1654$status||="Locally Removed";1655}1656if(defined($state->{entries}{$filename}{conflict} )and1657$state->{entries}{$filename}{conflict} =~/^\+=/)1658{1659$status||="Unresolved Conflict";1660}1661if(0)1662{1663$status||="File had conflicts on merge";1664}16651666$status||="Unknown";16671668my($filepart) = filenamesplit($filename);16691670print"M =======". ("=" x 60) ."\n";1671print"M File:$filepart\tStatus:$status\n";1672if(defined($state->{entries}{$filename}{revision}) )1673{1674print"M Working revision:\t".1675$state->{entries}{$filename}{revision} ."\n";1676}else{1677print"M Working revision:\tNo entry for$filename\n";1678}1679if(defined($meta->{revision}) )1680{1681print"M Repository revision:\t".1682$meta->{revision} .1683"\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1684my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};1685my($tag)=($tagOrDate=~m/^T(.+)$/);1686if( !defined($tag) )1687{1688$tag="(none)";1689}1690print"M Sticky Tag:\t\t$tag\n";1691my($date)=($tagOrDate=~m/^D(.+)$/);1692if( !defined($date) )1693{1694$date="(none)";1695}1696print"M Sticky Date:\t\t$date\n";1697my($options)=$state->{entries}{$filename}{options};1698if($optionseq"")1699{1700$options="(none)";1701}1702print"M Sticky Options:\t\t$options\n";1703}else{1704print"M Repository revision:\tNo revision control file\n";1705}1706print"M\n";1707}17081709print"ok\n";1710}17111712sub req_diff1713{1714my($cmd,$data) =@_;17151716 argsplit("diff");17171718$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1719#$log->debug("status state : " . Dumper($state));17201721my($revision1,$revision2);1722if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1723{1724$revision1=$state->{opt}{r}[0];1725$revision2=$state->{opt}{r}[1];1726}else{1727$revision1=$state->{opt}{r};1728}17291730$log->debug("Diffing revisions ".1731(defined($revision1) ?$revision1:"[NULL]") .1732" and ". (defined($revision2) ?$revision2:"[NULL]") );17331734# Grab a handle to the SQLite db and do any necessary updates1735my$updater;1736$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1737$updater->update();17381739# if no files were specified, we need to work out what files we should1740# be providing status on ...1741 argsfromdir($updater);17421743# foreach file specified on the command line ...1744foreachmy$filename( @{$state->{args}} )1745{1746$filename= filecleanup($filename);17471748my($fh,$file1,$file2,$meta1,$meta2,$filediff);17491750my$wrev= revparse($filename);17511752# We need _something_ to diff against1753next unless(defined($wrev) );17541755# if we have a -r switch, use it1756if(defined($revision1) )1757{1758(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1759$meta1=$updater->getmeta($filename,$revision1);1760unless(defined($meta1)and$meta1->{filehash}ne"deleted")1761{1762print"E File$filenameat revision$revision1doesn't exist\n";1763next;1764}1765 transmitfile($meta1->{filehash}, { targetfile =>$file1});1766}1767# otherwise we just use the working copy revision1768else1769{1770(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1771$meta1=$updater->getmeta($filename,$wrev);1772 transmitfile($meta1->{filehash}, { targetfile =>$file1});1773}17741775# if we have a second -r switch, use it too1776if(defined($revision2) )1777{1778(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1779$meta2=$updater->getmeta($filename,$revision2);17801781unless(defined($meta2)and$meta2->{filehash}ne"deleted")1782{1783print"E File$filenameat revision$revision2doesn't exist\n";1784next;1785}17861787 transmitfile($meta2->{filehash}, { targetfile =>$file2});1788}1789# otherwise we just use the working copy1790else1791{1792$file2=$state->{entries}{$filename}{modified_filename};1793}17941795# if we have been given -r, and we don't have a $file2 yet, lets1796# get one1797if(defined($revision1)and not defined($file2) )1798{1799(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1800$meta2=$updater->getmeta($filename,$wrev);1801 transmitfile($meta2->{filehash}, { targetfile =>$file2});1802}18031804# We need to have retrieved something useful1805next unless(defined($meta1) );18061807# Files to date if the working copy and repo copy have the same1808# revision, and the working copy is unmodified1809if(not defined($meta2)and$wreveq$meta1->{revision}and1810( ($state->{entries}{$filename}{unchanged}and1811(not defined($state->{entries}{$filename}{conflict} )or1812$state->{entries}{$filename}{conflict} !~/^\+=/) )or1813(defined($state->{entries}{$filename}{modified_hash})and1814$state->{entries}{$filename}{modified_hash}eq1815$meta1->{filehash} ) ) )1816{1817next;1818}18191820# Apparently we only show diffs for locally modified files1821unless(defined($meta2)or1822defined($state->{entries}{$filename}{modified_filename} ) )1823{1824next;1825}18261827print"M Index:$filename\n";1828print"M =======". ("=" x 60) ."\n";1829print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1830if(defined($meta1) )1831{1832print"M retrieving revision$meta1->{revision}\n"1833}1834if(defined($meta2) )1835{1836print"M retrieving revision$meta2->{revision}\n"1837}1838print"M diff ";1839foreachmy$opt(keys%{$state->{opt}} )1840{1841if(ref$state->{opt}{$opt}eq"ARRAY")1842{1843foreachmy$value( @{$state->{opt}{$opt}} )1844{1845print"-$opt$value";1846}1847}else{1848print"-$opt";1849if(defined($state->{opt}{$opt} ) )1850{1851print"$state->{opt}{$opt} "1852}1853}1854}1855print"$filename\n";18561857$log->info("Diffing$filename-r$meta1->{revision} -r ".1858($meta2->{revision}or"workingcopy"));18591860($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);18611862if(exists$state->{opt}{u} )1863{1864system("diff -u -L '$filenamerevision$meta1->{revision}'".1865" -L '$filename".1866(defined($meta2->{revision}) ?1867"revision$meta2->{revision}":1868"working copy") .1869"'$file1$file2>$filediff");1870}else{1871system("diff$file1$file2>$filediff");1872}18731874while( <$fh> )1875{1876print"M$_";1877}1878close$fh;1879}18801881print"ok\n";1882}18831884sub req_log1885{1886my($cmd,$data) =@_;18871888 argsplit("log");18891890$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1891#$log->debug("log state : " . Dumper($state));18921893my($revFilter);1894if(defined($state->{opt}{r} ) )1895{1896$revFilter=$state->{opt}{r};1897}18981899# Grab a handle to the SQLite db and do any necessary updates1900my$updater;1901$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1902$updater->update();19031904# if no files were specified, we need to work out what files we1905# should be providing status on ...1906 argsfromdir($updater);19071908# foreach file specified on the command line ...1909foreachmy$filename( @{$state->{args}} )1910{1911$filename= filecleanup($filename);19121913my$headmeta=$updater->getmeta($filename);19141915my($revisions,$totalrevisions) =$updater->getlog($filename,1916$revFilter);19171918next unless(scalar(@$revisions) );19191920print"M\n";1921print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1922print"M Working file:$filename\n";1923print"M head:$headmeta->{revision}\n";1924print"M branch:\n";1925print"M locks: strict\n";1926print"M access list:\n";1927print"M symbolic names:\n";1928print"M keyword substitution: kv\n";1929print"M total revisions:$totalrevisions;\tselected revisions: ".1930scalar(@$revisions) ."\n";1931print"M description:\n";19321933foreachmy$revision(@$revisions)1934{1935print"M ----------------------------\n";1936print"M revision$revision->{revision}\n";1937# reformat the date for log output1938if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and1939defined($DATE_LIST->{$2}) )1940{1941$revision->{modified} =sprintf('%04d/%02d/%02d%s',1942$3,$DATE_LIST->{$2},$1,$4);1943}1944$revision->{author} = cvs_author($revision->{author});1945print"M date:$revision->{modified};".1946" author:$revision->{author}; state: ".1947($revision->{filehash}eq"deleted"?"dead":"Exp") .1948"; lines: +2 -3\n";1949my$commitmessage;1950$commitmessage=$updater->commitmessage($revision->{commithash});1951$commitmessage=~s/^/M /mg;1952print$commitmessage."\n";1953}1954print"M =======". ("=" x 70) ."\n";1955}19561957print"ok\n";1958}19591960sub req_annotate1961{1962my($cmd,$data) =@_;19631964 argsplit("annotate");19651966$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1967#$log->debug("status state : " . Dumper($state));19681969# Grab a handle to the SQLite db and do any necessary updates1970my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1971$updater->update();19721973# if no files were specified, we need to work out what files we should be providing annotate on ...1974 argsfromdir($updater);19751976# we'll need a temporary checkout dir1977 setupWorkTree();19781979$log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");19801981# foreach file specified on the command line ...1982foreachmy$filename( @{$state->{args}} )1983{1984$filename= filecleanup($filename);19851986my$meta=$updater->getmeta($filename);19871988next unless($meta->{revision} );19891990# get all the commits that this file was in1991# in dense format -- aka skip dead revisions1992my$revisions=$updater->gethistorydense($filename);1993my$lastseenin=$revisions->[0][2];19941995# populate the temporary index based on the latest commit were we saw1996# the file -- but do it cheaply without checking out any files1997# TODO: if we got a revision from the client, use that instead1998# to look up the commithash in sqlite (still good to default to1999# the current head as we do now)2000system("git","read-tree",$lastseenin);2001unless($?==0)2002{2003print"E error running git-read-tree$lastseenin$ENV{GIT_INDEX_FILE}$!\n";2004return;2005}2006$log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit$lastseenin- exit status$?");20072008# do a checkout of the file2009system('git','checkout-index','-f','-u',$filename);2010unless($?==0) {2011print"E error running git-checkout-index -f -u$filename:$!\n";2012return;2013}20142015$log->info("Annotate$filename");20162017# Prepare a file with the commits from the linearized2018# history that annotate should know about. This prevents2019# git-jsannotate telling us about commits we are hiding2020# from the client.20212022my$a_hints="$work->{workDir}/.annotate_hints";2023if(!open(ANNOTATEHINTS,'>',$a_hints)) {2024print"E failed to open '$a_hints' for writing:$!\n";2025return;2026}2027for(my$i=0;$i<@$revisions;$i++)2028{2029print ANNOTATEHINTS $revisions->[$i][2];2030if($i+1<@$revisions) {# have we got a parent?2031print ANNOTATEHINTS ' '.$revisions->[$i+1][2];2032}2033print ANNOTATEHINTS "\n";2034}20352036print ANNOTATEHINTS "\n";2037close ANNOTATEHINTS2038or(print"E failed to write$a_hints:$!\n"),return;20392040my@cmd= (qw(git annotate -l -S),$a_hints,$filename);2041if(!open(ANNOTATE,"-|",@cmd)) {2042print"E error invoking ".join(' ',@cmd) .":$!\n";2043return;2044}2045my$metadata= {};2046print"E Annotations for$filename\n";2047print"E ***************\n";2048while( <ANNOTATE> )2049{2050if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)2051{2052my$commithash=$1;2053my$data=$2;2054unless(defined($metadata->{$commithash} ) )2055{2056$metadata->{$commithash} =$updater->getmeta($filename,$commithash);2057$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});2058$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);2059}2060printf("M %-7s (%-8s%10s):%s\n",2061$metadata->{$commithash}{revision},2062$metadata->{$commithash}{author},2063$metadata->{$commithash}{modified},2064$data2065);2066}else{2067$log->warn("Error in annotate output! LINE:$_");2068print"E Annotate error\n";2069next;2070}2071}2072close ANNOTATE;2073}20742075# done; get out of the tempdir2076 cleanupWorkTree();20772078print"ok\n";20792080}20812082# This method takes the state->{arguments} array and produces two new arrays.2083# The first is $state->{args} which is everything before the '--' argument, and2084# the second is $state->{files} which is everything after it.2085sub argsplit2086{2087$state->{args} = [];2088$state->{files} = [];2089$state->{opt} = {};20902091return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");20922093my$type=shift;20942095if(defined($type) )2096{2097my$opt= {};2098$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");2099$opt= { v =>0, l =>0, R =>0}if($typeeq"status");2100$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");2101$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");2102$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");2103$opt= { k =>1, m =>1}if($typeeq"add");2104$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");2105$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");210621072108while(scalar( @{$state->{arguments}} ) >0)2109{2110my$arg=shift@{$state->{arguments}};21112112next if($argeq"--");2113next unless($arg=~/\S/);21142115# if the argument looks like a switch2116if($arg=~/^-(\w)(.*)/)2117{2118# if it's a switch that takes an argument2119if($opt->{$1} )2120{2121# If this switch has already been provided2122if($opt->{$1} >1and exists($state->{opt}{$1} ) )2123{2124$state->{opt}{$1} = [$state->{opt}{$1} ];2125if(length($2) >0)2126{2127push@{$state->{opt}{$1}},$2;2128}else{2129push@{$state->{opt}{$1}},shift@{$state->{arguments}};2130}2131}else{2132# if there's extra data in the arg, use that as the argument for the switch2133if(length($2) >0)2134{2135$state->{opt}{$1} =$2;2136}else{2137$state->{opt}{$1} =shift@{$state->{arguments}};2138}2139}2140}else{2141$state->{opt}{$1} =undef;2142}2143}2144else2145{2146push@{$state->{args}},$arg;2147}2148}2149}2150else2151{2152my$mode=0;21532154foreachmy$value( @{$state->{arguments}} )2155{2156if($valueeq"--")2157{2158$mode++;2159next;2160}2161push@{$state->{args}},$valueif($mode==0);2162push@{$state->{files}},$valueif($mode==1);2163}2164}2165}21662167# This method uses $state->{directory} to populate $state->{args} with a list of filenames2168sub argsfromdir2169{2170my$updater=shift;21712172$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");21732174return if(scalar( @{$state->{args}} ) >1);21752176my@gethead= @{$updater->gethead};21772178# push added files2179foreachmy$file(keys%{$state->{entries}}) {2180if(exists$state->{entries}{$file}{revision} &&2181$state->{entries}{$file}{revision}eq'0')2182{2183push@gethead, { name =>$file, filehash =>'added'};2184}2185}21862187if(scalar(@{$state->{args}}) ==1)2188{2189my$arg=$state->{args}[0];2190$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );21912192$log->info("Only one arg specified, checking for directory expansion on '$arg'");21932194foreachmy$file(@gethead)2195{2196next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2197next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);2198push@{$state->{args}},$file->{name};2199}22002201shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);2202}else{2203$log->info("Only one arg specified, populating file list automatically");22042205$state->{args} = [];22062207foreachmy$file(@gethead)2208{2209next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2210next unless($file->{name} =~s/^$state->{prependdir}//);2211push@{$state->{args}},$file->{name};2212}2213}2214}22152216# This method cleans up the $state variable after a command that uses arguments has run2217sub statecleanup2218{2219$state->{files} = [];2220$state->{args} = [];2221$state->{arguments} = [];2222$state->{entries} = {};2223}22242225# Return working directory CVS revision "1.X" out2226# of the the working directory "entries" state, for the given filename.2227# This is prefixed with a dash if the file is scheduled for removal2228# when it is committed.2229sub revparse2230{2231my$filename=shift;22322233return$state->{entries}{$filename}{revision};2234}22352236# This method takes a file hash and does a CVS "file transfer". Its2237# exact behaviour depends on a second, optional hash table argument:2238# - If $options->{targetfile}, dump the contents to that file;2239# - If $options->{print}, use M/MT to transmit the contents one line2240# at a time;2241# - Otherwise, transmit the size of the file, followed by the file2242# contents.2243sub transmitfile2244{2245my$filehash=shift;2246my$options=shift;22472248if(defined($filehash)and$filehasheq"deleted")2249{2250$log->warn("filehash is 'deleted'");2251return;2252}22532254die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);22552256my$type=`git cat-file -t$filehash`;2257 chomp$type;22582259 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );22602261 my$size= `git cat-file -s $filehash`;2262chomp$size;22632264$log->debug("transmitfile($filehash) size=$size, type=$type");22652266if(open my$fh,'-|',"git","cat-file","blob",$filehash)2267{2268if(defined($options->{targetfile} ) )2269{2270my$targetfile=$options->{targetfile};2271open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2272print NEWFILE $_while( <$fh> );2273close NEWFILE or die("Failed to write '$targetfile':$!");2274}elsif(defined($options->{print} ) &&$options->{print} ) {2275while( <$fh> ) {2276if(/\n\z/) {2277print'M ',$_;2278}else{2279print'MT text ',$_,"\n";2280}2281}2282}else{2283print"$size\n";2284printwhile( <$fh> );2285}2286close$fhor die("Couldn't close filehandle for transmitfile():$!");2287}else{2288die("Couldn't execute git-cat-file");2289}2290}22912292# This method takes a file name, and returns ( $dirpart, $filepart ) which2293# refers to the directory portion and the file portion of the filename2294# respectively2295sub filenamesplit2296{2297my$filename=shift;2298my$fixforlocaldir=shift;22992300my($filepart,$dirpart) = ($filename,".");2301($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2302$dirpart.="/";23032304if($fixforlocaldir)2305{2306$dirpart=~s/^$state->{prependdir}//;2307}23082309return($filepart,$dirpart);2310}23112312sub filecleanup2313{2314my$filename=shift;23152316returnundefunless(defined($filename));2317if($filename=~/^\// )2318{2319print"E absolute filenames '$filename' not supported by server\n";2320returnundef;2321}23222323$filename=~s/^\.\///g;2324$filename=$state->{prependdir} .$filename;2325return$filename;2326}23272328sub validateGitDir2329{2330if( !defined($state->{CVSROOT}) )2331{2332print"error 1 CVSROOT not specified\n";2333 cleanupWorkTree();2334exit;2335}2336if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2337{2338print"error 1 Internally inconsistent CVSROOT\n";2339 cleanupWorkTree();2340exit;2341}2342}23432344# Setup working directory in a work tree with the requested version2345# loaded in the index.2346sub setupWorkTree2347{2348my($ver) =@_;23492350 validateGitDir();23512352if( (defined($work->{state}) &&$work->{state} !=1) ||2353defined($work->{tmpDir}) )2354{2355$log->warn("Bad work tree state management");2356print"error 1 Internal setup multiple work trees without cleanup\n";2357 cleanupWorkTree();2358exit;2359}23602361$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);23622363if( !defined($work->{index}) )2364{2365(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2366}23672368chdir$work->{workDir}or2369die"Unable to chdir to$work->{workDir}\n";23702371$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");23722373$ENV{GIT_WORK_TREE} =".";2374$ENV{GIT_INDEX_FILE} =$work->{index};2375$work->{state} =2;23762377if($ver)2378{2379system("git","read-tree",$ver);2380unless($?==0)2381{2382$log->warn("Error running git-read-tree");2383die"Error running git-read-tree$verin$work->{workDir}$!\n";2384}2385}2386# else # req_annotate reads tree for each file2387}23882389# Ensure current directory is in some kind of working directory,2390# with a recent version loaded in the index.2391sub ensureWorkTree2392{2393if(defined($work->{tmpDir}) )2394{2395$log->warn("Bad work tree state management [ensureWorkTree()]");2396print"error 1 Internal setup multiple dirs without cleanup\n";2397 cleanupWorkTree();2398exit;2399}2400if($work->{state} )2401{2402return;2403}24042405 validateGitDir();24062407if( !defined($work->{emptyDir}) )2408{2409$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2410}2411chdir$work->{emptyDir}or2412die"Unable to chdir to$work->{emptyDir}\n";24132414my$ver=`git show-ref -s refs/heads/$state->{module}`;2415chomp$ver;2416if($ver!~/^[0-9a-f]{40}$/)2417{2418$log->warn("Error from git show-ref -s refs/head$state->{module}");2419print"error 1 cannot find the current HEAD of module";2420 cleanupWorkTree();2421exit;2422}24232424if( !defined($work->{index}) )2425{2426(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2427}24282429$ENV{GIT_WORK_TREE} =".";2430$ENV{GIT_INDEX_FILE} =$work->{index};2431$work->{state} =1;24322433system("git","read-tree",$ver);2434unless($?==0)2435{2436die"Error running git-read-tree$ver$!\n";2437}2438}24392440# Cleanup working directory that is not needed any longer.2441sub cleanupWorkTree2442{2443if( !$work->{state} )2444{2445return;2446}24472448chdir"/"or die"Unable to chdir '/'\n";24492450if(defined($work->{workDir}) )2451{2452 rmtree($work->{workDir} );2453undef$work->{workDir};2454}2455undef$work->{state};2456}24572458# Setup a temporary directory (not a working tree), typically for2459# merging dirty state as in req_update.2460sub setupTmpDir2461{2462$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2463chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";24642465return$work->{tmpDir};2466}24672468# Clean up a previously setupTmpDir. Restore previous work tree if2469# appropriate.2470sub cleanupTmpDir2471{2472if( !defined($work->{tmpDir}) )2473{2474$log->warn("cleanup tmpdir that has not been setup");2475die"Cleanup tmpDir that has not been setup\n";2476}2477if(defined($work->{state}) )2478{2479if($work->{state} ==1)2480{2481chdir$work->{emptyDir}or2482die"Unable to chdir to$work->{emptyDir}\n";2483}2484elsif($work->{state} ==2)2485{2486chdir$work->{workDir}or2487die"Unable to chdir to$work->{emptyDir}\n";2488}2489else2490{2491$log->warn("Inconsistent work dir state");2492die"Inconsistent work dir state\n";2493}2494}2495else2496{2497chdir"/"or die"Unable to chdir '/'\n";2498}2499}25002501# Given a path, this function returns a string containing the kopts2502# that should go into that path's Entries line. For example, a binary2503# file should get -kb.2504sub kopts_from_path2505{2506my($path,$srcType,$name) =@_;25072508if(defined($cfg->{gitcvs}{usecrlfattr} )and2509$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2510{2511my($val) = check_attr("text",$path);2512if($valeq"unspecified")2513{2514$val= check_attr("crlf",$path);2515}2516if($valeq"unset")2517{2518return"-kb"2519}2520elsif( check_attr("eol",$path)ne"unspecified"||2521$valeq"set"||$valeq"input")2522{2523return"";2524}2525else2526{2527$log->info("Unrecognized check_attr crlf$path:$val");2528}2529}25302531if(defined($cfg->{gitcvs}{allbinary} ) )2532{2533if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2534{2535return"-kb";2536}2537elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2538{2539if( is_binary($srcType,$name) )2540{2541$log->debug("... as binary");2542return"-kb";2543}2544else2545{2546$log->debug("... as text");2547}2548}2549}2550# Return "" to give no special treatment to any path2551return"";2552}25532554sub check_attr2555{2556my($attr,$path) =@_;2557 ensureWorkTree();2558if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2559{2560my$val= <$fh>;2561close$fh;2562$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2563return$val;2564}2565else2566{2567returnundef;2568}2569}25702571# This should have the same heuristics as convert.c:is_binary() and related.2572# Note that the bare CR test is done by callers in convert.c.2573sub is_binary2574{2575my($srcType,$name) =@_;2576$log->debug("is_binary($srcType,$name)");25772578# Minimize amount of interpreted code run in the inner per-character2579# loop for large files, by totalling each character value and2580# then analyzing the totals.2581my@counts;2582my$i;2583for($i=0;$i<256;$i++)2584{2585$counts[$i]=0;2586}25872588my$fh= open_blob_or_die($srcType,$name);2589my$line;2590while(defined($line=<$fh>) )2591{2592# Any '\0' and bare CR are considered binary.2593if($line=~/\0|(\r[^\n])/)2594{2595close($fh);2596return1;2597}25982599# Count up each character in the line:2600my$len=length($line);2601for($i=0;$i<$len;$i++)2602{2603$counts[ord(substr($line,$i,1))]++;2604}2605}2606close$fh;26072608# Don't count CR and LF as either printable/nonprintable2609$counts[ord("\n")]=0;2610$counts[ord("\r")]=0;26112612# Categorize individual character count into printable and nonprintable:2613my$printable=0;2614my$nonprintable=0;2615for($i=0;$i<256;$i++)2616{2617if($i<32&&2618$i!=ord("\b") &&2619$i!=ord("\t") &&2620$i!=033&&# ESC2621$i!=014)# FF2622{2623$nonprintable+=$counts[$i];2624}2625elsif($i==127)# DEL2626{2627$nonprintable+=$counts[$i];2628}2629else2630{2631$printable+=$counts[$i];2632}2633}26342635return($printable>>7) <$nonprintable;2636}26372638# Returns open file handle. Possible invocations:2639# - open_blob_or_die("file",$filename);2640# - open_blob_or_die("sha1",$filehash);2641sub open_blob_or_die2642{2643my($srcType,$name) =@_;2644my($fh);2645if($srcTypeeq"file")2646{2647if( !open$fh,"<",$name)2648{2649$log->warn("Unable to open file$name:$!");2650die"Unable to open file$name:$!\n";2651}2652}2653elsif($srcTypeeq"sha1")2654{2655unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2656{2657$log->warn("Need filehash");2658die"Need filehash\n";2659}26602661my$type=`git cat-file -t$name`;2662 chomp$type;26632664 unless ( defined ($type) and$typeeq "blob" )2665 {2666$log->warn("Invalid type '$type' for '$name'");2667 die ( "Invalid type '$type' (expected 'blob')" )2668 }26692670 my$size= `git cat-file -s $name`;2671chomp$size;26722673$log->debug("open_blob_or_die($name) size=$size, type=$type");26742675unless(open$fh,'-|',"git","cat-file","blob",$name)2676{2677$log->warn("Unable to open sha1$name");2678die"Unable to open sha1$name\n";2679}2680}2681else2682{2683$log->warn("Unknown type of blob source:$srcType");2684die"Unknown type of blob source:$srcType\n";2685}2686return$fh;2687}26882689# Generate a CVS author name from Git author information, by taking the local2690# part of the email address and replacing characters not in the Portable2691# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2692# Login names are Unix login names, which should be restricted to this2693# character set.2694sub cvs_author2695{2696my$author_line=shift;2697(my$author) =$author_line=~/<([^@>]*)/;26982699$author=~s/[^-a-zA-Z0-9_.]/_/g;2700$author=~s/^-/_/;27012702$author;2703}270427052706sub descramble2707{2708# This table is from src/scramble.c in the CVS source2709my@SHIFTS= (27100,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,271116,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2712114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2713111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,271441,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2715125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,271636,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,271758,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2718225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2719199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2720174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2721207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2722192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2723227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2724182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2725243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522726);2727my($str) =@_;27282729# This should never happen, the same password format (A) has been2730# used by CVS since the beginning of time2731{2732my$fmt=substr($str,0,1);2733die"invalid password format `$fmt'"unless$fmteq'A';2734}27352736my@str=unpack"C*",substr($str,1);2737my$ret=join'',map{chr$SHIFTS[$_] }@str;2738return$ret;2739}274027412742package GITCVS::log;27432744####2745#### Copyright The Open University UK - 2006.2746####2747#### Authors: Martyn Smith <martyn@catalyst.net.nz>2748#### Martin Langhoff <martin@laptop.org>2749####2750####27512752use strict;2753use warnings;27542755=head1 NAME27562757GITCVS::log27582759=head1 DESCRIPTION27602761This module provides very crude logging with a similar interface to2762Log::Log4perl27632764=head1 METHODS27652766=cut27672768=head2 new27692770Creates a new log object, optionally you can specify a filename here to2771indicate the file to log to. If no log file is specified, you can specify one2772later with method setfile, or indicate you no longer want logging with method2773nofile.27742775Until one of these methods is called, all log calls will buffer messages ready2776to write out.27772778=cut2779sub new2780{2781my$class=shift;2782my$filename=shift;27832784my$self= {};27852786bless$self,$class;27872788if(defined($filename) )2789{2790open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2791}27922793return$self;2794}27952796=head2 setfile27972798This methods takes a filename, and attempts to open that file as the log file.2799If successful, all buffered data is written out to the file, and any further2800logging is written directly to the file.28012802=cut2803sub setfile2804{2805my$self=shift;2806my$filename=shift;28072808if(defined($filename) )2809{2810open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2811}28122813return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28142815while(my$line=shift@{$self->{buffer}} )2816{2817print{$self->{fh}}$line;2818}2819}28202821=head2 nofile28222823This method indicates no logging is going to be used. It flushes any entries in2824the internal buffer, and sets a flag to ensure no further data is put there.28252826=cut2827sub nofile2828{2829my$self=shift;28302831$self->{nolog} =1;28322833return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28342835$self->{buffer} = [];2836}28372838=head2 _logopen28392840Internal method. Returns true if the log file is open, false otherwise.28412842=cut2843sub _logopen2844{2845my$self=shift;28462847return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2848return0;2849}28502851=head2 debug info warn fatal28522853These four methods are wrappers to _log. They provide the actual interface for2854logging data.28552856=cut2857sub debug {my$self=shift;$self->_log("debug",@_); }2858sub info {my$self=shift;$self->_log("info",@_); }2859subwarn{my$self=shift;$self->_log("warn",@_); }2860sub fatal {my$self=shift;$self->_log("fatal",@_); }28612862=head2 _log28632864This is an internal method called by the logging functions. It generates a2865timestamp and pushes the logged line either to file, or internal buffer.28662867=cut2868sub _log2869{2870my$self=shift;2871my$level=shift;28722873return if($self->{nolog} );28742875my@time=localtime;2876my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2877$time[5] +1900,2878$time[4] +1,2879$time[3],2880$time[2],2881$time[1],2882$time[0],2883uc$level,2884);28852886if($self->_logopen)2887{2888print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2889}else{2890push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2891}2892}28932894=head2 DESTROY28952896This method simply closes the file handle if one is open28972898=cut2899sub DESTROY2900{2901my$self=shift;29022903if($self->_logopen)2904{2905close$self->{fh};2906}2907}29082909package GITCVS::updater;29102911####2912#### Copyright The Open University UK - 2006.2913####2914#### Authors: Martyn Smith <martyn@catalyst.net.nz>2915#### Martin Langhoff <martin@laptop.org>2916####2917####29182919use strict;2920use warnings;2921use DBI;29222923=head1 METHODS29242925=cut29262927=head2 new29282929=cut2930sub new2931{2932my$class=shift;2933my$config=shift;2934my$module=shift;2935my$log=shift;29362937die"Need to specify a git repository"unless(defined($config)and-d $config);2938die"Need to specify a module"unless(defined($module) );29392940$class=ref($class) ||$class;29412942my$self= {};29432944bless$self,$class;29452946$self->{valid_tables} = {'revision'=>1,2947'revision_ix1'=>1,2948'revision_ix2'=>1,2949'head'=>1,2950'head_ix1'=>1,2951'properties'=>1,2952'commitmsgs'=>1};29532954$self->{module} =$module;2955$self->{git_path} =$config."/";29562957$self->{log} =$log;29582959die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );29602961$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2962$cfg->{gitcvs}{dbdriver} ||"SQLite";2963$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2964$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2965$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2966$cfg->{gitcvs}{dbuser} ||"";2967$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2968$cfg->{gitcvs}{dbpass} ||"";2969$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||2970$cfg->{gitcvs}{dbtablenameprefix} ||"";2971my%mapping= ( m =>$module,2972 a =>$state->{method},2973 u =>getlogin||getpwuid($<) || $<,2974 G =>$self->{git_path},2975 g => mangle_dirname($self->{git_path}),2976);2977$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2978$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;2979$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;2980$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});29812982die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2983die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2984$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2985$self->{dbuser},2986$self->{dbpass});2987die"Error connecting to database\n"unlessdefined$self->{dbh};29882989$self->{tables} = {};2990foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2991{2992$self->{tables}{$table} =1;2993}29942995# Construct the revision table if required2996# The revision table stores an entry for each file, each time that file2997# changes.2998# numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )2999# This is not sufficient to support "-r {commithash}" for any3000# files except files that were modified by that commit (also,3001# some places in the code ignore/effectively strip out -r in3002# some cases, before it gets passed to getmeta()).3003# The "filehash" field typically has a git blob hash, but can also3004# be set to "dead" to indicate that the given version of the file3005# should not exist in the sandbox.3006unless($self->{tables}{$self->tablename("revision")} )3007{3008my$tablename=$self->tablename("revision");3009my$ix1name=$self->tablename("revision_ix1");3010my$ix2name=$self->tablename("revision_ix2");3011$self->{dbh}->do("3012 CREATE TABLE$tablename(3013 name TEXT NOT NULL,3014 revision INTEGER NOT NULL,3015 filehash TEXT NOT NULL,3016 commithash TEXT NOT NULL,3017 author TEXT NOT NULL,3018 modified TEXT NOT NULL,3019 mode TEXT NOT NULL3020 )3021 ");3022$self->{dbh}->do("3023 CREATE INDEX$ix1name3024 ON$tablename(name,revision)3025 ");3026$self->{dbh}->do("3027 CREATE INDEX$ix2name3028 ON$tablename(name,commithash)3029 ");3030}30313032# Construct the head table if required3033# The head table (along with the "last_commit" entry in the property3034# table) is the persisted working state of the "sub update" subroutine.3035# All of it's data is read entirely first, and completely recreated3036# last, every time "sub update" runs.3037# This is also used by "sub getmeta" when it is asked for the latest3038# version of a file (as opposed to some specific version).3039# Another way of thinking about it is as a single slice out of3040# "revisions", giving just the most recent revision information for3041# each file.3042unless($self->{tables}{$self->tablename("head")} )3043{3044my$tablename=$self->tablename("head");3045my$ix1name=$self->tablename("head_ix1");3046$self->{dbh}->do("3047 CREATE TABLE$tablename(3048 name TEXT NOT NULL,3049 revision INTEGER NOT NULL,3050 filehash TEXT NOT NULL,3051 commithash TEXT NOT NULL,3052 author TEXT NOT NULL,3053 modified TEXT NOT NULL,3054 mode TEXT NOT NULL3055 )3056 ");3057$self->{dbh}->do("3058 CREATE INDEX$ix1name3059 ON$tablename(name)3060 ");3061}30623063# Construct the properties table if required3064# - "last_commit" - Used by "sub update".3065unless($self->{tables}{$self->tablename("properties")} )3066{3067my$tablename=$self->tablename("properties");3068$self->{dbh}->do("3069 CREATE TABLE$tablename(3070 key TEXT NOT NULL PRIMARY KEY,3071 value TEXT3072 )3073 ");3074}30753076# Construct the commitmsgs table if required3077# The commitmsgs table is only used for merge commits, since3078# "sub update" will only keep one branch of parents. Shortlogs3079# for ignored commits (i.e. not on the chosen branch) will be used3080# to construct a replacement "collapsed" merge commit message,3081# which will be stored in this table. See also "sub commitmessage".3082unless($self->{tables}{$self->tablename("commitmsgs")} )3083{3084my$tablename=$self->tablename("commitmsgs");3085$self->{dbh}->do("3086 CREATE TABLE$tablename(3087 key TEXT NOT NULL PRIMARY KEY,3088 value TEXT3089 )3090 ");3091}30923093return$self;3094}30953096=head2 tablename30973098=cut3099sub tablename3100{3101my$self=shift;3102my$name=shift;31033104if(exists$self->{valid_tables}{$name}) {3105return$self->{dbtablenameprefix} .$name;3106}else{3107returnundef;3108}3109}31103111=head2 update31123113Bring the database up to date with the latest changes from3114the git repository.31153116Internal working state is read out of the "head" table and the3117"last_commit" property, then it updates "revisions" based on that, and3118finally it writes the new internal state back to the "head" table3119so it can be used as a starting point the next time update is called.31203121=cut3122sub update3123{3124my$self=shift;31253126# first lets get the commit list3127$ENV{GIT_DIR} =$self->{git_path};31283129my$commitsha1=`git rev-parse$self->{module}`;3130chomp$commitsha1;31313132my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3133unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3134{3135die("Invalid module '$self->{module}'");3136}313731383139my$git_log;3140my$lastcommit=$self->_get_prop("last_commit");31413142if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3143return1;3144}31453146# Start exclusive lock here...3147$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";31483149# TODO: log processing is memory bound3150# if we can parse into a 2nd file that is in reverse order3151# we can probably do something really efficient3152my@git_log_params= ('--pretty','--parents','--topo-order');31533154if(defined$lastcommit) {3155push@git_log_params,"$lastcommit..$self->{module}";3156}else{3157push@git_log_params,$self->{module};3158}3159# git-rev-list is the backend / plumbing version of git-log3160open(GITLOG,'-|','git','rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";31613162my@commits;31633164my%commit= ();31653166while( <GITLOG> )3167{3168chomp;3169if(m/^commit\s+(.*)$/) {3170# on ^commit lines put the just seen commit in the stack3171# and prime things for the next one3172if(keys%commit) {3173my%copy=%commit;3174unshift@commits, \%copy;3175%commit= ();3176}3177my@parents=split(m/\s+/,$1);3178$commit{hash} =shift@parents;3179$commit{parents} = \@parents;3180}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3181# on rfc822-like lines seen before we see any message,3182# lowercase the entry and put it in the hash as key-value3183$commit{lc($1)} =$2;3184}else{3185# message lines - skip initial empty line3186# and trim whitespace3187if(!exists($commit{message}) &&m/^\s*$/) {3188# define it to mark the end of headers3189$commit{message} ='';3190next;3191}3192s/^\s+//;s/\s+$//;# trim ws3193$commit{message} .=$_."\n";3194}3195}3196close GITLOG;31973198unshift@commits, \%commitif(keys%commit);31993200# Now all the commits are in the @commits bucket3201# ordered by time DESC. for each commit that needs processing,3202# determine whether it's following the last head we've seen or if3203# it's on its own branch, grab a file list, and add whatever's changed3204# NOTE: $lastcommit refers to the last commit from previous run3205# $lastpicked is the last commit we picked in this run3206my$lastpicked;3207my$head= {};3208if(defined$lastcommit) {3209$lastpicked=$lastcommit;3210}32113212my$committotal=scalar(@commits);3213my$commitcount=0;32143215# Load the head table into $head (for cached lookups during the update process)3216foreachmy$file( @{$self->gethead(1)} )3217{3218$head->{$file->{name}} =$file;3219}32203221foreachmy$commit(@commits)3222{3223$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3224if(defined$lastpicked)3225{3226if(!in_array($lastpicked, @{$commit->{parents}}))3227{3228# skip, we'll see this delta3229# as part of a merge later3230# warn "skipping off-track $commit->{hash}\n";3231next;3232}elsif(@{$commit->{parents}} >1) {3233# it is a merge commit, for each parent that is3234# not $lastpicked (not given a CVS revision number),3235# see if we can get a log3236# from the merge-base to that parent to put it3237# in the message as a merge summary.3238my@parents= @{$commit->{parents}};3239foreachmy$parent(@parents) {3240if($parenteq$lastpicked) {3241next;3242}3243# git-merge-base can potentially (but rarely) throw3244# several candidate merge bases. let's assume3245# that the first one is the best one.3246my$base=eval{3247 safe_pipe_capture('git','merge-base',3248$lastpicked,$parent);3249};3250# The two branches may not be related at all,3251# in which case merge base simply fails to find3252# any, but that's Ok.3253next if($@);32543255chomp$base;3256if($base) {3257my@merged;3258# print "want to log between $base $parent \n";3259open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3260or die"Cannot call git-log:$!";3261my$mergedhash;3262while(<GITLOG>) {3263chomp;3264if(!defined$mergedhash) {3265if(m/^commit\s+(.+)$/) {3266$mergedhash=$1;3267}else{3268next;3269}3270}else{3271# grab the first line that looks non-rfc8223272# aka has content after leading space3273if(m/^\s+(\S.*)$/) {3274my$title=$1;3275$title=substr($title,0,100);# truncate3276unshift@merged,"$mergedhash$title";3277undef$mergedhash;3278}3279}3280}3281close GITLOG;3282if(@merged) {3283$commit->{mergemsg} =$commit->{message};3284$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3285foreachmy$summary(@merged) {3286$commit->{mergemsg} .="\t$summary\n";3287}3288$commit->{mergemsg} .="\n\n";3289# print "Message for $commit->{hash} \n$commit->{mergemsg}";3290}3291}3292}3293}3294}32953296# convert the date to CVS-happy format3297$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);32983299if(defined($lastpicked) )3300{3301my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3302local($/) ="\0";3303while( <FILELIST> )3304{3305chomp;3306unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)3307{3308die("Couldn't process git-diff-tree line :$_");3309}3310my($mode,$hash,$change) = ($1,$2,$3);3311my$name= <FILELIST>;3312chomp($name);33133314# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");33153316my$git_perms="";3317$git_perms.="r"if($mode&4);3318$git_perms.="w"if($mode&2);3319$git_perms.="x"if($mode&1);3320$git_perms="rw"if($git_permseq"");33213322if($changeeq"D")3323{3324#$log->debug("DELETE $name");3325$head->{$name} = {3326 name =>$name,3327 revision =>$head->{$name}{revision} +1,3328 filehash =>"deleted",3329 commithash =>$commit->{hash},3330 modified =>$commit->{date},3331 author =>$commit->{author},3332 mode =>$git_perms,3333};3334$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3335}3336elsif($changeeq"M"||$changeeq"T")3337{3338#$log->debug("MODIFIED $name");3339$head->{$name} = {3340 name =>$name,3341 revision =>$head->{$name}{revision} +1,3342 filehash =>$hash,3343 commithash =>$commit->{hash},3344 modified =>$commit->{date},3345 author =>$commit->{author},3346 mode =>$git_perms,3347};3348$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3349}3350elsif($changeeq"A")3351{3352#$log->debug("ADDED $name");3353$head->{$name} = {3354 name =>$name,3355 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3356 filehash =>$hash,3357 commithash =>$commit->{hash},3358 modified =>$commit->{date},3359 author =>$commit->{author},3360 mode =>$git_perms,3361};3362$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3363}3364else3365{3366$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3367die;3368}3369}3370close FILELIST;3371}else{3372# this is used to detect files removed from the repo3373my$seen_files= {};33743375my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3376local$/="\0";3377while( <FILELIST> )3378{3379chomp;3380unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3381{3382die("Couldn't process git-ls-tree line :$_");3383}33843385my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);33863387$seen_files->{$git_filename} =1;33883389my($oldhash,$oldrevision,$oldmode) = (3390$head->{$git_filename}{filehash},3391$head->{$git_filename}{revision},3392$head->{$git_filename}{mode}3393);33943395if($git_perms=~/^\d\d\d(\d)\d\d/o)3396{3397$git_perms="";3398$git_perms.="r"if($1&4);3399$git_perms.="w"if($1&2);3400$git_perms.="x"if($1&1);3401}else{3402$git_perms="rw";3403}34043405# unless the file exists with the same hash, we need to update it ...3406unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)3407{3408my$newrevision= ($oldrevisionor0) +1;34093410$head->{$git_filename} = {3411 name =>$git_filename,3412 revision =>$newrevision,3413 filehash =>$git_hash,3414 commithash =>$commit->{hash},3415 modified =>$commit->{date},3416 author =>$commit->{author},3417 mode =>$git_perms,3418};341934203421$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3422}3423}3424close FILELIST;34253426# Detect deleted files3427foreachmy$file(keys%$head)3428{3429unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3430{3431$head->{$file}{revision}++;3432$head->{$file}{filehash} ="deleted";3433$head->{$file}{commithash} =$commit->{hash};3434$head->{$file}{modified} =$commit->{date};3435$head->{$file}{author} =$commit->{author};34363437$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});3438}3439}3440# END : "Detect deleted files"3441}344234433444if(exists$commit->{mergemsg})3445{3446$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3447}34483449$lastpicked=$commit->{hash};34503451$self->_set_prop("last_commit",$commit->{hash});3452}34533454$self->delete_head();3455foreachmy$file(keys%$head)3456{3457$self->insert_head(3458$file,3459$head->{$file}{revision},3460$head->{$file}{filehash},3461$head->{$file}{commithash},3462$head->{$file}{modified},3463$head->{$file}{author},3464$head->{$file}{mode},3465);3466}3467# invalidate the gethead cache3468$self->{gethead_cache} =undef;346934703471# Ending exclusive lock here3472$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3473}34743475sub insert_rev3476{3477my$self=shift;3478my$name=shift;3479my$revision=shift;3480my$filehash=shift;3481my$commithash=shift;3482my$modified=shift;3483my$author=shift;3484my$mode=shift;3485my$tablename=$self->tablename("revision");34863487my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3488$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3489}34903491sub insert_mergelog3492{3493my$self=shift;3494my$key=shift;3495my$value=shift;3496my$tablename=$self->tablename("commitmsgs");34973498my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3499$insert_mergelog->execute($key,$value);3500}35013502sub delete_head3503{3504my$self=shift;3505my$tablename=$self->tablename("head");35063507my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3508$delete_head->execute();3509}35103511sub insert_head3512{3513my$self=shift;3514my$name=shift;3515my$revision=shift;3516my$filehash=shift;3517my$commithash=shift;3518my$modified=shift;3519my$author=shift;3520my$mode=shift;3521my$tablename=$self->tablename("head");35223523my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3524$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3525}35263527sub _get_prop3528{3529my$self=shift;3530my$key=shift;3531my$tablename=$self->tablename("properties");35323533my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3534$db_query->execute($key);3535my($value) =$db_query->fetchrow_array;35363537return$value;3538}35393540sub _set_prop3541{3542my$self=shift;3543my$key=shift;3544my$value=shift;3545my$tablename=$self->tablename("properties");35463547my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3548$db_query->execute($value,$key);35493550unless($db_query->rows)3551{3552$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3553$db_query->execute($key,$value);3554}35553556return$value;3557}35583559=head2 gethead35603561=cut35623563sub gethead3564{3565my$self=shift;3566my$intRev=shift;3567my$tablename=$self->tablename("head");35683569return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );35703571my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3572$db_query->execute();35733574my$tree= [];3575while(my$file=$db_query->fetchrow_hashref)3576{3577if(!$intRev)3578{3579$file->{revision} ="1.$file->{revision}"3580}3581push@$tree,$file;3582}35833584$self->{gethead_cache} =$tree;35853586return$tree;3587}35883589=head2 getlog35903591See also gethistorydense().35923593=cut35943595sub getlog3596{3597my$self=shift;3598my$filename=shift;3599my$revFilter=shift;36003601my$tablename=$self->tablename("revision");36023603# Filters:3604# TODO: date, state, or by specific logins filters?3605# TODO: Handle comma-separated list of revFilter items, each item3606# can be a range [only case currently handled] or individual3607# rev or branch or "branch.".3608# TODO: Adjust $db_query WHERE clause based on revFilter, instead of3609# manually filtering the results of the query?3610my($minrev,$maxrev);3611if(defined($revFilter)and3612$state->{opt}{r} =~/^(1.(\d+))?(::?)(1.(\d.+))?$/)3613{3614my$control=$3;3615$minrev=$2;3616$maxrev=$5;3617$minrev++if(defined($minrev)and$controleq"::");3618}36193620my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3621$db_query->execute($filename);36223623my$totalRevs=0;3624my$tree= [];3625while(my$file=$db_query->fetchrow_hashref)3626{3627$totalRevs++;3628if(defined($minrev)and$file->{revision} <$minrev)3629{3630next;3631}3632if(defined($maxrev)and$file->{revision} >$maxrev)3633{3634next;3635}36363637$file->{revision} ="1.".$file->{revision};3638push@$tree,$file;3639}36403641return($tree,$totalRevs);3642}36433644=head2 getmeta36453646This function takes a filename (with path) argument and returns a hashref of3647metadata for that file.36483649=cut36503651sub getmeta3652{3653my$self=shift;3654my$filename=shift;3655my$revision=shift;3656my$tablename_rev=$self->tablename("revision");3657my$tablename_head=$self->tablename("head");36583659my$db_query;3660if(defined($revision)and$revision=~/^1\.(\d+)$/)3661{3662my($intRev) =$1;3663$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND revision=?",{},1);3664$db_query->execute($filename,$intRev);3665}3666elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)3667{3668$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND commithash=?",{},1);3669$db_query->execute($filename,$revision);3670}else{3671$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_headWHERE name=?",{},1);3672$db_query->execute($filename);3673}36743675my$meta=$db_query->fetchrow_hashref;3676if($meta)3677{3678$meta->{revision} ="1.$meta->{revision}";3679}3680return$meta;3681}36823683=head2 commitmessage36843685this function takes a commithash and returns the commit message for that commit36863687=cut3688sub commitmessage3689{3690my$self=shift;3691my$commithash=shift;3692my$tablename=$self->tablename("commitmsgs");36933694die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);36953696my$db_query;3697$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3698$db_query->execute($commithash);36993700my($message) =$db_query->fetchrow_array;37013702if(defined($message) )3703{3704$message.=" "if($message=~/\n$/);3705return$message;3706}37073708my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);3709shift@lineswhile($lines[0] =~/\S/);3710$message=join("",@lines);3711$message.=" "if($message=~/\n$/);3712return$message;3713}37143715=head2 gethistorydense37163717This function takes a filename (with path) argument and returns an arrayofarrays3718containing revision,filehash,commithash ordered by revision descending.37193720This version of gethistory skips deleted entries -- so it is useful for annotate.3721The 'dense' part is a reference to a '--dense' option available for git-rev-list3722and other git tools that depend on it.37233724See also getlog().37253726=cut3727sub gethistorydense3728{3729my$self=shift;3730my$filename=shift;3731my$tablename=$self->tablename("revision");37323733my$db_query;3734$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);3735$db_query->execute($filename);37363737my$result=$db_query->fetchall_arrayref;37383739my$i;3740for($i=0;$i<scalar(@$result) ;$i++)3741{3742$result->[$i][0]="1.".$result->[$i][0];3743}37443745return$result;3746}37473748=head2 in_array()37493750from Array::PAT - mimics the in_array() function3751found in PHP. Yuck but works for small arrays.37523753=cut3754sub in_array3755{3756my($check,@array) =@_;3757my$retval=0;3758foreachmy$test(@array){3759if($checkeq$test){3760$retval=1;3761}3762}3763return$retval;3764}37653766=head2 safe_pipe_capture37673768an alternative to `command` that allows input to be passed as an array3769to work around shell problems with weird characters in arguments37703771=cut3772sub safe_pipe_capture {37733774my@output;37753776if(my$pid=open my$child,'-|') {3777@output= (<$child>);3778close$childor die join(' ',@_).":$!$?";3779}else{3780exec(@_)or die"$!$?";# exec() can fail the executable can't be found3781}3782returnwantarray?@output:join('',@output);3783}37843785=head2 mangle_dirname37863787create a string from a directory name that is suitable to use as3788part of a filename, mainly by converting all chars except \w.- to _37893790=cut3791sub mangle_dirname {3792my$dirname=shift;3793return unlessdefined$dirname;37943795$dirname=~s/[^\w.-]/_/g;37963797return$dirname;3798}37993800=head2 mangle_tablename38013802create a string from a that is suitable to use as part of an SQL table3803name, mainly by converting all chars except \w to _38043805=cut3806sub mangle_tablename {3807my$tablename=shift;3808return unlessdefined$tablename;38093810$tablename=~s/[^\w_]/_/g;38113812return$tablename;3813}381438151;