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}23112312# Cleanup various junk in filename (try to canonicalize it), and2313# add prependdir to accomodate running CVS client from a2314# subdirectory (so the output is relative to top directory of the project).2315sub filecleanup2316{2317my$filename=shift;23182319returnundefunless(defined($filename));2320if($filename=~/^\// )2321{2322print"E absolute filenames '$filename' not supported by server\n";2323returnundef;2324}23252326if($filenameeq".")2327{2328$filename="";2329}2330$filename=~s/^\.\///g;2331$filename=~ s%/+%/%g;2332$filename=$state->{prependdir} .$filename;2333$filename=~ s%/$%%;2334return$filename;2335}23362337# Remove prependdir from the path, so that is is relative to the directory2338# the CVS client was started from, rather than the top of the project.2339# Essentially the inverse of filecleanup().2340sub remove_prependdir2341{2342my($path) =@_;2343if(defined($state->{prependdir}) &&$state->{prependdir}ne"")2344{2345my($pre)=$state->{prependdir};2346$pre=~s%/$%%;2347if(!($path=~s%^\Q$pre\E/?%%))2348{2349$log->fatal("internal error missing prependdir");2350die("internal error missing prependdir");2351}2352}2353return$path;2354}23552356sub validateGitDir2357{2358if( !defined($state->{CVSROOT}) )2359{2360print"error 1 CVSROOT not specified\n";2361 cleanupWorkTree();2362exit;2363}2364if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2365{2366print"error 1 Internally inconsistent CVSROOT\n";2367 cleanupWorkTree();2368exit;2369}2370}23712372# Setup working directory in a work tree with the requested version2373# loaded in the index.2374sub setupWorkTree2375{2376my($ver) =@_;23772378 validateGitDir();23792380if( (defined($work->{state}) &&$work->{state} !=1) ||2381defined($work->{tmpDir}) )2382{2383$log->warn("Bad work tree state management");2384print"error 1 Internal setup multiple work trees without cleanup\n";2385 cleanupWorkTree();2386exit;2387}23882389$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);23902391if( !defined($work->{index}) )2392{2393(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2394}23952396chdir$work->{workDir}or2397die"Unable to chdir to$work->{workDir}\n";23982399$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");24002401$ENV{GIT_WORK_TREE} =".";2402$ENV{GIT_INDEX_FILE} =$work->{index};2403$work->{state} =2;24042405if($ver)2406{2407system("git","read-tree",$ver);2408unless($?==0)2409{2410$log->warn("Error running git-read-tree");2411die"Error running git-read-tree$verin$work->{workDir}$!\n";2412}2413}2414# else # req_annotate reads tree for each file2415}24162417# Ensure current directory is in some kind of working directory,2418# with a recent version loaded in the index.2419sub ensureWorkTree2420{2421if(defined($work->{tmpDir}) )2422{2423$log->warn("Bad work tree state management [ensureWorkTree()]");2424print"error 1 Internal setup multiple dirs without cleanup\n";2425 cleanupWorkTree();2426exit;2427}2428if($work->{state} )2429{2430return;2431}24322433 validateGitDir();24342435if( !defined($work->{emptyDir}) )2436{2437$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2438}2439chdir$work->{emptyDir}or2440die"Unable to chdir to$work->{emptyDir}\n";24412442my$ver=`git show-ref -s refs/heads/$state->{module}`;2443chomp$ver;2444if($ver!~/^[0-9a-f]{40}$/)2445{2446$log->warn("Error from git show-ref -s refs/head$state->{module}");2447print"error 1 cannot find the current HEAD of module";2448 cleanupWorkTree();2449exit;2450}24512452if( !defined($work->{index}) )2453{2454(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2455}24562457$ENV{GIT_WORK_TREE} =".";2458$ENV{GIT_INDEX_FILE} =$work->{index};2459$work->{state} =1;24602461system("git","read-tree",$ver);2462unless($?==0)2463{2464die"Error running git-read-tree$ver$!\n";2465}2466}24672468# Cleanup working directory that is not needed any longer.2469sub cleanupWorkTree2470{2471if( !$work->{state} )2472{2473return;2474}24752476chdir"/"or die"Unable to chdir '/'\n";24772478if(defined($work->{workDir}) )2479{2480 rmtree($work->{workDir} );2481undef$work->{workDir};2482}2483undef$work->{state};2484}24852486# Setup a temporary directory (not a working tree), typically for2487# merging dirty state as in req_update.2488sub setupTmpDir2489{2490$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2491chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";24922493return$work->{tmpDir};2494}24952496# Clean up a previously setupTmpDir. Restore previous work tree if2497# appropriate.2498sub cleanupTmpDir2499{2500if( !defined($work->{tmpDir}) )2501{2502$log->warn("cleanup tmpdir that has not been setup");2503die"Cleanup tmpDir that has not been setup\n";2504}2505if(defined($work->{state}) )2506{2507if($work->{state} ==1)2508{2509chdir$work->{emptyDir}or2510die"Unable to chdir to$work->{emptyDir}\n";2511}2512elsif($work->{state} ==2)2513{2514chdir$work->{workDir}or2515die"Unable to chdir to$work->{emptyDir}\n";2516}2517else2518{2519$log->warn("Inconsistent work dir state");2520die"Inconsistent work dir state\n";2521}2522}2523else2524{2525chdir"/"or die"Unable to chdir '/'\n";2526}2527}25282529# Given a path, this function returns a string containing the kopts2530# that should go into that path's Entries line. For example, a binary2531# file should get -kb.2532sub kopts_from_path2533{2534my($path,$srcType,$name) =@_;25352536if(defined($cfg->{gitcvs}{usecrlfattr} )and2537$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2538{2539my($val) = check_attr("text",$path);2540if($valeq"unspecified")2541{2542$val= check_attr("crlf",$path);2543}2544if($valeq"unset")2545{2546return"-kb"2547}2548elsif( check_attr("eol",$path)ne"unspecified"||2549$valeq"set"||$valeq"input")2550{2551return"";2552}2553else2554{2555$log->info("Unrecognized check_attr crlf$path:$val");2556}2557}25582559if(defined($cfg->{gitcvs}{allbinary} ) )2560{2561if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2562{2563return"-kb";2564}2565elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2566{2567if( is_binary($srcType,$name) )2568{2569$log->debug("... as binary");2570return"-kb";2571}2572else2573{2574$log->debug("... as text");2575}2576}2577}2578# Return "" to give no special treatment to any path2579return"";2580}25812582sub check_attr2583{2584my($attr,$path) =@_;2585 ensureWorkTree();2586if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2587{2588my$val= <$fh>;2589close$fh;2590$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2591return$val;2592}2593else2594{2595returnundef;2596}2597}25982599# This should have the same heuristics as convert.c:is_binary() and related.2600# Note that the bare CR test is done by callers in convert.c.2601sub is_binary2602{2603my($srcType,$name) =@_;2604$log->debug("is_binary($srcType,$name)");26052606# Minimize amount of interpreted code run in the inner per-character2607# loop for large files, by totalling each character value and2608# then analyzing the totals.2609my@counts;2610my$i;2611for($i=0;$i<256;$i++)2612{2613$counts[$i]=0;2614}26152616my$fh= open_blob_or_die($srcType,$name);2617my$line;2618while(defined($line=<$fh>) )2619{2620# Any '\0' and bare CR are considered binary.2621if($line=~/\0|(\r[^\n])/)2622{2623close($fh);2624return1;2625}26262627# Count up each character in the line:2628my$len=length($line);2629for($i=0;$i<$len;$i++)2630{2631$counts[ord(substr($line,$i,1))]++;2632}2633}2634close$fh;26352636# Don't count CR and LF as either printable/nonprintable2637$counts[ord("\n")]=0;2638$counts[ord("\r")]=0;26392640# Categorize individual character count into printable and nonprintable:2641my$printable=0;2642my$nonprintable=0;2643for($i=0;$i<256;$i++)2644{2645if($i<32&&2646$i!=ord("\b") &&2647$i!=ord("\t") &&2648$i!=033&&# ESC2649$i!=014)# FF2650{2651$nonprintable+=$counts[$i];2652}2653elsif($i==127)# DEL2654{2655$nonprintable+=$counts[$i];2656}2657else2658{2659$printable+=$counts[$i];2660}2661}26622663return($printable>>7) <$nonprintable;2664}26652666# Returns open file handle. Possible invocations:2667# - open_blob_or_die("file",$filename);2668# - open_blob_or_die("sha1",$filehash);2669sub open_blob_or_die2670{2671my($srcType,$name) =@_;2672my($fh);2673if($srcTypeeq"file")2674{2675if( !open$fh,"<",$name)2676{2677$log->warn("Unable to open file$name:$!");2678die"Unable to open file$name:$!\n";2679}2680}2681elsif($srcTypeeq"sha1")2682{2683unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2684{2685$log->warn("Need filehash");2686die"Need filehash\n";2687}26882689my$type=`git cat-file -t$name`;2690 chomp$type;26912692 unless ( defined ($type) and$typeeq "blob" )2693 {2694$log->warn("Invalid type '$type' for '$name'");2695 die ( "Invalid type '$type' (expected 'blob')" )2696 }26972698 my$size= `git cat-file -s $name`;2699chomp$size;27002701$log->debug("open_blob_or_die($name) size=$size, type=$type");27022703unless(open$fh,'-|',"git","cat-file","blob",$name)2704{2705$log->warn("Unable to open sha1$name");2706die"Unable to open sha1$name\n";2707}2708}2709else2710{2711$log->warn("Unknown type of blob source:$srcType");2712die"Unknown type of blob source:$srcType\n";2713}2714return$fh;2715}27162717# Generate a CVS author name from Git author information, by taking the local2718# part of the email address and replacing characters not in the Portable2719# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2720# Login names are Unix login names, which should be restricted to this2721# character set.2722sub cvs_author2723{2724my$author_line=shift;2725(my$author) =$author_line=~/<([^@>]*)/;27262727$author=~s/[^-a-zA-Z0-9_.]/_/g;2728$author=~s/^-/_/;27292730$author;2731}273227332734sub descramble2735{2736# This table is from src/scramble.c in the CVS source2737my@SHIFTS= (27380,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,273916,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2740114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2741111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,274241,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2743125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,274436,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,274558,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2746225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2747199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2748174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2749207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2750192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2751227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2752182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2753243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522754);2755my($str) =@_;27562757# This should never happen, the same password format (A) has been2758# used by CVS since the beginning of time2759{2760my$fmt=substr($str,0,1);2761die"invalid password format `$fmt'"unless$fmteq'A';2762}27632764my@str=unpack"C*",substr($str,1);2765my$ret=join'',map{chr$SHIFTS[$_] }@str;2766return$ret;2767}276827692770package GITCVS::log;27712772####2773#### Copyright The Open University UK - 2006.2774####2775#### Authors: Martyn Smith <martyn@catalyst.net.nz>2776#### Martin Langhoff <martin@laptop.org>2777####2778####27792780use strict;2781use warnings;27822783=head1 NAME27842785GITCVS::log27862787=head1 DESCRIPTION27882789This module provides very crude logging with a similar interface to2790Log::Log4perl27912792=head1 METHODS27932794=cut27952796=head2 new27972798Creates a new log object, optionally you can specify a filename here to2799indicate the file to log to. If no log file is specified, you can specify one2800later with method setfile, or indicate you no longer want logging with method2801nofile.28022803Until one of these methods is called, all log calls will buffer messages ready2804to write out.28052806=cut2807sub new2808{2809my$class=shift;2810my$filename=shift;28112812my$self= {};28132814bless$self,$class;28152816if(defined($filename) )2817{2818open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2819}28202821return$self;2822}28232824=head2 setfile28252826This methods takes a filename, and attempts to open that file as the log file.2827If successful, all buffered data is written out to the file, and any further2828logging is written directly to the file.28292830=cut2831sub setfile2832{2833my$self=shift;2834my$filename=shift;28352836if(defined($filename) )2837{2838open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2839}28402841return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28422843while(my$line=shift@{$self->{buffer}} )2844{2845print{$self->{fh}}$line;2846}2847}28482849=head2 nofile28502851This method indicates no logging is going to be used. It flushes any entries in2852the internal buffer, and sets a flag to ensure no further data is put there.28532854=cut2855sub nofile2856{2857my$self=shift;28582859$self->{nolog} =1;28602861return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28622863$self->{buffer} = [];2864}28652866=head2 _logopen28672868Internal method. Returns true if the log file is open, false otherwise.28692870=cut2871sub _logopen2872{2873my$self=shift;28742875return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2876return0;2877}28782879=head2 debug info warn fatal28802881These four methods are wrappers to _log. They provide the actual interface for2882logging data.28832884=cut2885sub debug {my$self=shift;$self->_log("debug",@_); }2886sub info {my$self=shift;$self->_log("info",@_); }2887subwarn{my$self=shift;$self->_log("warn",@_); }2888sub fatal {my$self=shift;$self->_log("fatal",@_); }28892890=head2 _log28912892This is an internal method called by the logging functions. It generates a2893timestamp and pushes the logged line either to file, or internal buffer.28942895=cut2896sub _log2897{2898my$self=shift;2899my$level=shift;29002901return if($self->{nolog} );29022903my@time=localtime;2904my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2905$time[5] +1900,2906$time[4] +1,2907$time[3],2908$time[2],2909$time[1],2910$time[0],2911uc$level,2912);29132914if($self->_logopen)2915{2916print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2917}else{2918push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2919}2920}29212922=head2 DESTROY29232924This method simply closes the file handle if one is open29252926=cut2927sub DESTROY2928{2929my$self=shift;29302931if($self->_logopen)2932{2933close$self->{fh};2934}2935}29362937package GITCVS::updater;29382939####2940#### Copyright The Open University UK - 2006.2941####2942#### Authors: Martyn Smith <martyn@catalyst.net.nz>2943#### Martin Langhoff <martin@laptop.org>2944####2945####29462947use strict;2948use warnings;2949use DBI;29502951=head1 METHODS29522953=cut29542955=head2 new29562957=cut2958sub new2959{2960my$class=shift;2961my$config=shift;2962my$module=shift;2963my$log=shift;29642965die"Need to specify a git repository"unless(defined($config)and-d $config);2966die"Need to specify a module"unless(defined($module) );29672968$class=ref($class) ||$class;29692970my$self= {};29712972bless$self,$class;29732974$self->{valid_tables} = {'revision'=>1,2975'revision_ix1'=>1,2976'revision_ix2'=>1,2977'head'=>1,2978'head_ix1'=>1,2979'properties'=>1,2980'commitmsgs'=>1};29812982$self->{module} =$module;2983$self->{git_path} =$config."/";29842985$self->{log} =$log;29862987die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );29882989# Stores full sha1's for various branch/tag names, abbreviations, etc:2990$self->{commitRefCache} = {};29912992$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2993$cfg->{gitcvs}{dbdriver} ||"SQLite";2994$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2995$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2996$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2997$cfg->{gitcvs}{dbuser} ||"";2998$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2999$cfg->{gitcvs}{dbpass} ||"";3000$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||3001$cfg->{gitcvs}{dbtablenameprefix} ||"";3002my%mapping= ( m =>$module,3003 a =>$state->{method},3004 u =>getlogin||getpwuid($<) || $<,3005 G =>$self->{git_path},3006 g => mangle_dirname($self->{git_path}),3007);3008$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;3009$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;3010$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;3011$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});30123013die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;3014die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;3015$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",3016$self->{dbuser},3017$self->{dbpass});3018die"Error connecting to database\n"unlessdefined$self->{dbh};30193020$self->{tables} = {};3021foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )3022{3023$self->{tables}{$table} =1;3024}30253026# Construct the revision table if required3027# The revision table stores an entry for each file, each time that file3028# changes.3029# numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )3030# This is not sufficient to support "-r {commithash}" for any3031# files except files that were modified by that commit (also,3032# some places in the code ignore/effectively strip out -r in3033# some cases, before it gets passed to getmeta()).3034# The "filehash" field typically has a git blob hash, but can also3035# be set to "dead" to indicate that the given version of the file3036# should not exist in the sandbox.3037unless($self->{tables}{$self->tablename("revision")} )3038{3039my$tablename=$self->tablename("revision");3040my$ix1name=$self->tablename("revision_ix1");3041my$ix2name=$self->tablename("revision_ix2");3042$self->{dbh}->do("3043 CREATE TABLE$tablename(3044 name TEXT NOT NULL,3045 revision INTEGER NOT NULL,3046 filehash TEXT NOT NULL,3047 commithash TEXT NOT NULL,3048 author TEXT NOT NULL,3049 modified TEXT NOT NULL,3050 mode TEXT NOT NULL3051 )3052 ");3053$self->{dbh}->do("3054 CREATE INDEX$ix1name3055 ON$tablename(name,revision)3056 ");3057$self->{dbh}->do("3058 CREATE INDEX$ix2name3059 ON$tablename(name,commithash)3060 ");3061}30623063# Construct the head table if required3064# The head table (along with the "last_commit" entry in the property3065# table) is the persisted working state of the "sub update" subroutine.3066# All of it's data is read entirely first, and completely recreated3067# last, every time "sub update" runs.3068# This is also used by "sub getmeta" when it is asked for the latest3069# version of a file (as opposed to some specific version).3070# Another way of thinking about it is as a single slice out of3071# "revisions", giving just the most recent revision information for3072# each file.3073unless($self->{tables}{$self->tablename("head")} )3074{3075my$tablename=$self->tablename("head");3076my$ix1name=$self->tablename("head_ix1");3077$self->{dbh}->do("3078 CREATE TABLE$tablename(3079 name TEXT NOT NULL,3080 revision INTEGER NOT NULL,3081 filehash TEXT NOT NULL,3082 commithash TEXT NOT NULL,3083 author TEXT NOT NULL,3084 modified TEXT NOT NULL,3085 mode TEXT NOT NULL3086 )3087 ");3088$self->{dbh}->do("3089 CREATE INDEX$ix1name3090 ON$tablename(name)3091 ");3092}30933094# Construct the properties table if required3095# - "last_commit" - Used by "sub update".3096unless($self->{tables}{$self->tablename("properties")} )3097{3098my$tablename=$self->tablename("properties");3099$self->{dbh}->do("3100 CREATE TABLE$tablename(3101 key TEXT NOT NULL PRIMARY KEY,3102 value TEXT3103 )3104 ");3105}31063107# Construct the commitmsgs table if required3108# The commitmsgs table is only used for merge commits, since3109# "sub update" will only keep one branch of parents. Shortlogs3110# for ignored commits (i.e. not on the chosen branch) will be used3111# to construct a replacement "collapsed" merge commit message,3112# which will be stored in this table. See also "sub commitmessage".3113unless($self->{tables}{$self->tablename("commitmsgs")} )3114{3115my$tablename=$self->tablename("commitmsgs");3116$self->{dbh}->do("3117 CREATE TABLE$tablename(3118 key TEXT NOT NULL PRIMARY KEY,3119 value TEXT3120 )3121 ");3122}31233124return$self;3125}31263127=head2 tablename31283129=cut3130sub tablename3131{3132my$self=shift;3133my$name=shift;31343135if(exists$self->{valid_tables}{$name}) {3136return$self->{dbtablenameprefix} .$name;3137}else{3138returnundef;3139}3140}31413142=head2 update31433144Bring the database up to date with the latest changes from3145the git repository.31463147Internal working state is read out of the "head" table and the3148"last_commit" property, then it updates "revisions" based on that, and3149finally it writes the new internal state back to the "head" table3150so it can be used as a starting point the next time update is called.31513152=cut3153sub update3154{3155my$self=shift;31563157# first lets get the commit list3158$ENV{GIT_DIR} =$self->{git_path};31593160my$commitsha1=`git rev-parse$self->{module}`;3161chomp$commitsha1;31623163my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3164unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3165{3166die("Invalid module '$self->{module}'");3167}316831693170my$git_log;3171my$lastcommit=$self->_get_prop("last_commit");31723173if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3174return1;3175}31763177# Start exclusive lock here...3178$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";31793180# TODO: log processing is memory bound3181# if we can parse into a 2nd file that is in reverse order3182# we can probably do something really efficient3183my@git_log_params= ('--pretty','--parents','--topo-order');31843185if(defined$lastcommit) {3186push@git_log_params,"$lastcommit..$self->{module}";3187}else{3188push@git_log_params,$self->{module};3189}3190# git-rev-list is the backend / plumbing version of git-log3191open(my$gitLogPipe,'-|','git','rev-list',@git_log_params)3192or die"Cannot call git-rev-list:$!";3193my@commits=readCommits($gitLogPipe);3194close$gitLogPipe;31953196# Now all the commits are in the @commits bucket3197# ordered by time DESC. for each commit that needs processing,3198# determine whether it's following the last head we've seen or if3199# it's on its own branch, grab a file list, and add whatever's changed3200# NOTE: $lastcommit refers to the last commit from previous run3201# $lastpicked is the last commit we picked in this run3202my$lastpicked;3203my$head= {};3204if(defined$lastcommit) {3205$lastpicked=$lastcommit;3206}32073208my$committotal=scalar(@commits);3209my$commitcount=0;32103211# Load the head table into $head (for cached lookups during the update process)3212foreachmy$file( @{$self->gethead(1)} )3213{3214$head->{$file->{name}} =$file;3215}32163217foreachmy$commit(@commits)3218{3219$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3220if(defined$lastpicked)3221{3222if(!in_array($lastpicked, @{$commit->{parents}}))3223{3224# skip, we'll see this delta3225# as part of a merge later3226# warn "skipping off-track $commit->{hash}\n";3227next;3228}elsif(@{$commit->{parents}} >1) {3229# it is a merge commit, for each parent that is3230# not $lastpicked (not given a CVS revision number),3231# see if we can get a log3232# from the merge-base to that parent to put it3233# in the message as a merge summary.3234my@parents= @{$commit->{parents}};3235foreachmy$parent(@parents) {3236if($parenteq$lastpicked) {3237next;3238}3239# git-merge-base can potentially (but rarely) throw3240# several candidate merge bases. let's assume3241# that the first one is the best one.3242my$base=eval{3243 safe_pipe_capture('git','merge-base',3244$lastpicked,$parent);3245};3246# The two branches may not be related at all,3247# in which case merge base simply fails to find3248# any, but that's Ok.3249next if($@);32503251chomp$base;3252if($base) {3253my@merged;3254# print "want to log between $base $parent \n";3255open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3256or die"Cannot call git-log:$!";3257my$mergedhash;3258while(<GITLOG>) {3259chomp;3260if(!defined$mergedhash) {3261if(m/^commit\s+(.+)$/) {3262$mergedhash=$1;3263}else{3264next;3265}3266}else{3267# grab the first line that looks non-rfc8223268# aka has content after leading space3269if(m/^\s+(\S.*)$/) {3270my$title=$1;3271$title=substr($title,0,100);# truncate3272unshift@merged,"$mergedhash$title";3273undef$mergedhash;3274}3275}3276}3277close GITLOG;3278if(@merged) {3279$commit->{mergemsg} =$commit->{message};3280$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3281foreachmy$summary(@merged) {3282$commit->{mergemsg} .="\t$summary\n";3283}3284$commit->{mergemsg} .="\n\n";3285# print "Message for $commit->{hash} \n$commit->{mergemsg}";3286}3287}3288}3289}3290}32913292# convert the date to CVS-happy format3293my$cvsDate= convertToCvsDate($commit->{date});32943295if(defined($lastpicked) )3296{3297my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3298local($/) ="\0";3299while( <FILELIST> )3300{3301chomp;3302unless(/^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o)3303{3304die("Couldn't process git-diff-tree line :$_");3305}3306my($mode,$hash,$change) = ($1,$2,$3);3307my$name= <FILELIST>;3308chomp($name);33093310# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");33113312my$dbMode= convertToDbMode($mode);33133314if($changeeq"D")3315{3316#$log->debug("DELETE $name");3317$head->{$name} = {3318 name =>$name,3319 revision =>$head->{$name}{revision} +1,3320 filehash =>"deleted",3321 commithash =>$commit->{hash},3322 modified =>$cvsDate,3323 author =>$commit->{author},3324 mode =>$dbMode,3325};3326$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3327}3328elsif($changeeq"M"||$changeeq"T")3329{3330#$log->debug("MODIFIED $name");3331$head->{$name} = {3332 name =>$name,3333 revision =>$head->{$name}{revision} +1,3334 filehash =>$hash,3335 commithash =>$commit->{hash},3336 modified =>$cvsDate,3337 author =>$commit->{author},3338 mode =>$dbMode,3339};3340$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3341}3342elsif($changeeq"A")3343{3344#$log->debug("ADDED $name");3345$head->{$name} = {3346 name =>$name,3347 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3348 filehash =>$hash,3349 commithash =>$commit->{hash},3350 modified =>$cvsDate,3351 author =>$commit->{author},3352 mode =>$dbMode,3353};3354$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3355}3356else3357{3358$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3359die;3360}3361}3362close FILELIST;3363}else{3364# this is used to detect files removed from the repo3365my$seen_files= {};33663367my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3368local$/="\0";3369while( <FILELIST> )3370{3371chomp;3372unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3373{3374die("Couldn't process git-ls-tree line :$_");3375}33763377my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);33783379$seen_files->{$git_filename} =1;33803381my($oldhash,$oldrevision,$oldmode) = (3382$head->{$git_filename}{filehash},3383$head->{$git_filename}{revision},3384$head->{$git_filename}{mode}3385);33863387my$dbMode= convertToDbMode($mode);33883389# unless the file exists with the same hash, we need to update it ...3390unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$dbMode)3391{3392my$newrevision= ($oldrevisionor0) +1;33933394$head->{$git_filename} = {3395 name =>$git_filename,3396 revision =>$newrevision,3397 filehash =>$git_hash,3398 commithash =>$commit->{hash},3399 modified =>$cvsDate,3400 author =>$commit->{author},3401 mode =>$dbMode,3402};340334043405$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3406}3407}3408close FILELIST;34093410# Detect deleted files3411foreachmy$file(keys%$head)3412{3413unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3414{3415$head->{$file}{revision}++;3416$head->{$file}{filehash} ="deleted";3417$head->{$file}{commithash} =$commit->{hash};3418$head->{$file}{modified} =$cvsDate;3419$head->{$file}{author} =$commit->{author};34203421$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$cvsDate,$commit->{author},$head->{$file}{mode});3422}3423}3424# END : "Detect deleted files"3425}342634273428if(exists$commit->{mergemsg})3429{3430$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3431}34323433$lastpicked=$commit->{hash};34343435$self->_set_prop("last_commit",$commit->{hash});3436}34373438$self->delete_head();3439foreachmy$file(keys%$head)3440{3441$self->insert_head(3442$file,3443$head->{$file}{revision},3444$head->{$file}{filehash},3445$head->{$file}{commithash},3446$head->{$file}{modified},3447$head->{$file}{author},3448$head->{$file}{mode},3449);3450}3451# invalidate the gethead cache3452$self->clearCommitRefCaches();345334543455# Ending exclusive lock here3456$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3457}34583459sub readCommits3460{3461my$pipeHandle=shift;3462my@commits;34633464my%commit= ();34653466while( <$pipeHandle> )3467{3468chomp;3469if(m/^commit\s+(.*)$/) {3470# on ^commit lines put the just seen commit in the stack3471# and prime things for the next one3472if(keys%commit) {3473my%copy=%commit;3474unshift@commits, \%copy;3475%commit= ();3476}3477my@parents=split(m/\s+/,$1);3478$commit{hash} =shift@parents;3479$commit{parents} = \@parents;3480}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3481# on rfc822-like lines seen before we see any message,3482# lowercase the entry and put it in the hash as key-value3483$commit{lc($1)} =$2;3484}else{3485# message lines - skip initial empty line3486# and trim whitespace3487if(!exists($commit{message}) &&m/^\s*$/) {3488# define it to mark the end of headers3489$commit{message} ='';3490next;3491}3492s/^\s+//;s/\s+$//;# trim ws3493$commit{message} .=$_."\n";3494}3495}34963497unshift@commits, \%commitif(keys%commit);34983499return@commits;3500}35013502sub convertToCvsDate3503{3504my$date=shift;3505# Convert from: "git rev-list --pretty" formatted date3506# Convert to: "the format specified by RFC822 as modified by RFC1123."3507# Example: 26 May 1997 13:01:40 -04003508if($date=~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/)3509{3510$date="$2$1$4$3$5";3511}35123513return$date;3514}35153516sub convertToDbMode3517{3518my$mode=shift;35193520# NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",3521# but the database "mode" column historically (and currently)3522# only stores the "rw" (for user) part of the string.3523# FUTURE: It might make more sense to persist the raw3524# octal mode (or perhaps the final full CVS form) instead of3525# this half-converted form, but it isn't currently worth the3526# backwards compatibility headaches.35273528$mode=~/^\d\d(\d)\d{3}$/;3529my$userBits=$1;35303531my$dbMode="";3532$dbMode.="r"if($userBits&4);3533$dbMode.="w"if($userBits&2);3534$dbMode.="x"if($userBits&1);3535$dbMode="rw"if($dbModeeq"");35363537return$dbMode;3538}35393540sub insert_rev3541{3542my$self=shift;3543my$name=shift;3544my$revision=shift;3545my$filehash=shift;3546my$commithash=shift;3547my$modified=shift;3548my$author=shift;3549my$mode=shift;3550my$tablename=$self->tablename("revision");35513552my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3553$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3554}35553556sub insert_mergelog3557{3558my$self=shift;3559my$key=shift;3560my$value=shift;3561my$tablename=$self->tablename("commitmsgs");35623563my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3564$insert_mergelog->execute($key,$value);3565}35663567sub delete_head3568{3569my$self=shift;3570my$tablename=$self->tablename("head");35713572my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3573$delete_head->execute();3574}35753576sub insert_head3577{3578my$self=shift;3579my$name=shift;3580my$revision=shift;3581my$filehash=shift;3582my$commithash=shift;3583my$modified=shift;3584my$author=shift;3585my$mode=shift;3586my$tablename=$self->tablename("head");35873588my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3589$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3590}35913592sub _get_prop3593{3594my$self=shift;3595my$key=shift;3596my$tablename=$self->tablename("properties");35973598my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3599$db_query->execute($key);3600my($value) =$db_query->fetchrow_array;36013602return$value;3603}36043605sub _set_prop3606{3607my$self=shift;3608my$key=shift;3609my$value=shift;3610my$tablename=$self->tablename("properties");36113612my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3613$db_query->execute($value,$key);36143615unless($db_query->rows)3616{3617$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3618$db_query->execute($key,$value);3619}36203621return$value;3622}36233624=head2 gethead36253626=cut36273628sub gethead3629{3630my$self=shift;3631my$intRev=shift;3632my$tablename=$self->tablename("head");36333634return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );36353636my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3637$db_query->execute();36383639my$tree= [];3640while(my$file=$db_query->fetchrow_hashref)3641{3642if(!$intRev)3643{3644$file->{revision} ="1.$file->{revision}"3645}3646push@$tree,$file;3647}36483649$self->{gethead_cache} =$tree;36503651return$tree;3652}36533654=head2 getAnyHead36553656Returns a reference to an array of getmeta structures, one3657per file in the specified tree hash.36583659=cut36603661sub getAnyHead3662{3663my($self,$hash) =@_;36643665if(!defined($hash))3666{3667return$self->gethead();3668}36693670my@files;3671{3672open(my$filePipe,'-|','git','ls-tree','-z','-r',$hash)3673or die("Cannot call git-ls-tree :$!");3674local$/="\0";3675@files=<$filePipe>;3676close$filePipe;3677}36783679my$tree=[];3680my($line);3681foreach$line(@files)3682{3683$line=~s/\0$//;3684unless($line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3685{3686die("Couldn't process git-ls-tree line :$_");3687}36883689my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);3690push@$tree,$self->getMetaFromCommithash($git_filename,$hash);3691}36923693return$tree;3694}36953696=head2 getRevisionDirMap36973698A "revision dir map" contains all the plain-file filenames associated3699with a particular revision (treeish), organized by directory:37003701 $type = $out->{$dir}{$fullName}37023703The type of each is "F" (for ordinary file) or "D" (for directory,3704for which the map $out->{$fullName} will also exist).37053706=cut37073708sub getRevisionDirMap3709{3710my($self,$ver)=@_;37113712if(!defined($self->{revisionDirMapCache}))3713{3714$self->{revisionDirMapCache}={};3715}37163717# Get file list (previously cached results are dependent on HEAD,3718# but are early in each case):3719my$cacheKey;3720my(@fileList);3721if( !defined($ver) ||$vereq"")3722{3723$cacheKey="";3724if(defined($self->{revisionDirMapCache}{$cacheKey}) )3725{3726return$self->{revisionDirMapCache}{$cacheKey};3727}37283729my@head= @{$self->gethead()};3730foreachmy$file(@head)3731{3732next if($file->{filehash}eq"deleted");37333734push@fileList,$file->{name};3735}3736}3737else3738{3739my($hash)=$self->lookupCommitRef($ver);3740if( !defined($hash) )3741{3742returnundef;3743}37443745$cacheKey=$hash;3746if(defined($self->{revisionDirMapCache}{$cacheKey}) )3747{3748return$self->{revisionDirMapCache}{$cacheKey};3749}37503751open(my$filePipe,'-|','git','ls-tree','-z','-r',$hash)3752or die("Cannot call git-ls-tree :$!");3753local$/="\0";3754while( <$filePipe> )3755{3756chomp;3757unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3758{3759die("Couldn't process git-ls-tree line :$_");3760}37613762my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);37633764push@fileList,$git_filename;3765}3766close$filePipe;3767}37683769# Convert to normalized form:3770my%revMap;3771my$file;3772foreach$file(@fileList)3773{3774my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);3775$dir=''if(!defined($dir));37763777# parent directories:3778# ... create empty dir maps for parent dirs:3779my($td)=$dir;3780while(!defined($revMap{$td}))3781{3782$revMap{$td}={};37833784my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);3785$tp=''if(!defined($tp));3786$td=$tp;3787}3788# ... add children to parent maps (now that they exist):3789$td=$dir;3790while($tdne"")3791{3792my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);3793$tp=''if(!defined($tp));37943795if(defined($revMap{$tp}{$td}))3796{3797if($revMap{$tp}{$td}ne'D')3798{3799die"Weird file/directory inconsistency in$cacheKey";3800}3801last;# loop exit3802}3803$revMap{$tp}{$td}='D';38043805$td=$tp;3806}38073808# file3809$revMap{$dir}{$file}='F';3810}38113812# Save in cache:3813$self->{revisionDirMapCache}{$cacheKey}=\%revMap;3814return$self->{revisionDirMapCache}{$cacheKey};3815}38163817=head2 getlog38183819See also gethistorydense().38203821=cut38223823sub getlog3824{3825my$self=shift;3826my$filename=shift;3827my$revFilter=shift;38283829my$tablename=$self->tablename("revision");38303831# Filters:3832# TODO: date, state, or by specific logins filters?3833# TODO: Handle comma-separated list of revFilter items, each item3834# can be a range [only case currently handled] or individual3835# rev or branch or "branch.".3836# TODO: Adjust $db_query WHERE clause based on revFilter, instead of3837# manually filtering the results of the query?3838my($minrev,$maxrev);3839if(defined($revFilter)and3840$state->{opt}{r} =~/^(1.(\d+))?(::?)(1.(\d.+))?$/)3841{3842my$control=$3;3843$minrev=$2;3844$maxrev=$5;3845$minrev++if(defined($minrev)and$controleq"::");3846}38473848my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3849$db_query->execute($filename);38503851my$totalRevs=0;3852my$tree= [];3853while(my$file=$db_query->fetchrow_hashref)3854{3855$totalRevs++;3856if(defined($minrev)and$file->{revision} <$minrev)3857{3858next;3859}3860if(defined($maxrev)and$file->{revision} >$maxrev)3861{3862next;3863}38643865$file->{revision} ="1.".$file->{revision};3866push@$tree,$file;3867}38683869return($tree,$totalRevs);3870}38713872=head2 getmeta38733874This function takes a filename (with path) argument and returns a hashref of3875metadata for that file.38763877=cut38783879sub getmeta3880{3881my$self=shift;3882my$filename=shift;3883my$revision=shift;3884my$tablename_rev=$self->tablename("revision");3885my$tablename_head=$self->tablename("head");38863887my$db_query;3888if(defined($revision)and$revision=~/^1\.(\d+)$/)3889{3890my($intRev) =$1;3891$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND revision=?",{},1);3892$db_query->execute($filename,$intRev);3893}3894elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)3895{3896$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND commithash=?",{},1);3897$db_query->execute($filename,$revision);3898}else{3899$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_headWHERE name=?",{},1);3900$db_query->execute($filename);3901}39023903my$meta=$db_query->fetchrow_hashref;3904if($meta)3905{3906$meta->{revision} ="1.$meta->{revision}";3907}3908return$meta;3909}39103911sub getMetaFromCommithash3912{3913my$self=shift;3914my$filename=shift;3915my$revCommit=shift;39163917# NOTE: This function doesn't scale well (lots of forks), especially3918# if you have many files that have not been modified for many commits3919# (each git-rev-parse redoes a lot of work for each file3920# that theoretically could be done in parallel by smarter3921# graph traversal).3922#3923# TODO: Possible optimization strategies:3924# - Solve the issue of assigning and remembering "real" CVS3925# revision numbers for branches, and ensure the3926# data structure can do this efficiently. Perhaps something3927# similar to "git notes", and carefully structured to take3928# advantage same-sha1-is-same-contents, to roll the same3929# unmodified subdirectory data onto multiple commits?3930# - Write and use a C tool that is like git-blame, but3931# operates on multiple files with file granularity, instead3932# of one file with line granularity. Cache3933# most-recently-modified in $self->{commitRefCache}{$revCommit}.3934# Try to be intelligent about how many files we do with3935# one fork (perhaps one directory at a time, without recursion,3936# and/or include directory as one line item, recurse from here3937# instead of in C tool?).3938# - Perhaps we could ask the DB for (filename,fileHash),3939# and just guess that it is correct (that the file hadn't3940# changed between $revCommit and the found commit, then3941# changed back, confusing anything trying to interpret3942# history). Probably need to add another index to revisions3943# DB table for this.3944# - NOTE: Trying to store all (commit,file) keys in DB [to3945# find "lastModfiedCommit] (instead of3946# just files that changed in each commit as we do now) is3947# probably not practical from a disk space perspective.39483949# Does the file exist in $revCommit?3950# TODO: Include file hash in dirmap cache.3951my($dirMap)=$self->getRevisionDirMap($revCommit);3952my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);3953if(!defined($dir))3954{3955$dir="";3956}3957if( !defined($dirMap->{$dir}) ||3958!defined($dirMap->{$dir}{$filename}) )3959{3960my($fileHash)="deleted";39613962my($retVal)={};3963$retVal->{name}=$filename;3964$retVal->{filehash}=$fileHash;39653966# not needed and difficult to compute:3967$retVal->{revision}="0";# $revision;3968$retVal->{commithash}=$revCommit;3969#$retVal->{author}=$commit->{author};3970#$retVal->{modified}=convertToCvsDate($commit->{date});3971#$retVal->{mode}=convertToDbMode($mode);39723973return$retVal;3974}39753976my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");3977chomp$fileHash;3978if(!($fileHash=~/^[0-9a-f]{40}$/))3979{3980die"Invalid fileHash '$fileHash' looking up"3981." '$revCommit:$filename'\n";3982}39833984# information about most recent commit to modify $filename:3985open(my$gitLogPipe,'-|','git','rev-list',3986'--max-count=1','--pretty','--parents',3987$revCommit,'--',$filename)3988or die"Cannot call git-rev-list:$!";3989my@commits=readCommits($gitLogPipe);3990close$gitLogPipe;3991if(scalar(@commits)!=1)3992{3993die"Can't find most recent commit changing$filename\n";3994}3995my($commit)=$commits[0];3996if( !defined($commit) || !defined($commit->{hash}) )3997{3998returnundef;3999}40004001# does this (commit,file) have a real assigned CVS revision number?4002my$tablename_rev=$self->tablename("revision");4003my$db_query;4004$db_query=$self->{dbh}->prepare_cached(4005"SELECT * FROM$tablename_revWHERE name=? AND commithash=?",4006{},1);4007$db_query->execute($filename,$commit->{hash});4008my($meta)=$db_query->fetchrow_hashref;4009if($meta)4010{4011$meta->{revision} ="1.$meta->{revision}";4012return$meta;4013}40144015# fall back on special revision number4016my($revision)=$commit->{hash};4017$revision=~s/(..)/'.' . (hex($1)+100)/eg;4018$revision="2.1.1.2000$revision";40194020# meta data about $filename:4021open(my$filePipe,'-|','git','ls-tree','-z',4022$commit->{hash},'--',$filename)4023or die("Cannot call git-ls-tree :$!");4024local$/="\0";4025my$line;4026$line=<$filePipe>;4027if(defined(<$filePipe>))4028{4029die"Expected only a single file for git-ls-tree$filename\n";4030}4031close$filePipe;40324033chomp$line;4034unless($line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)4035{4036die("Couldn't process git-ls-tree line :$line\n");4037}4038my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);40394040# save result:4041my($retVal)={};4042$retVal->{name}=$filename;4043$retVal->{revision}=$revision;4044$retVal->{filehash}=$fileHash;4045$retVal->{commithash}=$revCommit;4046$retVal->{author}=$commit->{author};4047$retVal->{modified}=convertToCvsDate($commit->{date});4048$retVal->{mode}=convertToDbMode($mode);40494050return$retVal;4051}40524053=head2 lookupCommitRef40544055Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches4056the result so looking it up again is fast.40574058=cut40594060sub lookupCommitRef4061{4062my$self=shift;4063my$ref=shift;40644065my$commitHash=$self->{commitRefCache}{$ref};4066if(defined($commitHash))4067{4068return$commitHash;4069}40704071$commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",4072$self->unescapeRefName($ref));4073$commitHash=~s/\s*$//;4074if(!($commitHash=~/^[0-9a-f]{40}$/))4075{4076$commitHash=undef;4077}40784079if(defined($commitHash) )4080{4081my$type=safe_pipe_capture("git","cat-file","-t",$commitHash);4082if( ! ($type=~/^commit\s*$/) )4083{4084$commitHash=undef;4085}4086}4087if(defined($commitHash))4088{4089$self->{commitRefCache}{$ref}=$commitHash;4090}4091return$commitHash;4092}40934094=head2 clearCommitRefCaches40954096Clears cached commit cache (sha1's for various tags/abbeviations/etc),4097and related caches.40984099=cut41004101sub clearCommitRefCaches4102{4103my$self=shift;4104$self->{commitRefCache} = {};4105$self->{revisionDirMapCache} =undef;4106$self->{gethead_cache} =undef;4107}41084109=head2 commitmessage41104111this function takes a commithash and returns the commit message for that commit41124113=cut4114sub commitmessage4115{4116my$self=shift;4117my$commithash=shift;4118my$tablename=$self->tablename("commitmsgs");41194120die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);41214122my$db_query;4123$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);4124$db_query->execute($commithash);41254126my($message) =$db_query->fetchrow_array;41274128if(defined($message) )4129{4130$message.=" "if($message=~/\n$/);4131return$message;4132}41334134my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);4135shift@lineswhile($lines[0] =~/\S/);4136$message=join("",@lines);4137$message.=" "if($message=~/\n$/);4138return$message;4139}41404141=head2 gethistorydense41424143This function takes a filename (with path) argument and returns an arrayofarrays4144containing revision,filehash,commithash ordered by revision descending.41454146This version of gethistory skips deleted entries -- so it is useful for annotate.4147The 'dense' part is a reference to a '--dense' option available for git-rev-list4148and other git tools that depend on it.41494150See also getlog().41514152=cut4153sub gethistorydense4154{4155my$self=shift;4156my$filename=shift;4157my$tablename=$self->tablename("revision");41584159my$db_query;4160$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);4161$db_query->execute($filename);41624163my$result=$db_query->fetchall_arrayref;41644165my$i;4166for($i=0;$i<scalar(@$result) ;$i++)4167{4168$result->[$i][0]="1.".$result->[$i][0];4169}41704171return$result;4172}41734174=head2 escapeRefName41754176Apply an escape mechanism to compensate for characters that4177git ref names can have that CVS tags can not.41784179=cut4180sub escapeRefName4181{4182my($self,$refName)=@_;41834184# CVS officially only allows [-_A-Za-z0-9] in tag names (or in4185# many contexts it can also be a CVS revision number).4186#4187# Git tags commonly use '/' and '.' as well, but also handle4188# anything else just in case:4189#4190# = "_-s-" For '/'.4191# = "_-p-" For '.'.4192# = "_-u-" For underscore, in case someone wants a literal "_-" in4193# a tag name.4194# = "_-xx-" Where "xx" is the hexadecimal representation of the4195# desired ASCII character byte. (for anything else)41964197if(!$refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)4198{4199$refName=~s/_-/_-u--/g;4200$refName=~s/\./_-p-/g;4201$refName=~s%/%_-s-%g;4202$refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;4203}4204}42054206=head2 unescapeRefName42074208Undo an escape mechanism to compensate for characters that4209git ref names can have that CVS tags can not.42104211=cut4212sub unescapeRefName4213{4214my($self,$refName)=@_;42154216# see escapeRefName() for description of escape mechanism.42174218$refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;42194220# allowed tag names4221# TODO: Perhaps use git check-ref-format, with an in-process cache of4222# validated names?4223if( !($refName=~m%^[^-][-a-zA-Z0-9_/.]*$%) ||4224($refName=~m%[/.]$%) ||4225($refName=~/\.lock$/) ||4226($refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) )# matching }4227{4228# Error:4229$log->warn("illegal refName:$refName");4230$refName=undef;4231}4232return$refName;4233}42344235sub unescapeRefNameChar4236{4237my($char)=@_;42384239if($chareq"s")4240{4241$char="/";4242}4243elsif($chareq"p")4244{4245$char=".";4246}4247elsif($chareq"u")4248{4249$char="_";4250}4251elsif($char=~/^[0-9a-f][0-9a-f]$/)4252{4253$char=chr(hex($char));4254}4255else4256{4257# Error case: Maybe it has come straight from user, and4258# wasn't supposed to be escaped? Restore it the way we got it:4259$char="_-$char-";4260}42614262return$char;4263}42644265=head2 in_array()42664267from Array::PAT - mimics the in_array() function4268found in PHP. Yuck but works for small arrays.42694270=cut4271sub in_array4272{4273my($check,@array) =@_;4274my$retval=0;4275foreachmy$test(@array){4276if($checkeq$test){4277$retval=1;4278}4279}4280return$retval;4281}42824283=head2 safe_pipe_capture42844285an alternative to `command` that allows input to be passed as an array4286to work around shell problems with weird characters in arguments42874288=cut4289sub safe_pipe_capture {42904291my@output;42924293if(my$pid=open my$child,'-|') {4294@output= (<$child>);4295close$childor die join(' ',@_).":$!$?";4296}else{4297exec(@_)or die"$!$?";# exec() can fail the executable can't be found4298}4299returnwantarray?@output:join('',@output);4300}43014302=head2 mangle_dirname43034304create a string from a directory name that is suitable to use as4305part of a filename, mainly by converting all chars except \w.- to _43064307=cut4308sub mangle_dirname {4309my$dirname=shift;4310return unlessdefined$dirname;43114312$dirname=~s/[^\w.-]/_/g;43134314return$dirname;4315}43164317=head2 mangle_tablename43184319create a string from a that is suitable to use as part of an SQL table4320name, mainly by converting all chars except \w to _43214322=cut4323sub mangle_tablename {4324my$tablename=shift;4325return unlessdefined$tablename;43264327$tablename=~s/[^\w_]/_/g;43284329return$tablename;4330}433143321;