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); 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 546 argsfromdir($updater); 547 548my$addcount=0; 549 550foreachmy$filename( @{$state->{args}} ) 551{ 552$filename= filecleanup($filename); 553 554my$meta=$updater->getmeta($filename); 555my$wrev= revparse($filename); 556 557if($wrev&&$meta&& ($wrev<0)) 558{ 559# previously removed file, add back 560$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 561 562print"MT +updated\n"; 563print"MT text U\n"; 564print"MT fname$filename\n"; 565print"MT newline\n"; 566print"MT -updated\n"; 567 568unless($state->{globaloptions}{-n} ) 569{ 570my($filepart,$dirpart) = filenamesplit($filename,1); 571 572print"Created$dirpart\n"; 573print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 574 575# this is an "entries" line 576my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 577$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 578print"/$filepart/1.$meta->{revision}//$kopts/\n"; 579# permissions 580$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 581print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 582# transmit file 583 transmitfile($meta->{filehash}); 584} 585 586next; 587} 588 589unless(defined($state->{entries}{$filename}{modified_filename} ) ) 590{ 591print"E cvs add: nothing known about `$filename'\n"; 592next; 593} 594# TODO : check we're not squashing an already existing file 595if(defined($state->{entries}{$filename}{revision} ) ) 596{ 597print"E cvs add: `$filename' has already been entered\n"; 598next; 599} 600 601my($filepart,$dirpart) = filenamesplit($filename,1); 602 603print"E cvs add: scheduling file `$filename' for addition\n"; 604 605print"Checked-in$dirpart\n"; 606print"$filename\n"; 607my$kopts= kopts_from_path($filename,"file", 608$state->{entries}{$filename}{modified_filename}); 609print"/$filepart/0//$kopts/\n"; 610 611my$requestedKopts=$state->{opt}{k}; 612if(defined($requestedKopts)) 613{ 614$requestedKopts="-k$requestedKopts"; 615} 616else 617{ 618$requestedKopts=""; 619} 620if($koptsne$requestedKopts) 621{ 622$log->warn("Ignoring requested -k='$requestedKopts'" 623." for '$filename'; detected -k='$kopts' instead"); 624#TODO: Also have option to send warning to user? 625} 626 627$addcount++; 628} 629 630if($addcount==1) 631{ 632print"E cvs add: use `cvs commit' to add this file permanently\n"; 633} 634elsif($addcount>1) 635{ 636print"E cvs add: use `cvs commit' to add these files permanently\n"; 637} 638 639print"ok\n"; 640} 641 642# remove \n 643# Response expected: yes. Remove a file. This uses any previous Argument, 644# Directory, Entry, or Modified requests, if they have been sent. The last 645# Directory sent specifies the working directory at the time of the 646# operation. Note that this request does not actually do anything to the 647# repository; the only effect of a successful remove request is to supply 648# the client with a new entries line containing `-' to indicate a removed 649# file. In fact, the client probably could perform this operation without 650# contacting the server, although using remove may cause the server to 651# perform a few more checks. The client sends a subsequent ci request to 652# actually record the removal in the repository. 653sub req_remove 654{ 655my($cmd,$data) =@_; 656 657 argsplit("remove"); 658 659# Grab a handle to the SQLite db and do any necessary updates 660my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 661$updater->update(); 662 663#$log->debug("add state : " . Dumper($state)); 664 665my$rmcount=0; 666 667foreachmy$filename( @{$state->{args}} ) 668{ 669$filename= filecleanup($filename); 670 671if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 672{ 673print"E cvs remove: file `$filename' still in working directory\n"; 674next; 675} 676 677my$meta=$updater->getmeta($filename); 678my$wrev= revparse($filename); 679 680unless(defined($wrev) ) 681{ 682print"E cvs remove: nothing known about `$filename'\n"; 683next; 684} 685 686if(defined($wrev)and$wrev<0) 687{ 688print"E cvs remove: file `$filename' already scheduled for removal\n"; 689next; 690} 691 692unless($wrev==$meta->{revision} ) 693{ 694# TODO : not sure if the format of this message is quite correct. 695print"E cvs remove: Up to date check failed for `$filename'\n"; 696next; 697} 698 699 700my($filepart,$dirpart) = filenamesplit($filename,1); 701 702print"E cvs remove: scheduling `$filename' for removal\n"; 703 704print"Checked-in$dirpart\n"; 705print"$filename\n"; 706my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 707print"/$filepart/-1.$wrev//$kopts/\n"; 708 709$rmcount++; 710} 711 712if($rmcount==1) 713{ 714print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 715} 716elsif($rmcount>1) 717{ 718print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 719} 720 721print"ok\n"; 722} 723 724# Modified filename \n 725# Response expected: no. Additional data: mode, \n, file transmission. Send 726# the server a copy of one locally modified file. filename is a file within 727# the most recent directory sent with Directory; it must not contain `/'. 728# If the user is operating on only some files in a directory, only those 729# files need to be included. This can also be sent without Entry, if there 730# is no entry for the file. 731sub req_Modified 732{ 733my($cmd,$data) =@_; 734 735my$mode= <STDIN>; 736defined$mode 737or(print"E end of file reading mode for$data\n"),return; 738chomp$mode; 739my$size= <STDIN>; 740defined$size 741or(print"E end of file reading size of$data\n"),return; 742chomp$size; 743 744# Grab config information 745my$blocksize=8192; 746my$bytesleft=$size; 747my$tmp; 748 749# Get a filehandle/name to write it to 750my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 751 752# Loop over file data writing out to temporary file. 753while($bytesleft) 754{ 755$blocksize=$bytesleftif($bytesleft<$blocksize); 756read STDIN,$tmp,$blocksize; 757print$fh $tmp; 758$bytesleft-=$blocksize; 759} 760 761close$fh 762or(print"E failed to write temporary,$filename:$!\n"),return; 763 764# Ensure we have something sensible for the file mode 765if($mode=~/u=(\w+)/) 766{ 767$mode=$1; 768}else{ 769$mode="rw"; 770} 771 772# Save the file data in $state 773$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 774$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 775$state->{entries}{$state->{directory}.$data}{modified_hash} =`git hash-object$filename`; 776$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 777 778 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 779} 780 781# Unchanged filename\n 782# Response expected: no. Tell the server that filename has not been 783# modified in the checked out directory. The filename is a file within the 784# most recent directory sent with Directory; it must not contain `/'. 785sub req_Unchanged 786{ 787 my ($cmd,$data) =@_; 788 789$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 790 791 #$log->debug("req_Unchanged :$data"); 792} 793 794# Argument text\n 795# Response expected: no. Save argument for use in a subsequent command. 796# Arguments accumulate until an argument-using command is given, at which 797# point they are forgotten. 798# Argumentx text\n 799# Response expected: no. Append\nfollowed by text to the current argument 800# being saved. 801sub req_Argument 802{ 803 my ($cmd,$data) =@_; 804 805 # Argumentx means: append to last Argument (with a newline in front) 806 807$log->debug("$cmd:$data"); 808 809 if ($cmdeq 'Argumentx') { 810 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 811 } else { 812 push @{$state->{arguments}},$data; 813 } 814} 815 816# expand-modules\n 817# Response expected: yes. Expand the modules which are specified in the 818# arguments. Returns the data in Module-expansion responses. Note that the 819# server can assume that this is checkout or export, not rtag or rdiff; the 820# latter do not access the working directory and thus have no need to 821# expand modules on the client side. Expand may not be the best word for 822# what this request does. It does not necessarily tell you all the files 823# contained in a module, for example. Basically it is a way of telling you 824# which working directories the server needs to know about in order to 825# handle a checkout of the specified modules. For example, suppose that the 826# server has a module defined by 827# aliasmodule -a 1dir 828# That is, one can check out aliasmodule and it will take 1dir in the 829# repository and check it out to 1dir in the working directory. Now suppose 830# the client already has this module checked out and is planning on using 831# the co request to update it. Without using expand-modules, the client 832# would have two bad choices: it could either send information about all 833# working directories under the current directory, which could be 834# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 835# stands for 1dir, and neglect to send information for 1dir, which would 836# lead to incorrect operation. With expand-modules, the client would first 837# ask for the module to be expanded: 838sub req_expandmodules 839{ 840 my ($cmd,$data) =@_; 841 842 argsplit(); 843 844$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 845 846 unless ( ref$state->{arguments} eq "ARRAY" ) 847 { 848 print "ok\n"; 849 return; 850 } 851 852 foreach my$module( @{$state->{arguments}} ) 853 { 854$log->debug("SEND : Module-expansion$module"); 855 print "Module-expansion$module\n"; 856 } 857 858 print "ok\n"; 859 statecleanup(); 860} 861 862# co\n 863# Response expected: yes. Get files from the repository. This uses any 864# previous Argument, Directory, Entry, or Modified requests, if they have 865# been sent. Arguments to this command are module names; the client cannot 866# know what directories they correspond to except by (1) just sending the 867# co request, and then seeing what directory names the server sends back in 868# its responses, and (2) the expand-modules request. 869sub req_co 870{ 871 my ($cmd,$data) =@_; 872 873 argsplit("co"); 874 875 # Provide list of modules, if -c was used. 876 if (exists$state->{opt}{c}) { 877 my$showref= `git show-ref --heads`; 878 for my$line(split '\n',$showref) { 879 if ($line=~ m% refs/heads/(.*)$%) { 880 print "M$1\t$1\n"; 881 } 882 } 883 print "ok\n"; 884 return 1; 885 } 886 887 my$module=$state->{args}[0]; 888$state->{module} =$module; 889 my$checkout_path=$module; 890 891 # use the user specified directory if we're given it 892$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 893 894$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 895 896$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 897 898$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 899 900# Grab a handle to the SQLite db and do any necessary updates 901my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 902$updater->update(); 903 904$checkout_path=~ s|/$||;# get rid of trailing slashes 905 906# Eclipse seems to need the Clear-sticky command 907# to prepare the 'Entries' file for the new directory. 908print"Clear-sticky$checkout_path/\n"; 909print$state->{CVSROOT} ."/$module/\n"; 910print"Clear-static-directory$checkout_path/\n"; 911print$state->{CVSROOT} ."/$module/\n"; 912print"Clear-sticky$checkout_path/\n";# yes, twice 913print$state->{CVSROOT} ."/$module/\n"; 914print"Template$checkout_path/\n"; 915print$state->{CVSROOT} ."/$module/\n"; 916print"0\n"; 917 918# instruct the client that we're checking out to $checkout_path 919print"E cvs checkout: Updating$checkout_path\n"; 920 921my%seendirs= (); 922my$lastdir=''; 923 924# recursive 925sub prepdir { 926my($dir,$repodir,$remotedir,$seendirs) =@_; 927my$parent= dirname($dir); 928$dir=~ s|/+$||; 929$repodir=~ s|/+$||; 930$remotedir=~ s|/+$||; 931$parent=~ s|/+$||; 932$log->debug("announcedir$dir,$repodir,$remotedir"); 933 934if($parenteq'.'||$parenteq'./') { 935$parent=''; 936} 937# recurse to announce unseen parents first 938if(length($parent) && !exists($seendirs->{$parent})) { 939 prepdir($parent,$repodir,$remotedir,$seendirs); 940} 941# Announce that we are going to modify at the parent level 942if($parent) { 943print"E cvs checkout: Updating$remotedir/$parent\n"; 944}else{ 945print"E cvs checkout: Updating$remotedir\n"; 946} 947print"Clear-sticky$remotedir/$parent/\n"; 948print"$repodir/$parent/\n"; 949 950print"Clear-static-directory$remotedir/$dir/\n"; 951print"$repodir/$dir/\n"; 952print"Clear-sticky$remotedir/$parent/\n";# yes, twice 953print"$repodir/$parent/\n"; 954print"Template$remotedir/$dir/\n"; 955print"$repodir/$dir/\n"; 956print"0\n"; 957 958$seendirs->{$dir} =1; 959} 960 961foreachmy$git( @{$updater->gethead} ) 962{ 963# Don't want to check out deleted files 964next if($git->{filehash}eq"deleted"); 965 966my$fullName=$git->{name}; 967($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 968 969if(length($git->{dir}) &&$git->{dir}ne'./' 970&&$git->{dir}ne$lastdir) { 971unless(exists($seendirs{$git->{dir}})) { 972 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 973$checkout_path, \%seendirs); 974$lastdir=$git->{dir}; 975$seendirs{$git->{dir}} =1; 976} 977print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 978} 979 980# modification time of this file 981print"Mod-time$git->{modified}\n"; 982 983# print some information to the client 984if(defined($git->{dir} )and$git->{dir}ne"./") 985{ 986print"M U$checkout_path/$git->{dir}$git->{name}\n"; 987}else{ 988print"M U$checkout_path/$git->{name}\n"; 989} 990 991# instruct client we're sending a file to put in this path 992print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 993 994print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 995 996# this is an "entries" line 997my$kopts= kopts_from_path($fullName,"sha1",$git->{filehash}); 998print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 999# permissions1000print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";10011002# transmit file1003 transmitfile($git->{filehash});1004}10051006print"ok\n";10071008 statecleanup();1009}10101011# update \n1012# Response expected: yes. Actually do a cvs update command. This uses any1013# previous Argument, Directory, Entry, or Modified requests, if they have1014# been sent. The last Directory sent specifies the working directory at the1015# time of the operation. The -I option is not used--files which the client1016# can decide whether to ignore are not mentioned and the client sends the1017# Questionable request for others.1018sub req_update1019{1020my($cmd,$data) =@_;10211022$log->debug("req_update : ". (defined($data) ?$data:"[NULL]"));10231024 argsplit("update");10251026#1027# It may just be a client exploring the available heads/modules1028# in that case, list them as top level directories and leave it1029# at that. Eclipse uses this technique to offer you a list of1030# projects (heads in this case) to checkout.1031#1032if($state->{module}eq'') {1033my$showref=`git show-ref --heads`;1034print"E cvs update: Updating .\n";1035formy$line(split'\n',$showref) {1036if($line=~ m% refs/heads/(.*)$%) {1037print"E cvs update: New directory `$1'\n";1038}1039}1040print"ok\n";1041return1;1042}104310441045# Grab a handle to the SQLite db and do any necessary updates1046my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);10471048$updater->update();10491050 argsfromdir($updater);10511052#$log->debug("update state : " . Dumper($state));10531054my$last_dirname="///";10551056# foreach file specified on the command line ...1057foreachmy$filename( @{$state->{args}} )1058{1059$filename= filecleanup($filename);10601061$log->debug("Processing file$filename");10621063unless($state->{globaloptions}{-Q} ||$state->{globaloptions}{-q} )1064{1065my$cur_dirname= dirname($filename);1066if($cur_dirnamene$last_dirname)1067{1068$last_dirname=$cur_dirname;1069if($cur_dirnameeq"")1070{1071$cur_dirname=".";1072}1073print"E cvs update: Updating$cur_dirname\n";1074}1075}10761077# if we have a -C we should pretend we never saw modified stuff1078if(exists($state->{opt}{C} ) )1079{1080delete$state->{entries}{$filename}{modified_hash};1081delete$state->{entries}{$filename}{modified_filename};1082$state->{entries}{$filename}{unchanged} =1;1083}10841085my$meta;1086if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/)1087{1088$meta=$updater->getmeta($filename,$1);1089}else{1090$meta=$updater->getmeta($filename);1091}10921093# If -p was given, "print" the contents of the requested revision.1094if(exists($state->{opt}{p} ) ) {1095if(defined($meta->{revision} ) ) {1096$log->info("Printing '$filename' revision ".$meta->{revision});10971098 transmitfile($meta->{filehash}, {print=>1});1099}11001101next;1102}11031104if( !defined$meta)1105{1106$meta= {1107 name =>$filename,1108 revision =>0,1109 filehash =>'added'1110};1111}11121113my$oldmeta=$meta;11141115my$wrev= revparse($filename);11161117# If the working copy is an old revision, lets get that version too for comparison.1118if(defined($wrev)and$wrev!=$meta->{revision} )1119{1120$oldmeta=$updater->getmeta($filename,$wrev);1121}11221123#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");11241125# Files are up to date if the working copy and repo copy have the same revision,1126# and the working copy is unmodified _and_ the user hasn't specified -C1127next if(defined($wrev)1128and defined($meta->{revision})1129and$wrev==$meta->{revision}1130and$state->{entries}{$filename}{unchanged}1131and not exists($state->{opt}{C} ) );11321133# If the working copy and repo copy have the same revision,1134# but the working copy is modified, tell the client it's modified1135if(defined($wrev)1136and defined($meta->{revision})1137and$wrev==$meta->{revision}1138and defined($state->{entries}{$filename}{modified_hash})1139and not exists($state->{opt}{C} ) )1140{1141$log->info("Tell the client the file is modified");1142print"MT text M\n";1143print"MT fname$filename\n";1144print"MT newline\n";1145next;1146}11471148if($meta->{filehash}eq"deleted")1149{1150# TODO: If it has been modified in the sandbox, error out1151# with the appropriate message, rather than deleting a modified1152# file.11531154my($filepart,$dirpart) = filenamesplit($filename,1);11551156$log->info("Removing '$filename' from working copy (no longer in the repo)");11571158print"E cvs update: `$filename' is no longer in the repository\n";1159# Don't want to actually _DO_ the update if -n specified1160unless($state->{globaloptions}{-n} ) {1161print"Removed$dirpart\n";1162print"$filepart\n";1163}1164}1165elsif(not defined($state->{entries}{$filename}{modified_hash} )1166or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1167or$meta->{filehash}eq'added')1168{1169# normal update, just send the new revision (either U=Update,1170# or A=Add, or R=Remove)1171if(defined($wrev) &&$wrev<0)1172{1173$log->info("Tell the client the file is scheduled for removal");1174print"MT text R\n";1175print"MT fname$filename\n";1176print"MT newline\n";1177next;1178}1179elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) )1180{1181$log->info("Tell the client the file is scheduled for addition");1182print"MT text A\n";1183print"MT fname$filename\n";1184print"MT newline\n";1185next;11861187}1188else{1189$log->info("Updating '$filename' to ".$meta->{revision});1190print"MT +updated\n";1191print"MT text U\n";1192print"MT fname$filename\n";1193print"MT newline\n";1194print"MT -updated\n";1195}11961197my($filepart,$dirpart) = filenamesplit($filename,1);11981199# Don't want to actually _DO_ the update if -n specified1200unless($state->{globaloptions}{-n} )1201{1202if(defined($wrev) )1203{1204# instruct client we're sending a file to put in this path as a replacement1205print"Update-existing$dirpart\n";1206$log->debug("Updating existing file 'Update-existing$dirpart'");1207}else{1208# instruct client we're sending a file to put in this path as a new file1209print"Clear-static-directory$dirpart\n";1210print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1211print"Clear-sticky$dirpart\n";1212print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";12131214$log->debug("Creating new file 'Created$dirpart'");1215print"Created$dirpart\n";1216}1217print$state->{CVSROOT} ."/$state->{module}/$filename\n";12181219# this is an "entries" line1220my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1221$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1222print"/$filepart/1.$meta->{revision}//$kopts/\n";12231224# permissions1225$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1226print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";12271228# transmit file1229 transmitfile($meta->{filehash});1230}1231}else{1232$log->info("Updating '$filename'");1233my($filepart,$dirpart) = filenamesplit($meta->{name},1);12341235my$mergeDir= setupTmpDir();12361237my$file_local=$filepart.".mine";1238my$mergedFile="$mergeDir/$file_local";1239system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1240my$file_old=$filepart.".".$oldmeta->{revision};1241 transmitfile($oldmeta->{filehash}, { targetfile =>$file_old});1242my$file_new=$filepart.".".$meta->{revision};1243 transmitfile($meta->{filehash}, { targetfile =>$file_new});12441245# we need to merge with the local changes ( M=successful merge, C=conflict merge )1246$log->info("Merging$file_local,$file_old,$file_new");1247print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";12481249$log->debug("Temporary directory for merge is$mergeDir");12501251my$return=system("git","merge-file",$file_local,$file_old,$file_new);1252$return>>=8;12531254 cleanupTmpDir();12551256if($return==0)1257{1258$log->info("Merged successfully");1259print"M M$filename\n";1260$log->debug("Merged$dirpart");12611262# Don't want to actually _DO_ the update if -n specified1263unless($state->{globaloptions}{-n} )1264{1265print"Merged$dirpart\n";1266$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1267print$state->{CVSROOT} ."/$state->{module}/$filename\n";1268my$kopts= kopts_from_path("$dirpart/$filepart",1269"file",$mergedFile);1270$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1271print"/$filepart/1.$meta->{revision}//$kopts/\n";1272}1273}1274elsif($return==1)1275{1276$log->info("Merged with conflicts");1277print"E cvs update: conflicts found in$filename\n";1278print"M C$filename\n";12791280# Don't want to actually _DO_ the update if -n specified1281unless($state->{globaloptions}{-n} )1282{1283print"Merged$dirpart\n";1284print$state->{CVSROOT} ."/$state->{module}/$filename\n";1285my$kopts= kopts_from_path("$dirpart/$filepart",1286"file",$mergedFile);1287print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1288}1289}1290else1291{1292$log->warn("Merge failed");1293next;1294}12951296# Don't want to actually _DO_ the update if -n specified1297unless($state->{globaloptions}{-n} )1298{1299# permissions1300$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1301print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";13021303# transmit file, format is single integer on a line by itself (file1304# size) followed by the file contents1305# TODO : we should copy files in blocks1306my$data=`cat$mergedFile`;1307$log->debug("File size : " . length($data));1308 print length($data) . "\n";1309 print$data;1310 }1311 }13121313 }13141315 print "ok\n";1316}13171318sub req_ci1319{1320 my ($cmd,$data) =@_;13211322 argsplit("ci");13231324 #$log->debug("State : " . Dumper($state));13251326$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));13271328 if ($state->{method} eq 'pserver' and$state->{user} eq 'anonymous' )1329 {1330 print "error 1 anonymous user cannot commit via pserver\n";1331 cleanupWorkTree();1332 exit;1333 }13341335 if ( -e$state->{CVSROOT} . "/index" )1336 {1337$log->warn("file 'index' already exists in the git repository");1338 print "error 1 Index already exists in git repo\n";1339 cleanupWorkTree();1340 exit;1341 }13421343 # Grab a handle to the SQLite db and do any necessary updates1344 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1345$updater->update();13461347 # Remember where the head was at the beginning.1348 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1349 chomp$parenthash;1350 if ($parenthash!~ /^[0-9a-f]{40}$/) {1351 print "error 1 pserver cannot find the current HEAD of module";1352 cleanupWorkTree();1353 exit;1354 }13551356 setupWorkTree($parenthash);13571358$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");13591360$log->info("Created index '$work->{index}' for head$state->{module} - exit status$?");13611362 my@committedfiles= ();1363 my%oldmeta;13641365 # foreach file specified on the command line ...1366 foreach my$filename( @{$state->{args}} )1367 {1368 my$committedfile=$filename;1369$filename= filecleanup($filename);13701371 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );13721373 my$meta=$updater->getmeta($filename);1374$oldmeta{$filename} =$meta;13751376 my$wrev= revparse($filename);13771378 my ($filepart,$dirpart) = filenamesplit($filename);13791380 # do a checkout of the file if it is part of this tree1381 if ($wrev) {1382 system('git', 'checkout-index', '-f', '-u',$filename);1383 unless ($?== 0) {1384 die "Error running git-checkout-index -f -u$filename:$!";1385 }1386 }13871388 my$addflag= 0;1389 my$rmflag= 0;1390$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1391$addflag= 1 unless ( -e$filename);13921393 # Do up to date checking1394 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1395 {1396 # fail everything if an up to date check fails1397 print "error 1 Up to date check failed for$filename\n";1398 cleanupWorkTree();1399 exit;1400 }14011402 push@committedfiles,$committedfile;1403$log->info("Committing$filename");14041405 system("mkdir","-p",$dirpart) unless ( -d$dirpart);14061407 unless ($rmflag)1408 {1409$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1410 rename$state->{entries}{$filename}{modified_filename},$filename;14111412 # Calculate modes to remove1413 my$invmode= "";1414 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }14151416$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1417 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1418 }14191420 if ($rmflag)1421 {1422$log->info("Removing file '$filename'");1423 unlink($filename);1424 system("git", "update-index", "--remove",$filename);1425 }1426 elsif ($addflag)1427 {1428$log->info("Adding file '$filename'");1429 system("git", "update-index", "--add",$filename);1430 } else {1431$log->info("Updating file '$filename'");1432 system("git", "update-index",$filename);1433 }1434 }14351436 unless ( scalar(@committedfiles) > 0 )1437 {1438 print "E No files to commit\n";1439 print "ok\n";1440 cleanupWorkTree();1441 return;1442 }14431444 my$treehash= `git write-tree`;1445 chomp$treehash;14461447$log->debug("Treehash :$treehash, Parenthash :$parenthash");14481449 # write our commit message out if we have one ...1450 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1451 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1452 if ( defined ($cfg->{gitcvs}{commitmsgannotation} ) ) {1453 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/) {1454 print$msg_fh"\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"1455 }1456 } else {1457 print$msg_fh"\n\nvia git-CVS emulator\n";1458 }1459 close$msg_fh;14601461 my$commithash= `git commit-tree $treehash-p $parenthash<$msg_filename`;1462chomp($commithash);1463$log->info("Commit hash :$commithash");14641465unless($commithash=~/[a-zA-Z0-9]{40}/)1466{1467$log->warn("Commit failed (Invalid commit hash)");1468print"error 1 Commit failed (unknown reason)\n";1469 cleanupWorkTree();1470exit;1471}14721473### Emulate git-receive-pack by running hooks/update1474my@hook= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1475$parenthash,$commithash);1476if( -x $hook[0] ) {1477unless(system(@hook) ==0)1478{1479$log->warn("Commit failed (update hook declined to update ref)");1480print"error 1 Commit failed (update hook declined)\n";1481 cleanupWorkTree();1482exit;1483}1484}14851486### Update the ref1487if(system(qw(git update-ref -m),"cvsserver ci",1488"refs/heads/$state->{module}",$commithash,$parenthash)) {1489$log->warn("update-ref for$state->{module} failed.");1490print"error 1 Cannot commit -- update first\n";1491 cleanupWorkTree();1492exit;1493}14941495### Emulate git-receive-pack by running hooks/post-receive1496my$hook=$ENV{GIT_DIR}.'hooks/post-receive';1497if( -x $hook) {1498open(my$pipe,"|$hook") ||die"can't fork$!";14991500local$SIG{PIPE} =sub{die'pipe broke'};15011502print$pipe"$parenthash$commithashrefs/heads/$state->{module}\n";15031504close$pipe||die"bad pipe:$!$?";1505}15061507$updater->update();15081509### Then hooks/post-update1510$hook=$ENV{GIT_DIR}.'hooks/post-update';1511if(-x $hook) {1512system($hook,"refs/heads/$state->{module}");1513}15141515# foreach file specified on the command line ...1516foreachmy$filename(@committedfiles)1517{1518$filename= filecleanup($filename);15191520my$meta=$updater->getmeta($filename);1521unless(defined$meta->{revision}) {1522$meta->{revision} =1;1523}15241525my($filepart,$dirpart) = filenamesplit($filename,1);15261527$log->debug("Checked-in$dirpart:$filename");15281529print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1530if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1531{1532print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1533print"Remove-entry$dirpart\n";1534print"$filename\n";1535}else{1536if($meta->{revision} ==1) {1537print"M initial revision: 1.1\n";1538}else{1539print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1540}1541print"Checked-in$dirpart\n";1542print"$filename\n";1543my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1544print"/$filepart/1.$meta->{revision}//$kopts/\n";1545}1546}15471548 cleanupWorkTree();1549print"ok\n";1550}15511552sub req_status1553{1554my($cmd,$data) =@_;15551556 argsplit("status");15571558$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1559#$log->debug("status state : " . Dumper($state));15601561# Grab a handle to the SQLite db and do any necessary updates1562my$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 should be providing status on ...1566 argsfromdir($updater);15671568# foreach file specified on the command line ...1569foreachmy$filename( @{$state->{args}} )1570{1571$filename= filecleanup($filename);15721573next ifexists($state->{opt}{l}) &&index($filename,'/',length($state->{prependdir})) >=0;15741575my$meta=$updater->getmeta($filename);1576my$oldmeta=$meta;15771578my$wrev= revparse($filename);15791580# If the working copy is an old revision, lets get that version too for comparison.1581if(defined($wrev)and$wrev!=$meta->{revision} )1582{1583$oldmeta=$updater->getmeta($filename,$wrev);1584}15851586# TODO : All possible statuses aren't yet implemented1587my$status;1588# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1589$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1590and1591( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1592or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1593);15941595# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1596$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1597and1598($state->{entries}{$filename}{unchanged}1599or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1600);16011602# Need checkout if it exists in the repo but doesn't have a working copy1603$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );16041605# Locally modified if working copy and repo copy have the same revision but there are local changes1606$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );16071608# Needs Merge if working copy revision is less than repo copy and there are local changes1609$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );16101611$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1612$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1613$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1614$status||="File had conflicts on merge"if(0);16151616$status||="Unknown";16171618my($filepart) = filenamesplit($filename);16191620print"M ===================================================================\n";1621print"M File:$filepart\tStatus:$status\n";1622if(defined($state->{entries}{$filename}{revision}) )1623{1624print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1625}else{1626print"M Working revision:\tNo entry for$filename\n";1627}1628if(defined($meta->{revision}) )1629{1630print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1631print"M Sticky Tag:\t\t(none)\n";1632print"M Sticky Date:\t\t(none)\n";1633print"M Sticky Options:\t\t(none)\n";1634}else{1635print"M Repository revision:\tNo revision control file\n";1636}1637print"M\n";1638}16391640print"ok\n";1641}16421643sub req_diff1644{1645my($cmd,$data) =@_;16461647 argsplit("diff");16481649$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1650#$log->debug("status state : " . Dumper($state));16511652my($revision1,$revision2);1653if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1654{1655$revision1=$state->{opt}{r}[0];1656$revision2=$state->{opt}{r}[1];1657}else{1658$revision1=$state->{opt}{r};1659}16601661$revision1=~s/^1\.//if(defined($revision1) );1662$revision2=~s/^1\.//if(defined($revision2) );16631664$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );16651666# Grab a handle to the SQLite db and do any necessary updates1667my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1668$updater->update();16691670# if no files were specified, we need to work out what files we should be providing status on ...1671 argsfromdir($updater);16721673# foreach file specified on the command line ...1674foreachmy$filename( @{$state->{args}} )1675{1676$filename= filecleanup($filename);16771678my($fh,$file1,$file2,$meta1,$meta2,$filediff);16791680my$wrev= revparse($filename);16811682# We need _something_ to diff against1683next unless(defined($wrev) );16841685# if we have a -r switch, use it1686if(defined($revision1) )1687{1688(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1689$meta1=$updater->getmeta($filename,$revision1);1690unless(defined($meta1)and$meta1->{filehash}ne"deleted")1691{1692print"E File$filenameat revision 1.$revision1doesn't exist\n";1693next;1694}1695 transmitfile($meta1->{filehash}, { targetfile =>$file1});1696}1697# otherwise we just use the working copy revision1698else1699{1700(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1701$meta1=$updater->getmeta($filename,$wrev);1702 transmitfile($meta1->{filehash}, { targetfile =>$file1});1703}17041705# if we have a second -r switch, use it too1706if(defined($revision2) )1707{1708(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1709$meta2=$updater->getmeta($filename,$revision2);17101711unless(defined($meta2)and$meta2->{filehash}ne"deleted")1712{1713print"E File$filenameat revision 1.$revision2doesn't exist\n";1714next;1715}17161717 transmitfile($meta2->{filehash}, { targetfile =>$file2});1718}1719# otherwise we just use the working copy1720else1721{1722$file2=$state->{entries}{$filename}{modified_filename};1723}17241725# if we have been given -r, and we don't have a $file2 yet, lets get one1726if(defined($revision1)and not defined($file2) )1727{1728(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1729$meta2=$updater->getmeta($filename,$wrev);1730 transmitfile($meta2->{filehash}, { targetfile =>$file2});1731}17321733# We need to have retrieved something useful1734next unless(defined($meta1) );17351736# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1737next if(not defined($meta2)and$wrev==$meta1->{revision}1738and1739( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1740or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1741);17421743# Apparently we only show diffs for locally modified files1744next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );17451746print"M Index:$filename\n";1747print"M ===================================================================\n";1748print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1749print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1750print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1751print"M diff ";1752foreachmy$opt(keys%{$state->{opt}} )1753{1754if(ref$state->{opt}{$opt}eq"ARRAY")1755{1756foreachmy$value( @{$state->{opt}{$opt}} )1757{1758print"-$opt$value";1759}1760}else{1761print"-$opt";1762print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1763}1764}1765print"$filename\n";17661767$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));17681769($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);17701771if(exists$state->{opt}{u} )1772{1773system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1774}else{1775system("diff$file1$file2>$filediff");1776}17771778while( <$fh> )1779{1780print"M$_";1781}1782close$fh;1783}17841785print"ok\n";1786}17871788sub req_log1789{1790my($cmd,$data) =@_;17911792 argsplit("log");17931794$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1795#$log->debug("log state : " . Dumper($state));17961797my($minrev,$maxrev);1798if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1799{1800my$control=$2;1801$minrev=$1;1802$maxrev=$3;1803$minrev=~s/^1\.//if(defined($minrev) );1804$maxrev=~s/^1\.//if(defined($maxrev) );1805$minrev++if(defined($minrev)and$controleq"::");1806}18071808# Grab a handle to the SQLite db and do any necessary updates1809my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1810$updater->update();18111812# if no files were specified, we need to work out what files we should be providing status on ...1813 argsfromdir($updater);18141815# foreach file specified on the command line ...1816foreachmy$filename( @{$state->{args}} )1817{1818$filename= filecleanup($filename);18191820my$headmeta=$updater->getmeta($filename);18211822my$revisions=$updater->getlog($filename);1823my$totalrevisions=scalar(@$revisions);18241825if(defined($minrev) )1826{1827$log->debug("Removing revisions less than$minrev");1828while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1829{1830pop@$revisions;1831}1832}1833if(defined($maxrev) )1834{1835$log->debug("Removing revisions greater than$maxrev");1836while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1837{1838shift@$revisions;1839}1840}18411842next unless(scalar(@$revisions) );18431844print"M\n";1845print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1846print"M Working file:$filename\n";1847print"M head: 1.$headmeta->{revision}\n";1848print"M branch:\n";1849print"M locks: strict\n";1850print"M access list:\n";1851print"M symbolic names:\n";1852print"M keyword substitution: kv\n";1853print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1854print"M description:\n";18551856foreachmy$revision(@$revisions)1857{1858print"M ----------------------------\n";1859print"M revision 1.$revision->{revision}\n";1860# reformat the date for log output1861$revision->{modified} =sprintf('%04d/%02d/%02d%s',$3,$DATE_LIST->{$2},$1,$4)if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and defined($DATE_LIST->{$2}) );1862$revision->{author} = cvs_author($revision->{author});1863print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1864my$commitmessage=$updater->commitmessage($revision->{commithash});1865$commitmessage=~s/^/M /mg;1866print$commitmessage."\n";1867}1868print"M =============================================================================\n";1869}18701871print"ok\n";1872}18731874sub req_annotate1875{1876my($cmd,$data) =@_;18771878 argsplit("annotate");18791880$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1881#$log->debug("status state : " . Dumper($state));18821883# Grab a handle to the SQLite db and do any necessary updates1884my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1885$updater->update();18861887# if no files were specified, we need to work out what files we should be providing annotate on ...1888 argsfromdir($updater);18891890# we'll need a temporary checkout dir1891 setupWorkTree();18921893$log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");18941895# foreach file specified on the command line ...1896foreachmy$filename( @{$state->{args}} )1897{1898$filename= filecleanup($filename);18991900my$meta=$updater->getmeta($filename);19011902next unless($meta->{revision} );19031904# get all the commits that this file was in1905# in dense format -- aka skip dead revisions1906my$revisions=$updater->gethistorydense($filename);1907my$lastseenin=$revisions->[0][2];19081909# populate the temporary index based on the latest commit were we saw1910# the file -- but do it cheaply without checking out any files1911# TODO: if we got a revision from the client, use that instead1912# to look up the commithash in sqlite (still good to default to1913# the current head as we do now)1914system("git","read-tree",$lastseenin);1915unless($?==0)1916{1917print"E error running git-read-tree$lastseenin$ENV{GIT_INDEX_FILE}$!\n";1918return;1919}1920$log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit$lastseenin- exit status$?");19211922# do a checkout of the file1923system('git','checkout-index','-f','-u',$filename);1924unless($?==0) {1925print"E error running git-checkout-index -f -u$filename:$!\n";1926return;1927}19281929$log->info("Annotate$filename");19301931# Prepare a file with the commits from the linearized1932# history that annotate should know about. This prevents1933# git-jsannotate telling us about commits we are hiding1934# from the client.19351936my$a_hints="$work->{workDir}/.annotate_hints";1937if(!open(ANNOTATEHINTS,'>',$a_hints)) {1938print"E failed to open '$a_hints' for writing:$!\n";1939return;1940}1941for(my$i=0;$i<@$revisions;$i++)1942{1943print ANNOTATEHINTS $revisions->[$i][2];1944if($i+1<@$revisions) {# have we got a parent?1945print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1946}1947print ANNOTATEHINTS "\n";1948}19491950print ANNOTATEHINTS "\n";1951close ANNOTATEHINTS1952or(print"E failed to write$a_hints:$!\n"),return;19531954my@cmd= (qw(git annotate -l -S),$a_hints,$filename);1955if(!open(ANNOTATE,"-|",@cmd)) {1956print"E error invoking ".join(' ',@cmd) .":$!\n";1957return;1958}1959my$metadata= {};1960print"E Annotations for$filename\n";1961print"E ***************\n";1962while( <ANNOTATE> )1963{1964if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1965{1966my$commithash=$1;1967my$data=$2;1968unless(defined($metadata->{$commithash} ) )1969{1970$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1971$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});1972$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1973}1974printf("M 1.%-5d (%-8s%10s):%s\n",1975$metadata->{$commithash}{revision},1976$metadata->{$commithash}{author},1977$metadata->{$commithash}{modified},1978$data1979);1980}else{1981$log->warn("Error in annotate output! LINE:$_");1982print"E Annotate error\n";1983next;1984}1985}1986close ANNOTATE;1987}19881989# done; get out of the tempdir1990 cleanupWorkTree();19911992print"ok\n";19931994}19951996# This method takes the state->{arguments} array and produces two new arrays.1997# The first is $state->{args} which is everything before the '--' argument, and1998# the second is $state->{files} which is everything after it.1999sub argsplit2000{2001$state->{args} = [];2002$state->{files} = [];2003$state->{opt} = {};20042005return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");20062007my$type=shift;20082009if(defined($type) )2010{2011my$opt= {};2012$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");2013$opt= { v =>0, l =>0, R =>0}if($typeeq"status");2014$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");2015$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");2016$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");2017$opt= { k =>1, m =>1}if($typeeq"add");2018$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");2019$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");202020212022while(scalar( @{$state->{arguments}} ) >0)2023{2024my$arg=shift@{$state->{arguments}};20252026next if($argeq"--");2027next unless($arg=~/\S/);20282029# if the argument looks like a switch2030if($arg=~/^-(\w)(.*)/)2031{2032# if it's a switch that takes an argument2033if($opt->{$1} )2034{2035# If this switch has already been provided2036if($opt->{$1} >1and exists($state->{opt}{$1} ) )2037{2038$state->{opt}{$1} = [$state->{opt}{$1} ];2039if(length($2) >0)2040{2041push@{$state->{opt}{$1}},$2;2042}else{2043push@{$state->{opt}{$1}},shift@{$state->{arguments}};2044}2045}else{2046# if there's extra data in the arg, use that as the argument for the switch2047if(length($2) >0)2048{2049$state->{opt}{$1} =$2;2050}else{2051$state->{opt}{$1} =shift@{$state->{arguments}};2052}2053}2054}else{2055$state->{opt}{$1} =undef;2056}2057}2058else2059{2060push@{$state->{args}},$arg;2061}2062}2063}2064else2065{2066my$mode=0;20672068foreachmy$value( @{$state->{arguments}} )2069{2070if($valueeq"--")2071{2072$mode++;2073next;2074}2075push@{$state->{args}},$valueif($mode==0);2076push@{$state->{files}},$valueif($mode==1);2077}2078}2079}20802081# This method uses $state->{directory} to populate $state->{args} with a list of filenames2082sub argsfromdir2083{2084my$updater=shift;20852086$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");20872088return if(scalar( @{$state->{args}} ) >1);20892090my@gethead= @{$updater->gethead};20912092# push added files2093foreachmy$file(keys%{$state->{entries}}) {2094if(exists$state->{entries}{$file}{revision} &&2095$state->{entries}{$file}{revision} ==0)2096{2097push@gethead, { name =>$file, filehash =>'added'};2098}2099}21002101if(scalar(@{$state->{args}}) ==1)2102{2103my$arg=$state->{args}[0];2104$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );21052106$log->info("Only one arg specified, checking for directory expansion on '$arg'");21072108foreachmy$file(@gethead)2109{2110next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2111next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);2112push@{$state->{args}},$file->{name};2113}21142115shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);2116}else{2117$log->info("Only one arg specified, populating file list automatically");21182119$state->{args} = [];21202121foreachmy$file(@gethead)2122{2123next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2124next unless($file->{name} =~s/^$state->{prependdir}//);2125push@{$state->{args}},$file->{name};2126}2127}2128}21292130# This method cleans up the $state variable after a command that uses arguments has run2131sub statecleanup2132{2133$state->{files} = [];2134$state->{args} = [];2135$state->{arguments} = [];2136$state->{entries} = {};2137}21382139# Return working directory revision int "X" from CVS revision "1.X" out2140# of the the working directory "entries" state, for the given filename.2141# Return negative "X" to represent the file is scheduled for removal2142# when it is committed.2143sub revparse2144{2145my$filename=shift;21462147returnundefunless(defined($state->{entries}{$filename}{revision} ) );21482149return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);2150return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);21512152returnundef;2153}21542155# This method takes a file hash and does a CVS "file transfer". Its2156# exact behaviour depends on a second, optional hash table argument:2157# - If $options->{targetfile}, dump the contents to that file;2158# - If $options->{print}, use M/MT to transmit the contents one line2159# at a time;2160# - Otherwise, transmit the size of the file, followed by the file2161# contents.2162sub transmitfile2163{2164my$filehash=shift;2165my$options=shift;21662167if(defined($filehash)and$filehasheq"deleted")2168{2169$log->warn("filehash is 'deleted'");2170return;2171}21722173die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);21742175my$type=`git cat-file -t$filehash`;2176 chomp$type;21772178 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );21792180 my$size= `git cat-file -s $filehash`;2181chomp$size;21822183$log->debug("transmitfile($filehash) size=$size, type=$type");21842185if(open my$fh,'-|',"git","cat-file","blob",$filehash)2186{2187if(defined($options->{targetfile} ) )2188{2189my$targetfile=$options->{targetfile};2190open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2191print NEWFILE $_while( <$fh> );2192close NEWFILE or die("Failed to write '$targetfile':$!");2193}elsif(defined($options->{print} ) &&$options->{print} ) {2194while( <$fh> ) {2195if(/\n\z/) {2196print'M ',$_;2197}else{2198print'MT text ',$_,"\n";2199}2200}2201}else{2202print"$size\n";2203printwhile( <$fh> );2204}2205close$fhor die("Couldn't close filehandle for transmitfile():$!");2206}else{2207die("Couldn't execute git-cat-file");2208}2209}22102211# This method takes a file name, and returns ( $dirpart, $filepart ) which2212# refers to the directory portion and the file portion of the filename2213# respectively2214sub filenamesplit2215{2216my$filename=shift;2217my$fixforlocaldir=shift;22182219my($filepart,$dirpart) = ($filename,".");2220($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2221$dirpart.="/";22222223if($fixforlocaldir)2224{2225$dirpart=~s/^$state->{prependdir}//;2226}22272228return($filepart,$dirpart);2229}22302231sub filecleanup2232{2233my$filename=shift;22342235returnundefunless(defined($filename));2236if($filename=~/^\// )2237{2238print"E absolute filenames '$filename' not supported by server\n";2239returnundef;2240}22412242$filename=~s/^\.\///g;2243$filename=$state->{prependdir} .$filename;2244return$filename;2245}22462247sub validateGitDir2248{2249if( !defined($state->{CVSROOT}) )2250{2251print"error 1 CVSROOT not specified\n";2252 cleanupWorkTree();2253exit;2254}2255if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2256{2257print"error 1 Internally inconsistent CVSROOT\n";2258 cleanupWorkTree();2259exit;2260}2261}22622263# Setup working directory in a work tree with the requested version2264# loaded in the index.2265sub setupWorkTree2266{2267my($ver) =@_;22682269 validateGitDir();22702271if( (defined($work->{state}) &&$work->{state} !=1) ||2272defined($work->{tmpDir}) )2273{2274$log->warn("Bad work tree state management");2275print"error 1 Internal setup multiple work trees without cleanup\n";2276 cleanupWorkTree();2277exit;2278}22792280$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);22812282if( !defined($work->{index}) )2283{2284(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2285}22862287chdir$work->{workDir}or2288die"Unable to chdir to$work->{workDir}\n";22892290$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");22912292$ENV{GIT_WORK_TREE} =".";2293$ENV{GIT_INDEX_FILE} =$work->{index};2294$work->{state} =2;22952296if($ver)2297{2298system("git","read-tree",$ver);2299unless($?==0)2300{2301$log->warn("Error running git-read-tree");2302die"Error running git-read-tree$verin$work->{workDir}$!\n";2303}2304}2305# else # req_annotate reads tree for each file2306}23072308# Ensure current directory is in some kind of working directory,2309# with a recent version loaded in the index.2310sub ensureWorkTree2311{2312if(defined($work->{tmpDir}) )2313{2314$log->warn("Bad work tree state management [ensureWorkTree()]");2315print"error 1 Internal setup multiple dirs without cleanup\n";2316 cleanupWorkTree();2317exit;2318}2319if($work->{state} )2320{2321return;2322}23232324 validateGitDir();23252326if( !defined($work->{emptyDir}) )2327{2328$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2329}2330chdir$work->{emptyDir}or2331die"Unable to chdir to$work->{emptyDir}\n";23322333my$ver=`git show-ref -s refs/heads/$state->{module}`;2334chomp$ver;2335if($ver!~/^[0-9a-f]{40}$/)2336{2337$log->warn("Error from git show-ref -s refs/head$state->{module}");2338print"error 1 cannot find the current HEAD of module";2339 cleanupWorkTree();2340exit;2341}23422343if( !defined($work->{index}) )2344{2345(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2346}23472348$ENV{GIT_WORK_TREE} =".";2349$ENV{GIT_INDEX_FILE} =$work->{index};2350$work->{state} =1;23512352system("git","read-tree",$ver);2353unless($?==0)2354{2355die"Error running git-read-tree$ver$!\n";2356}2357}23582359# Cleanup working directory that is not needed any longer.2360sub cleanupWorkTree2361{2362if( !$work->{state} )2363{2364return;2365}23662367chdir"/"or die"Unable to chdir '/'\n";23682369if(defined($work->{workDir}) )2370{2371 rmtree($work->{workDir} );2372undef$work->{workDir};2373}2374undef$work->{state};2375}23762377# Setup a temporary directory (not a working tree), typically for2378# merging dirty state as in req_update.2379sub setupTmpDir2380{2381$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2382chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";23832384return$work->{tmpDir};2385}23862387# Clean up a previously setupTmpDir. Restore previous work tree if2388# appropriate.2389sub cleanupTmpDir2390{2391if( !defined($work->{tmpDir}) )2392{2393$log->warn("cleanup tmpdir that has not been setup");2394die"Cleanup tmpDir that has not been setup\n";2395}2396if(defined($work->{state}) )2397{2398if($work->{state} ==1)2399{2400chdir$work->{emptyDir}or2401die"Unable to chdir to$work->{emptyDir}\n";2402}2403elsif($work->{state} ==2)2404{2405chdir$work->{workDir}or2406die"Unable to chdir to$work->{emptyDir}\n";2407}2408else2409{2410$log->warn("Inconsistent work dir state");2411die"Inconsistent work dir state\n";2412}2413}2414else2415{2416chdir"/"or die"Unable to chdir '/'\n";2417}2418}24192420# Given a path, this function returns a string containing the kopts2421# that should go into that path's Entries line. For example, a binary2422# file should get -kb.2423sub kopts_from_path2424{2425my($path,$srcType,$name) =@_;24262427if(defined($cfg->{gitcvs}{usecrlfattr} )and2428$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2429{2430my($val) = check_attr("text",$path);2431if($valeq"unspecified")2432{2433$val= check_attr("crlf",$path);2434}2435if($valeq"unset")2436{2437return"-kb"2438}2439elsif( check_attr("eol",$path)ne"unspecified"||2440$valeq"set"||$valeq"input")2441{2442return"";2443}2444else2445{2446$log->info("Unrecognized check_attr crlf$path:$val");2447}2448}24492450if(defined($cfg->{gitcvs}{allbinary} ) )2451{2452if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2453{2454return"-kb";2455}2456elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2457{2458if( is_binary($srcType,$name) )2459{2460$log->debug("... as binary");2461return"-kb";2462}2463else2464{2465$log->debug("... as text");2466}2467}2468}2469# Return "" to give no special treatment to any path2470return"";2471}24722473sub check_attr2474{2475my($attr,$path) =@_;2476 ensureWorkTree();2477if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2478{2479my$val= <$fh>;2480close$fh;2481$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2482return$val;2483}2484else2485{2486returnundef;2487}2488}24892490# This should have the same heuristics as convert.c:is_binary() and related.2491# Note that the bare CR test is done by callers in convert.c.2492sub is_binary2493{2494my($srcType,$name) =@_;2495$log->debug("is_binary($srcType,$name)");24962497# Minimize amount of interpreted code run in the inner per-character2498# loop for large files, by totalling each character value and2499# then analyzing the totals.2500my@counts;2501my$i;2502for($i=0;$i<256;$i++)2503{2504$counts[$i]=0;2505}25062507my$fh= open_blob_or_die($srcType,$name);2508my$line;2509while(defined($line=<$fh>) )2510{2511# Any '\0' and bare CR are considered binary.2512if($line=~/\0|(\r[^\n])/)2513{2514close($fh);2515return1;2516}25172518# Count up each character in the line:2519my$len=length($line);2520for($i=0;$i<$len;$i++)2521{2522$counts[ord(substr($line,$i,1))]++;2523}2524}2525close$fh;25262527# Don't count CR and LF as either printable/nonprintable2528$counts[ord("\n")]=0;2529$counts[ord("\r")]=0;25302531# Categorize individual character count into printable and nonprintable:2532my$printable=0;2533my$nonprintable=0;2534for($i=0;$i<256;$i++)2535{2536if($i<32&&2537$i!=ord("\b") &&2538$i!=ord("\t") &&2539$i!=033&&# ESC2540$i!=014)# FF2541{2542$nonprintable+=$counts[$i];2543}2544elsif($i==127)# DEL2545{2546$nonprintable+=$counts[$i];2547}2548else2549{2550$printable+=$counts[$i];2551}2552}25532554return($printable>>7) <$nonprintable;2555}25562557# Returns open file handle. Possible invocations:2558# - open_blob_or_die("file",$filename);2559# - open_blob_or_die("sha1",$filehash);2560sub open_blob_or_die2561{2562my($srcType,$name) =@_;2563my($fh);2564if($srcTypeeq"file")2565{2566if( !open$fh,"<",$name)2567{2568$log->warn("Unable to open file$name:$!");2569die"Unable to open file$name:$!\n";2570}2571}2572elsif($srcTypeeq"sha1")2573{2574unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2575{2576$log->warn("Need filehash");2577die"Need filehash\n";2578}25792580my$type=`git cat-file -t$name`;2581 chomp$type;25822583 unless ( defined ($type) and$typeeq "blob" )2584 {2585$log->warn("Invalid type '$type' for '$name'");2586 die ( "Invalid type '$type' (expected 'blob')" )2587 }25882589 my$size= `git cat-file -s $name`;2590chomp$size;25912592$log->debug("open_blob_or_die($name) size=$size, type=$type");25932594unless(open$fh,'-|',"git","cat-file","blob",$name)2595{2596$log->warn("Unable to open sha1$name");2597die"Unable to open sha1$name\n";2598}2599}2600else2601{2602$log->warn("Unknown type of blob source:$srcType");2603die"Unknown type of blob source:$srcType\n";2604}2605return$fh;2606}26072608# Generate a CVS author name from Git author information, by taking the local2609# part of the email address and replacing characters not in the Portable2610# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2611# Login names are Unix login names, which should be restricted to this2612# character set.2613sub cvs_author2614{2615my$author_line=shift;2616(my$author) =$author_line=~/<([^@>]*)/;26172618$author=~s/[^-a-zA-Z0-9_.]/_/g;2619$author=~s/^-/_/;26202621$author;2622}262326242625sub descramble2626{2627# This table is from src/scramble.c in the CVS source2628my@SHIFTS= (26290,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,263016,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2631114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2632111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,263341,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2634125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,263536,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,263658,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2637225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2638199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2639174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2640207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2641192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2642227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2643182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2644243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522645);2646my($str) =@_;26472648# This should never happen, the same password format (A) has been2649# used by CVS since the beginning of time2650{2651my$fmt=substr($str,0,1);2652die"invalid password format `$fmt'"unless$fmteq'A';2653}26542655my@str=unpack"C*",substr($str,1);2656my$ret=join'',map{chr$SHIFTS[$_] }@str;2657return$ret;2658}265926602661package GITCVS::log;26622663####2664#### Copyright The Open University UK - 2006.2665####2666#### Authors: Martyn Smith <martyn@catalyst.net.nz>2667#### Martin Langhoff <martin@laptop.org>2668####2669####26702671use strict;2672use warnings;26732674=head1 NAME26752676GITCVS::log26772678=head1 DESCRIPTION26792680This module provides very crude logging with a similar interface to2681Log::Log4perl26822683=head1 METHODS26842685=cut26862687=head2 new26882689Creates a new log object, optionally you can specify a filename here to2690indicate the file to log to. If no log file is specified, you can specify one2691later with method setfile, or indicate you no longer want logging with method2692nofile.26932694Until one of these methods is called, all log calls will buffer messages ready2695to write out.26962697=cut2698sub new2699{2700my$class=shift;2701my$filename=shift;27022703my$self= {};27042705bless$self,$class;27062707if(defined($filename) )2708{2709open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2710}27112712return$self;2713}27142715=head2 setfile27162717This methods takes a filename, and attempts to open that file as the log file.2718If successful, all buffered data is written out to the file, and any further2719logging is written directly to the file.27202721=cut2722sub setfile2723{2724my$self=shift;2725my$filename=shift;27262727if(defined($filename) )2728{2729open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2730}27312732return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");27332734while(my$line=shift@{$self->{buffer}} )2735{2736print{$self->{fh}}$line;2737}2738}27392740=head2 nofile27412742This method indicates no logging is going to be used. It flushes any entries in2743the internal buffer, and sets a flag to ensure no further data is put there.27442745=cut2746sub nofile2747{2748my$self=shift;27492750$self->{nolog} =1;27512752return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");27532754$self->{buffer} = [];2755}27562757=head2 _logopen27582759Internal method. Returns true if the log file is open, false otherwise.27602761=cut2762sub _logopen2763{2764my$self=shift;27652766return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2767return0;2768}27692770=head2 debug info warn fatal27712772These four methods are wrappers to _log. They provide the actual interface for2773logging data.27742775=cut2776sub debug {my$self=shift;$self->_log("debug",@_); }2777sub info {my$self=shift;$self->_log("info",@_); }2778subwarn{my$self=shift;$self->_log("warn",@_); }2779sub fatal {my$self=shift;$self->_log("fatal",@_); }27802781=head2 _log27822783This is an internal method called by the logging functions. It generates a2784timestamp and pushes the logged line either to file, or internal buffer.27852786=cut2787sub _log2788{2789my$self=shift;2790my$level=shift;27912792return if($self->{nolog} );27932794my@time=localtime;2795my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2796$time[5] +1900,2797$time[4] +1,2798$time[3],2799$time[2],2800$time[1],2801$time[0],2802uc$level,2803);28042805if($self->_logopen)2806{2807print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2808}else{2809push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2810}2811}28122813=head2 DESTROY28142815This method simply closes the file handle if one is open28162817=cut2818sub DESTROY2819{2820my$self=shift;28212822if($self->_logopen)2823{2824close$self->{fh};2825}2826}28272828package GITCVS::updater;28292830####2831#### Copyright The Open University UK - 2006.2832####2833#### Authors: Martyn Smith <martyn@catalyst.net.nz>2834#### Martin Langhoff <martin@laptop.org>2835####2836####28372838use strict;2839use warnings;2840use DBI;28412842=head1 METHODS28432844=cut28452846=head2 new28472848=cut2849sub new2850{2851my$class=shift;2852my$config=shift;2853my$module=shift;2854my$log=shift;28552856die"Need to specify a git repository"unless(defined($config)and-d $config);2857die"Need to specify a module"unless(defined($module) );28582859$class=ref($class) ||$class;28602861my$self= {};28622863bless$self,$class;28642865$self->{valid_tables} = {'revision'=>1,2866'revision_ix1'=>1,2867'revision_ix2'=>1,2868'head'=>1,2869'head_ix1'=>1,2870'properties'=>1,2871'commitmsgs'=>1};28722873$self->{module} =$module;2874$self->{git_path} =$config."/";28752876$self->{log} =$log;28772878die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );28792880$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2881$cfg->{gitcvs}{dbdriver} ||"SQLite";2882$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2883$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2884$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2885$cfg->{gitcvs}{dbuser} ||"";2886$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2887$cfg->{gitcvs}{dbpass} ||"";2888$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||2889$cfg->{gitcvs}{dbtablenameprefix} ||"";2890my%mapping= ( m =>$module,2891 a =>$state->{method},2892 u =>getlogin||getpwuid($<) || $<,2893 G =>$self->{git_path},2894 g => mangle_dirname($self->{git_path}),2895);2896$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2897$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;2898$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;2899$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});29002901die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2902die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2903$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2904$self->{dbuser},2905$self->{dbpass});2906die"Error connecting to database\n"unlessdefined$self->{dbh};29072908$self->{tables} = {};2909foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2910{2911$self->{tables}{$table} =1;2912}29132914# Construct the revision table if required2915# The revision table stores an entry for each file, each time that file2916# changes.2917# numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )2918# This is not sufficient to support "-r {commithash}" for any2919# files except files that were modified by that commit (also,2920# some places in the code ignore/effectively strip out -r in2921# some cases, before it gets passed to getmeta()).2922# The "filehash" field typically has a git blob hash, but can also2923# be set to "dead" to indicate that the given version of the file2924# should not exist in the sandbox.2925unless($self->{tables}{$self->tablename("revision")} )2926{2927my$tablename=$self->tablename("revision");2928my$ix1name=$self->tablename("revision_ix1");2929my$ix2name=$self->tablename("revision_ix2");2930$self->{dbh}->do("2931 CREATE TABLE$tablename(2932 name TEXT NOT NULL,2933 revision INTEGER NOT NULL,2934 filehash TEXT NOT NULL,2935 commithash TEXT NOT NULL,2936 author TEXT NOT NULL,2937 modified TEXT NOT NULL,2938 mode TEXT NOT NULL2939 )2940 ");2941$self->{dbh}->do("2942 CREATE INDEX$ix1name2943 ON$tablename(name,revision)2944 ");2945$self->{dbh}->do("2946 CREATE INDEX$ix2name2947 ON$tablename(name,commithash)2948 ");2949}29502951# Construct the head table if required2952# The head table (along with the "last_commit" entry in the property2953# table) is the persisted working state of the "sub update" subroutine.2954# All of it's data is read entirely first, and completely recreated2955# last, every time "sub update" runs.2956# This is also used by "sub getmeta" when it is asked for the latest2957# version of a file (as opposed to some specific version).2958# Another way of thinking about it is as a single slice out of2959# "revisions", giving just the most recent revision information for2960# each file.2961unless($self->{tables}{$self->tablename("head")} )2962{2963my$tablename=$self->tablename("head");2964my$ix1name=$self->tablename("head_ix1");2965$self->{dbh}->do("2966 CREATE TABLE$tablename(2967 name TEXT NOT NULL,2968 revision INTEGER NOT NULL,2969 filehash TEXT NOT NULL,2970 commithash TEXT NOT NULL,2971 author TEXT NOT NULL,2972 modified TEXT NOT NULL,2973 mode TEXT NOT NULL2974 )2975 ");2976$self->{dbh}->do("2977 CREATE INDEX$ix1name2978 ON$tablename(name)2979 ");2980}29812982# Construct the properties table if required2983# - "last_commit" - Used by "sub update".2984unless($self->{tables}{$self->tablename("properties")} )2985{2986my$tablename=$self->tablename("properties");2987$self->{dbh}->do("2988 CREATE TABLE$tablename(2989 key TEXT NOT NULL PRIMARY KEY,2990 value TEXT2991 )2992 ");2993}29942995# Construct the commitmsgs table if required2996# The commitmsgs table is only used for merge commits, since2997# "sub update" will only keep one branch of parents. Shortlogs2998# for ignored commits (i.e. not on the chosen branch) will be used2999# to construct a replacement "collapsed" merge commit message,3000# which will be stored in this table. See also "sub commitmessage".3001unless($self->{tables}{$self->tablename("commitmsgs")} )3002{3003my$tablename=$self->tablename("commitmsgs");3004$self->{dbh}->do("3005 CREATE TABLE$tablename(3006 key TEXT NOT NULL PRIMARY KEY,3007 value TEXT3008 )3009 ");3010}30113012return$self;3013}30143015=head2 tablename30163017=cut3018sub tablename3019{3020my$self=shift;3021my$name=shift;30223023if(exists$self->{valid_tables}{$name}) {3024return$self->{dbtablenameprefix} .$name;3025}else{3026returnundef;3027}3028}30293030=head2 update30313032Bring the database up to date with the latest changes from3033the git repository.30343035Internal working state is read out of the "head" table and the3036"last_commit" property, then it updates "revisions" based on that, and3037finally it writes the new internal state back to the "head" table3038so it can be used as a starting point the next time update is called.30393040=cut3041sub update3042{3043my$self=shift;30443045# first lets get the commit list3046$ENV{GIT_DIR} =$self->{git_path};30473048my$commitsha1=`git rev-parse$self->{module}`;3049chomp$commitsha1;30503051my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3052unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3053{3054die("Invalid module '$self->{module}'");3055}305630573058my$git_log;3059my$lastcommit=$self->_get_prop("last_commit");30603061if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3062return1;3063}30643065# Start exclusive lock here...3066$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";30673068# TODO: log processing is memory bound3069# if we can parse into a 2nd file that is in reverse order3070# we can probably do something really efficient3071my@git_log_params= ('--pretty','--parents','--topo-order');30723073if(defined$lastcommit) {3074push@git_log_params,"$lastcommit..$self->{module}";3075}else{3076push@git_log_params,$self->{module};3077}3078# git-rev-list is the backend / plumbing version of git-log3079open(GITLOG,'-|','git','rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";30803081my@commits;30823083my%commit= ();30843085while( <GITLOG> )3086{3087chomp;3088if(m/^commit\s+(.*)$/) {3089# on ^commit lines put the just seen commit in the stack3090# and prime things for the next one3091if(keys%commit) {3092my%copy=%commit;3093unshift@commits, \%copy;3094%commit= ();3095}3096my@parents=split(m/\s+/,$1);3097$commit{hash} =shift@parents;3098$commit{parents} = \@parents;3099}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3100# on rfc822-like lines seen before we see any message,3101# lowercase the entry and put it in the hash as key-value3102$commit{lc($1)} =$2;3103}else{3104# message lines - skip initial empty line3105# and trim whitespace3106if(!exists($commit{message}) &&m/^\s*$/) {3107# define it to mark the end of headers3108$commit{message} ='';3109next;3110}3111s/^\s+//;s/\s+$//;# trim ws3112$commit{message} .=$_."\n";3113}3114}3115close GITLOG;31163117unshift@commits, \%commitif(keys%commit);31183119# Now all the commits are in the @commits bucket3120# ordered by time DESC. for each commit that needs processing,3121# determine whether it's following the last head we've seen or if3122# it's on its own branch, grab a file list, and add whatever's changed3123# NOTE: $lastcommit refers to the last commit from previous run3124# $lastpicked is the last commit we picked in this run3125my$lastpicked;3126my$head= {};3127if(defined$lastcommit) {3128$lastpicked=$lastcommit;3129}31303131my$committotal=scalar(@commits);3132my$commitcount=0;31333134# Load the head table into $head (for cached lookups during the update process)3135foreachmy$file( @{$self->gethead()} )3136{3137$head->{$file->{name}} =$file;3138}31393140foreachmy$commit(@commits)3141{3142$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3143if(defined$lastpicked)3144{3145if(!in_array($lastpicked, @{$commit->{parents}}))3146{3147# skip, we'll see this delta3148# as part of a merge later3149# warn "skipping off-track $commit->{hash}\n";3150next;3151}elsif(@{$commit->{parents}} >1) {3152# it is a merge commit, for each parent that is3153# not $lastpicked (not given a CVS revision number),3154# see if we can get a log3155# from the merge-base to that parent to put it3156# in the message as a merge summary.3157my@parents= @{$commit->{parents}};3158foreachmy$parent(@parents) {3159if($parenteq$lastpicked) {3160next;3161}3162# git-merge-base can potentially (but rarely) throw3163# several candidate merge bases. let's assume3164# that the first one is the best one.3165my$base=eval{3166 safe_pipe_capture('git','merge-base',3167$lastpicked,$parent);3168};3169# The two branches may not be related at all,3170# in which case merge base simply fails to find3171# any, but that's Ok.3172next if($@);31733174chomp$base;3175if($base) {3176my@merged;3177# print "want to log between $base $parent \n";3178open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3179or die"Cannot call git-log:$!";3180my$mergedhash;3181while(<GITLOG>) {3182chomp;3183if(!defined$mergedhash) {3184if(m/^commit\s+(.+)$/) {3185$mergedhash=$1;3186}else{3187next;3188}3189}else{3190# grab the first line that looks non-rfc8223191# aka has content after leading space3192if(m/^\s+(\S.*)$/) {3193my$title=$1;3194$title=substr($title,0,100);# truncate3195unshift@merged,"$mergedhash$title";3196undef$mergedhash;3197}3198}3199}3200close GITLOG;3201if(@merged) {3202$commit->{mergemsg} =$commit->{message};3203$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3204foreachmy$summary(@merged) {3205$commit->{mergemsg} .="\t$summary\n";3206}3207$commit->{mergemsg} .="\n\n";3208# print "Message for $commit->{hash} \n$commit->{mergemsg}";3209}3210}3211}3212}3213}32143215# convert the date to CVS-happy format3216$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);32173218if(defined($lastpicked) )3219{3220my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3221local($/) ="\0";3222while( <FILELIST> )3223{3224chomp;3225unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)3226{3227die("Couldn't process git-diff-tree line :$_");3228}3229my($mode,$hash,$change) = ($1,$2,$3);3230my$name= <FILELIST>;3231chomp($name);32323233# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");32343235my$git_perms="";3236$git_perms.="r"if($mode&4);3237$git_perms.="w"if($mode&2);3238$git_perms.="x"if($mode&1);3239$git_perms="rw"if($git_permseq"");32403241if($changeeq"D")3242{3243#$log->debug("DELETE $name");3244$head->{$name} = {3245 name =>$name,3246 revision =>$head->{$name}{revision} +1,3247 filehash =>"deleted",3248 commithash =>$commit->{hash},3249 modified =>$commit->{date},3250 author =>$commit->{author},3251 mode =>$git_perms,3252};3253$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3254}3255elsif($changeeq"M"||$changeeq"T")3256{3257#$log->debug("MODIFIED $name");3258$head->{$name} = {3259 name =>$name,3260 revision =>$head->{$name}{revision} +1,3261 filehash =>$hash,3262 commithash =>$commit->{hash},3263 modified =>$commit->{date},3264 author =>$commit->{author},3265 mode =>$git_perms,3266};3267$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3268}3269elsif($changeeq"A")3270{3271#$log->debug("ADDED $name");3272$head->{$name} = {3273 name =>$name,3274 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3275 filehash =>$hash,3276 commithash =>$commit->{hash},3277 modified =>$commit->{date},3278 author =>$commit->{author},3279 mode =>$git_perms,3280};3281$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3282}3283else3284{3285$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3286die;3287}3288}3289close FILELIST;3290}else{3291# this is used to detect files removed from the repo3292my$seen_files= {};32933294my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3295local$/="\0";3296while( <FILELIST> )3297{3298chomp;3299unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3300{3301die("Couldn't process git-ls-tree line :$_");3302}33033304my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);33053306$seen_files->{$git_filename} =1;33073308my($oldhash,$oldrevision,$oldmode) = (3309$head->{$git_filename}{filehash},3310$head->{$git_filename}{revision},3311$head->{$git_filename}{mode}3312);33133314if($git_perms=~/^\d\d\d(\d)\d\d/o)3315{3316$git_perms="";3317$git_perms.="r"if($1&4);3318$git_perms.="w"if($1&2);3319$git_perms.="x"if($1&1);3320}else{3321$git_perms="rw";3322}33233324# unless the file exists with the same hash, we need to update it ...3325unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)3326{3327my$newrevision= ($oldrevisionor0) +1;33283329$head->{$git_filename} = {3330 name =>$git_filename,3331 revision =>$newrevision,3332 filehash =>$git_hash,3333 commithash =>$commit->{hash},3334 modified =>$commit->{date},3335 author =>$commit->{author},3336 mode =>$git_perms,3337};333833393340$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3341}3342}3343close FILELIST;33443345# Detect deleted files3346foreachmy$file(keys%$head)3347{3348unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3349{3350$head->{$file}{revision}++;3351$head->{$file}{filehash} ="deleted";3352$head->{$file}{commithash} =$commit->{hash};3353$head->{$file}{modified} =$commit->{date};3354$head->{$file}{author} =$commit->{author};33553356$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});3357}3358}3359# END : "Detect deleted files"3360}336133623363if(exists$commit->{mergemsg})3364{3365$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3366}33673368$lastpicked=$commit->{hash};33693370$self->_set_prop("last_commit",$commit->{hash});3371}33723373$self->delete_head();3374foreachmy$file(keys%$head)3375{3376$self->insert_head(3377$file,3378$head->{$file}{revision},3379$head->{$file}{filehash},3380$head->{$file}{commithash},3381$head->{$file}{modified},3382$head->{$file}{author},3383$head->{$file}{mode},3384);3385}3386# invalidate the gethead cache3387$self->{gethead_cache} =undef;338833893390# Ending exclusive lock here3391$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3392}33933394sub insert_rev3395{3396my$self=shift;3397my$name=shift;3398my$revision=shift;3399my$filehash=shift;3400my$commithash=shift;3401my$modified=shift;3402my$author=shift;3403my$mode=shift;3404my$tablename=$self->tablename("revision");34053406my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3407$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3408}34093410sub insert_mergelog3411{3412my$self=shift;3413my$key=shift;3414my$value=shift;3415my$tablename=$self->tablename("commitmsgs");34163417my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3418$insert_mergelog->execute($key,$value);3419}34203421sub delete_head3422{3423my$self=shift;3424my$tablename=$self->tablename("head");34253426my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3427$delete_head->execute();3428}34293430sub insert_head3431{3432my$self=shift;3433my$name=shift;3434my$revision=shift;3435my$filehash=shift;3436my$commithash=shift;3437my$modified=shift;3438my$author=shift;3439my$mode=shift;3440my$tablename=$self->tablename("head");34413442my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3443$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3444}34453446sub _get_prop3447{3448my$self=shift;3449my$key=shift;3450my$tablename=$self->tablename("properties");34513452my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3453$db_query->execute($key);3454my($value) =$db_query->fetchrow_array;34553456return$value;3457}34583459sub _set_prop3460{3461my$self=shift;3462my$key=shift;3463my$value=shift;3464my$tablename=$self->tablename("properties");34653466my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3467$db_query->execute($value,$key);34683469unless($db_query->rows)3470{3471$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3472$db_query->execute($key,$value);3473}34743475return$value;3476}34773478=head2 gethead34793480=cut34813482sub gethead3483{3484my$self=shift;3485my$tablename=$self->tablename("head");34863487return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );34883489my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3490$db_query->execute();34913492my$tree= [];3493while(my$file=$db_query->fetchrow_hashref)3494{3495push@$tree,$file;3496}34973498$self->{gethead_cache} =$tree;34993500return$tree;3501}35023503=head2 getlog35043505See also gethistorydense().35063507=cut35083509sub getlog3510{3511my$self=shift;3512my$filename=shift;3513my$tablename=$self->tablename("revision");35143515my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3516$db_query->execute($filename);35173518my$tree= [];3519while(my$file=$db_query->fetchrow_hashref)3520{3521push@$tree,$file;3522}35233524return$tree;3525}35263527=head2 getmeta35283529This function takes a filename (with path) argument and returns a hashref of3530metadata for that file.35313532=cut35333534sub getmeta3535{3536my$self=shift;3537my$filename=shift;3538my$revision=shift;3539my$tablename_rev=$self->tablename("revision");3540my$tablename_head=$self->tablename("head");35413542my$db_query;3543if(defined($revision)and$revision=~/^\d+$/)3544{3545$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND revision=?",{},1);3546$db_query->execute($filename,$revision);3547}3548elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)3549{3550$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND commithash=?",{},1);3551$db_query->execute($filename,$revision);3552}else{3553$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_headWHERE name=?",{},1);3554$db_query->execute($filename);3555}35563557return$db_query->fetchrow_hashref;3558}35593560=head2 commitmessage35613562this function takes a commithash and returns the commit message for that commit35633564=cut3565sub commitmessage3566{3567my$self=shift;3568my$commithash=shift;3569my$tablename=$self->tablename("commitmsgs");35703571die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);35723573my$db_query;3574$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3575$db_query->execute($commithash);35763577my($message) =$db_query->fetchrow_array;35783579if(defined($message) )3580{3581$message.=" "if($message=~/\n$/);3582return$message;3583}35843585my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);3586shift@lineswhile($lines[0] =~/\S/);3587$message=join("",@lines);3588$message.=" "if($message=~/\n$/);3589return$message;3590}35913592=head2 gethistorydense35933594This function takes a filename (with path) argument and returns an arrayofarrays3595containing revision,filehash,commithash ordered by revision descending.35963597This version of gethistory skips deleted entries -- so it is useful for annotate.3598The 'dense' part is a reference to a '--dense' option available for git-rev-list3599and other git tools that depend on it.36003601See also getlog().36023603=cut3604sub gethistorydense3605{3606my$self=shift;3607my$filename=shift;3608my$tablename=$self->tablename("revision");36093610my$db_query;3611$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);3612$db_query->execute($filename);36133614return$db_query->fetchall_arrayref;3615}36163617=head2 in_array()36183619from Array::PAT - mimics the in_array() function3620found in PHP. Yuck but works for small arrays.36213622=cut3623sub in_array3624{3625my($check,@array) =@_;3626my$retval=0;3627foreachmy$test(@array){3628if($checkeq$test){3629$retval=1;3630}3631}3632return$retval;3633}36343635=head2 safe_pipe_capture36363637an alternative to `command` that allows input to be passed as an array3638to work around shell problems with weird characters in arguments36393640=cut3641sub safe_pipe_capture {36423643my@output;36443645if(my$pid=open my$child,'-|') {3646@output= (<$child>);3647close$childor die join(' ',@_).":$!$?";3648}else{3649exec(@_)or die"$!$?";# exec() can fail the executable can't be found3650}3651returnwantarray?@output:join('',@output);3652}36533654=head2 mangle_dirname36553656create a string from a directory name that is suitable to use as3657part of a filename, mainly by converting all chars except \w.- to _36583659=cut3660sub mangle_dirname {3661my$dirname=shift;3662return unlessdefined$dirname;36633664$dirname=~s/[^\w.-]/_/g;36653666return$dirname;3667}36683669=head2 mangle_tablename36703671create a string from a that is suitable to use as part of an SQL table3672name, mainly by converting all chars except \w to _36733674=cut3675sub mangle_tablename {3676my$tablename=shift;3677return unlessdefined$tablename;36783679$tablename=~s/[^\w_]/_/g;36803681return$tablename;3682}368336841;