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;1563$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1564$updater->update();15651566# if no files were specified, we need to work out what files we should1567# be providing status on ...1568 argsfromdir($updater);15691570# foreach file specified on the command line ...1571foreachmy$filename( @{$state->{args}} )1572{1573$filename= filecleanup($filename);15741575if(exists($state->{opt}{l}) &&1576index($filename,'/',length($state->{prependdir})) >=0)1577{1578next;1579}15801581my$meta=$updater->getmeta($filename);1582my$oldmeta=$meta;15831584my$wrev= revparse($filename);15851586# If the working copy is an old revision, lets get that1587# version too for comparison.1588if(defined($wrev)and$wrev!=$meta->{revision} )1589{1590$oldmeta=$updater->getmeta($filename,$wrev);1591}15921593# TODO : All possible statuses aren't yet implemented1594my$status;1595# Files are up to date if the working copy and repo copy have1596# the same revision, and the working copy is unmodified1597if(defined($wrev)and defined($meta->{revision})and1598$wrev==$meta->{revision}and1599( ($state->{entries}{$filename}{unchanged}and1600(not defined($state->{entries}{$filename}{conflict} )or1601$state->{entries}{$filename}{conflict} !~/^\+=/) )or1602(defined($state->{entries}{$filename}{modified_hash})and1603$state->{entries}{$filename}{modified_hash}eq1604$meta->{filehash} ) ) )1605{1606$status="Up-to-date";1607}16081609# Need checkout if the working copy has an older revision than1610# the repo copy, and the working copy is unmodified1611if(defined($wrev)and defined($meta->{revision} )and1612$meta->{revision} >$wrevand1613($state->{entries}{$filename}{unchanged}or1614(defined($state->{entries}{$filename}{modified_hash})and1615$state->{entries}{$filename}{modified_hash}eq1616$oldmeta->{filehash} ) ) )1617{1618$status||="Needs Checkout";1619}16201621# Need checkout if it exists in the repo but doesn't have a working1622# copy1623if(not defined($wrev)and defined($meta->{revision} ) )1624{1625$status||="Needs Checkout";1626}16271628# Locally modified if working copy and repo copy have the1629# same revision but there are local changes1630if(defined($wrev)and defined($meta->{revision})and1631$wrev==$meta->{revision}and1632$state->{entries}{$filename}{modified_filename} )1633{1634$status||="Locally Modified";1635}16361637# Needs Merge if working copy revision is less than repo copy1638# and there are local changes1639if(defined($wrev)and defined($meta->{revision} )and1640$meta->{revision} >$wrevand1641$state->{entries}{$filename}{modified_filename} )1642{1643$status||="Needs Merge";1644}16451646if(defined($state->{entries}{$filename}{revision} )and1647not defined($meta->{revision} ) )1648{1649$status||="Locally Added";1650}1651if(defined($wrev)and defined($meta->{revision} )and1652-$wrev==$meta->{revision} )1653{1654$status||="Locally Removed";1655}1656if(defined($state->{entries}{$filename}{conflict} )and1657$state->{entries}{$filename}{conflict} =~/^\+=/)1658{1659$status||="Unresolved Conflict";1660}1661if(0)1662{1663$status||="File had conflicts on merge";1664}16651666$status||="Unknown";16671668my($filepart) = filenamesplit($filename);16691670print"M =======". ("=" x 60) ."\n";1671print"M File:$filepart\tStatus:$status\n";1672if(defined($state->{entries}{$filename}{revision}) )1673{1674print"M Working revision:\t".1675$state->{entries}{$filename}{revision} ."\n";1676}else{1677print"M Working revision:\tNo entry for$filename\n";1678}1679if(defined($meta->{revision}) )1680{1681print"M Repository revision:\t1.".1682$meta->{revision} .1683"\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1684print"M Sticky Tag:\t\t(none)\n";1685print"M Sticky Date:\t\t(none)\n";1686print"M Sticky Options:\t\t(none)\n";1687}else{1688print"M Repository revision:\tNo revision control file\n";1689}1690print"M\n";1691}16921693print"ok\n";1694}16951696sub req_diff1697{1698my($cmd,$data) =@_;16991700 argsplit("diff");17011702$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1703#$log->debug("status state : " . Dumper($state));17041705my($revision1,$revision2);1706if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1707{1708$revision1=$state->{opt}{r}[0];1709$revision2=$state->{opt}{r}[1];1710}else{1711$revision1=$state->{opt}{r};1712}17131714$revision1=~s/^1\.//if(defined($revision1) );1715$revision2=~s/^1\.//if(defined($revision2) );17161717$log->debug("Diffing revisions ".1718(defined($revision1) ?$revision1:"[NULL]") .1719" and ". (defined($revision2) ?$revision2:"[NULL]") );17201721# Grab a handle to the SQLite db and do any necessary updates1722my$updater;1723$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1724$updater->update();17251726# if no files were specified, we need to work out what files we should1727# be providing status on ...1728 argsfromdir($updater);17291730# foreach file specified on the command line ...1731foreachmy$filename( @{$state->{args}} )1732{1733$filename= filecleanup($filename);17341735my($fh,$file1,$file2,$meta1,$meta2,$filediff);17361737my$wrev= revparse($filename);17381739# We need _something_ to diff against1740next unless(defined($wrev) );17411742# if we have a -r switch, use it1743if(defined($revision1) )1744{1745(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1746$meta1=$updater->getmeta($filename,$revision1);1747unless(defined($meta1)and$meta1->{filehash}ne"deleted")1748{1749print"E File$filenameat revision 1.$revision1doesn't exist\n";1750next;1751}1752 transmitfile($meta1->{filehash}, { targetfile =>$file1});1753}1754# otherwise we just use the working copy revision1755else1756{1757(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1758$meta1=$updater->getmeta($filename,$wrev);1759 transmitfile($meta1->{filehash}, { targetfile =>$file1});1760}17611762# if we have a second -r switch, use it too1763if(defined($revision2) )1764{1765(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1766$meta2=$updater->getmeta($filename,$revision2);17671768unless(defined($meta2)and$meta2->{filehash}ne"deleted")1769{1770print"E File$filenameat revision 1.$revision2doesn't exist\n";1771next;1772}17731774 transmitfile($meta2->{filehash}, { targetfile =>$file2});1775}1776# otherwise we just use the working copy1777else1778{1779$file2=$state->{entries}{$filename}{modified_filename};1780}17811782# if we have been given -r, and we don't have a $file2 yet, lets1783# get one1784if(defined($revision1)and not defined($file2) )1785{1786(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1787$meta2=$updater->getmeta($filename,$wrev);1788 transmitfile($meta2->{filehash}, { targetfile =>$file2});1789}17901791# We need to have retrieved something useful1792next unless(defined($meta1) );17931794# Files to date if the working copy and repo copy have the same1795# revision, and the working copy is unmodified1796if(not defined($meta2)and$wrev==$meta1->{revision}and1797( ($state->{entries}{$filename}{unchanged}and1798(not defined($state->{entries}{$filename}{conflict} )or1799$state->{entries}{$filename}{conflict} !~/^\+=/) )or1800(defined($state->{entries}{$filename}{modified_hash})and1801$state->{entries}{$filename}{modified_hash}eq1802$meta1->{filehash} ) ) )1803{1804next;1805}18061807# Apparently we only show diffs for locally modified files1808unless(defined($meta2)or1809defined($state->{entries}{$filename}{modified_filename} ) )1810{1811next;1812}18131814print"M Index:$filename\n";1815print"M =======". ("=" x 60) ."\n";1816print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1817if(defined($meta1) )1818{1819print"M retrieving revision 1.$meta1->{revision}\n"1820}1821if(defined($meta2) )1822{1823print"M retrieving revision 1.$meta2->{revision}\n"1824}1825print"M diff ";1826foreachmy$opt(keys%{$state->{opt}} )1827{1828if(ref$state->{opt}{$opt}eq"ARRAY")1829{1830foreachmy$value( @{$state->{opt}{$opt}} )1831{1832print"-$opt$value";1833}1834}else{1835print"-$opt";1836if(defined($state->{opt}{$opt} ) )1837{1838print"$state->{opt}{$opt} "1839}1840}1841}1842print"$filename\n";18431844$log->info("Diffing$filename-r$meta1->{revision} -r ".1845($meta2->{revision}or"workingcopy"));18461847($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);18481849if(exists$state->{opt}{u} )1850{1851system("diff -u -L '$filenamerevision 1.$meta1->{revision}'".1852" -L '$filename".1853(defined($meta2->{revision}) ?1854"revision 1.$meta2->{revision}":1855"working copy") .1856"'$file1$file2>$filediff");1857}else{1858system("diff$file1$file2>$filediff");1859}18601861while( <$fh> )1862{1863print"M$_";1864}1865close$fh;1866}18671868print"ok\n";1869}18701871sub req_log1872{1873my($cmd,$data) =@_;18741875 argsplit("log");18761877$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1878#$log->debug("log state : " . Dumper($state));18791880my($minrev,$maxrev);1881if(defined($state->{opt}{r} )and1882$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1883{1884my$control=$2;1885$minrev=$1;1886$maxrev=$3;1887$minrev=~s/^1\.//if(defined($minrev) );1888$maxrev=~s/^1\.//if(defined($maxrev) );1889$minrev++if(defined($minrev)and$controleq"::");1890}18911892# Grab a handle to the SQLite db and do any necessary updates1893my$updater;1894$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1895$updater->update();18961897# if no files were specified, we need to work out what files we1898# should be providing status on ...1899 argsfromdir($updater);19001901# foreach file specified on the command line ...1902foreachmy$filename( @{$state->{args}} )1903{1904$filename= filecleanup($filename);19051906my$headmeta=$updater->getmeta($filename);19071908my$revisions=$updater->getlog($filename);1909my$totalrevisions=scalar(@$revisions);19101911if(defined($minrev) )1912{1913$log->debug("Removing revisions less than$minrev");1914while(scalar(@$revisions) >0and1915$revisions->[-1]{revision} <$minrev)1916{1917pop@$revisions;1918}1919}1920if(defined($maxrev) )1921{1922$log->debug("Removing revisions greater than$maxrev");1923while(scalar(@$revisions) >0and1924$revisions->[0]{revision} >$maxrev)1925{1926shift@$revisions;1927}1928}19291930next unless(scalar(@$revisions) );19311932print"M\n";1933print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1934print"M Working file:$filename\n";1935print"M head: 1.$headmeta->{revision}\n";1936print"M branch:\n";1937print"M locks: strict\n";1938print"M access list:\n";1939print"M symbolic names:\n";1940print"M keyword substitution: kv\n";1941print"M total revisions:$totalrevisions;\tselected revisions: ".1942scalar(@$revisions) ."\n";1943print"M description:\n";19441945foreachmy$revision(@$revisions)1946{1947print"M ----------------------------\n";1948print"M revision 1.$revision->{revision}\n";1949# reformat the date for log output1950if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and1951defined($DATE_LIST->{$2}) )1952{1953$revision->{modified} =sprintf('%04d/%02d/%02d%s',1954$3,$DATE_LIST->{$2},$1,$4);1955}1956$revision->{author} = cvs_author($revision->{author});1957print"M date:$revision->{modified};".1958" author:$revision->{author}; state: ".1959($revision->{filehash}eq"deleted"?"dead":"Exp") .1960"; lines: +2 -3\n";1961my$commitmessage;1962$commitmessage=$updater->commitmessage($revision->{commithash});1963$commitmessage=~s/^/M /mg;1964print$commitmessage."\n";1965}1966print"M =======". ("=" x 70) ."\n";1967}19681969print"ok\n";1970}19711972sub req_annotate1973{1974my($cmd,$data) =@_;19751976 argsplit("annotate");19771978$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1979#$log->debug("status state : " . Dumper($state));19801981# Grab a handle to the SQLite db and do any necessary updates1982my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1983$updater->update();19841985# if no files were specified, we need to work out what files we should be providing annotate on ...1986 argsfromdir($updater);19871988# we'll need a temporary checkout dir1989 setupWorkTree();19901991$log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");19921993# foreach file specified on the command line ...1994foreachmy$filename( @{$state->{args}} )1995{1996$filename= filecleanup($filename);19971998my$meta=$updater->getmeta($filename);19992000next unless($meta->{revision} );20012002# get all the commits that this file was in2003# in dense format -- aka skip dead revisions2004my$revisions=$updater->gethistorydense($filename);2005my$lastseenin=$revisions->[0][2];20062007# populate the temporary index based on the latest commit were we saw2008# the file -- but do it cheaply without checking out any files2009# TODO: if we got a revision from the client, use that instead2010# to look up the commithash in sqlite (still good to default to2011# the current head as we do now)2012system("git","read-tree",$lastseenin);2013unless($?==0)2014{2015print"E error running git-read-tree$lastseenin$ENV{GIT_INDEX_FILE}$!\n";2016return;2017}2018$log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit$lastseenin- exit status$?");20192020# do a checkout of the file2021system('git','checkout-index','-f','-u',$filename);2022unless($?==0) {2023print"E error running git-checkout-index -f -u$filename:$!\n";2024return;2025}20262027$log->info("Annotate$filename");20282029# Prepare a file with the commits from the linearized2030# history that annotate should know about. This prevents2031# git-jsannotate telling us about commits we are hiding2032# from the client.20332034my$a_hints="$work->{workDir}/.annotate_hints";2035if(!open(ANNOTATEHINTS,'>',$a_hints)) {2036print"E failed to open '$a_hints' for writing:$!\n";2037return;2038}2039for(my$i=0;$i<@$revisions;$i++)2040{2041print ANNOTATEHINTS $revisions->[$i][2];2042if($i+1<@$revisions) {# have we got a parent?2043print ANNOTATEHINTS ' '.$revisions->[$i+1][2];2044}2045print ANNOTATEHINTS "\n";2046}20472048print ANNOTATEHINTS "\n";2049close ANNOTATEHINTS2050or(print"E failed to write$a_hints:$!\n"),return;20512052my@cmd= (qw(git annotate -l -S),$a_hints,$filename);2053if(!open(ANNOTATE,"-|",@cmd)) {2054print"E error invoking ".join(' ',@cmd) .":$!\n";2055return;2056}2057my$metadata= {};2058print"E Annotations for$filename\n";2059print"E ***************\n";2060while( <ANNOTATE> )2061{2062if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)2063{2064my$commithash=$1;2065my$data=$2;2066unless(defined($metadata->{$commithash} ) )2067{2068$metadata->{$commithash} =$updater->getmeta($filename,$commithash);2069$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});2070$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);2071}2072printf("M 1.%-5d (%-8s%10s):%s\n",2073$metadata->{$commithash}{revision},2074$metadata->{$commithash}{author},2075$metadata->{$commithash}{modified},2076$data2077);2078}else{2079$log->warn("Error in annotate output! LINE:$_");2080print"E Annotate error\n";2081next;2082}2083}2084close ANNOTATE;2085}20862087# done; get out of the tempdir2088 cleanupWorkTree();20892090print"ok\n";20912092}20932094# This method takes the state->{arguments} array and produces two new arrays.2095# The first is $state->{args} which is everything before the '--' argument, and2096# the second is $state->{files} which is everything after it.2097sub argsplit2098{2099$state->{args} = [];2100$state->{files} = [];2101$state->{opt} = {};21022103return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");21042105my$type=shift;21062107if(defined($type) )2108{2109my$opt= {};2110$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");2111$opt= { v =>0, l =>0, R =>0}if($typeeq"status");2112$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");2113$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");2114$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");2115$opt= { k =>1, m =>1}if($typeeq"add");2116$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");2117$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");211821192120while(scalar( @{$state->{arguments}} ) >0)2121{2122my$arg=shift@{$state->{arguments}};21232124next if($argeq"--");2125next unless($arg=~/\S/);21262127# if the argument looks like a switch2128if($arg=~/^-(\w)(.*)/)2129{2130# if it's a switch that takes an argument2131if($opt->{$1} )2132{2133# If this switch has already been provided2134if($opt->{$1} >1and exists($state->{opt}{$1} ) )2135{2136$state->{opt}{$1} = [$state->{opt}{$1} ];2137if(length($2) >0)2138{2139push@{$state->{opt}{$1}},$2;2140}else{2141push@{$state->{opt}{$1}},shift@{$state->{arguments}};2142}2143}else{2144# if there's extra data in the arg, use that as the argument for the switch2145if(length($2) >0)2146{2147$state->{opt}{$1} =$2;2148}else{2149$state->{opt}{$1} =shift@{$state->{arguments}};2150}2151}2152}else{2153$state->{opt}{$1} =undef;2154}2155}2156else2157{2158push@{$state->{args}},$arg;2159}2160}2161}2162else2163{2164my$mode=0;21652166foreachmy$value( @{$state->{arguments}} )2167{2168if($valueeq"--")2169{2170$mode++;2171next;2172}2173push@{$state->{args}},$valueif($mode==0);2174push@{$state->{files}},$valueif($mode==1);2175}2176}2177}21782179# This method uses $state->{directory} to populate $state->{args} with a list of filenames2180sub argsfromdir2181{2182my$updater=shift;21832184$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");21852186return if(scalar( @{$state->{args}} ) >1);21872188my@gethead= @{$updater->gethead};21892190# push added files2191foreachmy$file(keys%{$state->{entries}}) {2192if(exists$state->{entries}{$file}{revision} &&2193$state->{entries}{$file}{revision} ==0)2194{2195push@gethead, { name =>$file, filehash =>'added'};2196}2197}21982199if(scalar(@{$state->{args}}) ==1)2200{2201my$arg=$state->{args}[0];2202$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );22032204$log->info("Only one arg specified, checking for directory expansion on '$arg'");22052206foreachmy$file(@gethead)2207{2208next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2209next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);2210push@{$state->{args}},$file->{name};2211}22122213shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);2214}else{2215$log->info("Only one arg specified, populating file list automatically");22162217$state->{args} = [];22182219foreachmy$file(@gethead)2220{2221next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2222next unless($file->{name} =~s/^$state->{prependdir}//);2223push@{$state->{args}},$file->{name};2224}2225}2226}22272228# This method cleans up the $state variable after a command that uses arguments has run2229sub statecleanup2230{2231$state->{files} = [];2232$state->{args} = [];2233$state->{arguments} = [];2234$state->{entries} = {};2235}22362237# Return working directory revision int "X" from CVS revision "1.X" out2238# of the the working directory "entries" state, for the given filename.2239# Return negative "X" to represent the file is scheduled for removal2240# when it is committed.2241sub revparse2242{2243my$filename=shift;22442245returnundefunless(defined($state->{entries}{$filename}{revision} ) );22462247return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);2248return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);22492250returnundef;2251}22522253# This method takes a file hash and does a CVS "file transfer". Its2254# exact behaviour depends on a second, optional hash table argument:2255# - If $options->{targetfile}, dump the contents to that file;2256# - If $options->{print}, use M/MT to transmit the contents one line2257# at a time;2258# - Otherwise, transmit the size of the file, followed by the file2259# contents.2260sub transmitfile2261{2262my$filehash=shift;2263my$options=shift;22642265if(defined($filehash)and$filehasheq"deleted")2266{2267$log->warn("filehash is 'deleted'");2268return;2269}22702271die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);22722273my$type=`git cat-file -t$filehash`;2274 chomp$type;22752276 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );22772278 my$size= `git cat-file -s $filehash`;2279chomp$size;22802281$log->debug("transmitfile($filehash) size=$size, type=$type");22822283if(open my$fh,'-|',"git","cat-file","blob",$filehash)2284{2285if(defined($options->{targetfile} ) )2286{2287my$targetfile=$options->{targetfile};2288open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2289print NEWFILE $_while( <$fh> );2290close NEWFILE or die("Failed to write '$targetfile':$!");2291}elsif(defined($options->{print} ) &&$options->{print} ) {2292while( <$fh> ) {2293if(/\n\z/) {2294print'M ',$_;2295}else{2296print'MT text ',$_,"\n";2297}2298}2299}else{2300print"$size\n";2301printwhile( <$fh> );2302}2303close$fhor die("Couldn't close filehandle for transmitfile():$!");2304}else{2305die("Couldn't execute git-cat-file");2306}2307}23082309# This method takes a file name, and returns ( $dirpart, $filepart ) which2310# refers to the directory portion and the file portion of the filename2311# respectively2312sub filenamesplit2313{2314my$filename=shift;2315my$fixforlocaldir=shift;23162317my($filepart,$dirpart) = ($filename,".");2318($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2319$dirpart.="/";23202321if($fixforlocaldir)2322{2323$dirpart=~s/^$state->{prependdir}//;2324}23252326return($filepart,$dirpart);2327}23282329sub filecleanup2330{2331my$filename=shift;23322333returnundefunless(defined($filename));2334if($filename=~/^\// )2335{2336print"E absolute filenames '$filename' not supported by server\n";2337returnundef;2338}23392340$filename=~s/^\.\///g;2341$filename=$state->{prependdir} .$filename;2342return$filename;2343}23442345sub validateGitDir2346{2347if( !defined($state->{CVSROOT}) )2348{2349print"error 1 CVSROOT not specified\n";2350 cleanupWorkTree();2351exit;2352}2353if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2354{2355print"error 1 Internally inconsistent CVSROOT\n";2356 cleanupWorkTree();2357exit;2358}2359}23602361# Setup working directory in a work tree with the requested version2362# loaded in the index.2363sub setupWorkTree2364{2365my($ver) =@_;23662367 validateGitDir();23682369if( (defined($work->{state}) &&$work->{state} !=1) ||2370defined($work->{tmpDir}) )2371{2372$log->warn("Bad work tree state management");2373print"error 1 Internal setup multiple work trees without cleanup\n";2374 cleanupWorkTree();2375exit;2376}23772378$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);23792380if( !defined($work->{index}) )2381{2382(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2383}23842385chdir$work->{workDir}or2386die"Unable to chdir to$work->{workDir}\n";23872388$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");23892390$ENV{GIT_WORK_TREE} =".";2391$ENV{GIT_INDEX_FILE} =$work->{index};2392$work->{state} =2;23932394if($ver)2395{2396system("git","read-tree",$ver);2397unless($?==0)2398{2399$log->warn("Error running git-read-tree");2400die"Error running git-read-tree$verin$work->{workDir}$!\n";2401}2402}2403# else # req_annotate reads tree for each file2404}24052406# Ensure current directory is in some kind of working directory,2407# with a recent version loaded in the index.2408sub ensureWorkTree2409{2410if(defined($work->{tmpDir}) )2411{2412$log->warn("Bad work tree state management [ensureWorkTree()]");2413print"error 1 Internal setup multiple dirs without cleanup\n";2414 cleanupWorkTree();2415exit;2416}2417if($work->{state} )2418{2419return;2420}24212422 validateGitDir();24232424if( !defined($work->{emptyDir}) )2425{2426$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2427}2428chdir$work->{emptyDir}or2429die"Unable to chdir to$work->{emptyDir}\n";24302431my$ver=`git show-ref -s refs/heads/$state->{module}`;2432chomp$ver;2433if($ver!~/^[0-9a-f]{40}$/)2434{2435$log->warn("Error from git show-ref -s refs/head$state->{module}");2436print"error 1 cannot find the current HEAD of module";2437 cleanupWorkTree();2438exit;2439}24402441if( !defined($work->{index}) )2442{2443(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2444}24452446$ENV{GIT_WORK_TREE} =".";2447$ENV{GIT_INDEX_FILE} =$work->{index};2448$work->{state} =1;24492450system("git","read-tree",$ver);2451unless($?==0)2452{2453die"Error running git-read-tree$ver$!\n";2454}2455}24562457# Cleanup working directory that is not needed any longer.2458sub cleanupWorkTree2459{2460if( !$work->{state} )2461{2462return;2463}24642465chdir"/"or die"Unable to chdir '/'\n";24662467if(defined($work->{workDir}) )2468{2469 rmtree($work->{workDir} );2470undef$work->{workDir};2471}2472undef$work->{state};2473}24742475# Setup a temporary directory (not a working tree), typically for2476# merging dirty state as in req_update.2477sub setupTmpDir2478{2479$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2480chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";24812482return$work->{tmpDir};2483}24842485# Clean up a previously setupTmpDir. Restore previous work tree if2486# appropriate.2487sub cleanupTmpDir2488{2489if( !defined($work->{tmpDir}) )2490{2491$log->warn("cleanup tmpdir that has not been setup");2492die"Cleanup tmpDir that has not been setup\n";2493}2494if(defined($work->{state}) )2495{2496if($work->{state} ==1)2497{2498chdir$work->{emptyDir}or2499die"Unable to chdir to$work->{emptyDir}\n";2500}2501elsif($work->{state} ==2)2502{2503chdir$work->{workDir}or2504die"Unable to chdir to$work->{emptyDir}\n";2505}2506else2507{2508$log->warn("Inconsistent work dir state");2509die"Inconsistent work dir state\n";2510}2511}2512else2513{2514chdir"/"or die"Unable to chdir '/'\n";2515}2516}25172518# Given a path, this function returns a string containing the kopts2519# that should go into that path's Entries line. For example, a binary2520# file should get -kb.2521sub kopts_from_path2522{2523my($path,$srcType,$name) =@_;25242525if(defined($cfg->{gitcvs}{usecrlfattr} )and2526$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2527{2528my($val) = check_attr("text",$path);2529if($valeq"unspecified")2530{2531$val= check_attr("crlf",$path);2532}2533if($valeq"unset")2534{2535return"-kb"2536}2537elsif( check_attr("eol",$path)ne"unspecified"||2538$valeq"set"||$valeq"input")2539{2540return"";2541}2542else2543{2544$log->info("Unrecognized check_attr crlf$path:$val");2545}2546}25472548if(defined($cfg->{gitcvs}{allbinary} ) )2549{2550if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2551{2552return"-kb";2553}2554elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2555{2556if( is_binary($srcType,$name) )2557{2558$log->debug("... as binary");2559return"-kb";2560}2561else2562{2563$log->debug("... as text");2564}2565}2566}2567# Return "" to give no special treatment to any path2568return"";2569}25702571sub check_attr2572{2573my($attr,$path) =@_;2574 ensureWorkTree();2575if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2576{2577my$val= <$fh>;2578close$fh;2579$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2580return$val;2581}2582else2583{2584returnundef;2585}2586}25872588# This should have the same heuristics as convert.c:is_binary() and related.2589# Note that the bare CR test is done by callers in convert.c.2590sub is_binary2591{2592my($srcType,$name) =@_;2593$log->debug("is_binary($srcType,$name)");25942595# Minimize amount of interpreted code run in the inner per-character2596# loop for large files, by totalling each character value and2597# then analyzing the totals.2598my@counts;2599my$i;2600for($i=0;$i<256;$i++)2601{2602$counts[$i]=0;2603}26042605my$fh= open_blob_or_die($srcType,$name);2606my$line;2607while(defined($line=<$fh>) )2608{2609# Any '\0' and bare CR are considered binary.2610if($line=~/\0|(\r[^\n])/)2611{2612close($fh);2613return1;2614}26152616# Count up each character in the line:2617my$len=length($line);2618for($i=0;$i<$len;$i++)2619{2620$counts[ord(substr($line,$i,1))]++;2621}2622}2623close$fh;26242625# Don't count CR and LF as either printable/nonprintable2626$counts[ord("\n")]=0;2627$counts[ord("\r")]=0;26282629# Categorize individual character count into printable and nonprintable:2630my$printable=0;2631my$nonprintable=0;2632for($i=0;$i<256;$i++)2633{2634if($i<32&&2635$i!=ord("\b") &&2636$i!=ord("\t") &&2637$i!=033&&# ESC2638$i!=014)# FF2639{2640$nonprintable+=$counts[$i];2641}2642elsif($i==127)# DEL2643{2644$nonprintable+=$counts[$i];2645}2646else2647{2648$printable+=$counts[$i];2649}2650}26512652return($printable>>7) <$nonprintable;2653}26542655# Returns open file handle. Possible invocations:2656# - open_blob_or_die("file",$filename);2657# - open_blob_or_die("sha1",$filehash);2658sub open_blob_or_die2659{2660my($srcType,$name) =@_;2661my($fh);2662if($srcTypeeq"file")2663{2664if( !open$fh,"<",$name)2665{2666$log->warn("Unable to open file$name:$!");2667die"Unable to open file$name:$!\n";2668}2669}2670elsif($srcTypeeq"sha1")2671{2672unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2673{2674$log->warn("Need filehash");2675die"Need filehash\n";2676}26772678my$type=`git cat-file -t$name`;2679 chomp$type;26802681 unless ( defined ($type) and$typeeq "blob" )2682 {2683$log->warn("Invalid type '$type' for '$name'");2684 die ( "Invalid type '$type' (expected 'blob')" )2685 }26862687 my$size= `git cat-file -s $name`;2688chomp$size;26892690$log->debug("open_blob_or_die($name) size=$size, type=$type");26912692unless(open$fh,'-|',"git","cat-file","blob",$name)2693{2694$log->warn("Unable to open sha1$name");2695die"Unable to open sha1$name\n";2696}2697}2698else2699{2700$log->warn("Unknown type of blob source:$srcType");2701die"Unknown type of blob source:$srcType\n";2702}2703return$fh;2704}27052706# Generate a CVS author name from Git author information, by taking the local2707# part of the email address and replacing characters not in the Portable2708# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2709# Login names are Unix login names, which should be restricted to this2710# character set.2711sub cvs_author2712{2713my$author_line=shift;2714(my$author) =$author_line=~/<([^@>]*)/;27152716$author=~s/[^-a-zA-Z0-9_.]/_/g;2717$author=~s/^-/_/;27182719$author;2720}272127222723sub descramble2724{2725# This table is from src/scramble.c in the CVS source2726my@SHIFTS= (27270,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,272816,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2729114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2730111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,273141,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2732125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,273336,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,273458,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2735225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2736199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2737174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2738207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2739192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2740227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2741182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2742243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522743);2744my($str) =@_;27452746# This should never happen, the same password format (A) has been2747# used by CVS since the beginning of time2748{2749my$fmt=substr($str,0,1);2750die"invalid password format `$fmt'"unless$fmteq'A';2751}27522753my@str=unpack"C*",substr($str,1);2754my$ret=join'',map{chr$SHIFTS[$_] }@str;2755return$ret;2756}275727582759package GITCVS::log;27602761####2762#### Copyright The Open University UK - 2006.2763####2764#### Authors: Martyn Smith <martyn@catalyst.net.nz>2765#### Martin Langhoff <martin@laptop.org>2766####2767####27682769use strict;2770use warnings;27712772=head1 NAME27732774GITCVS::log27752776=head1 DESCRIPTION27772778This module provides very crude logging with a similar interface to2779Log::Log4perl27802781=head1 METHODS27822783=cut27842785=head2 new27862787Creates a new log object, optionally you can specify a filename here to2788indicate the file to log to. If no log file is specified, you can specify one2789later with method setfile, or indicate you no longer want logging with method2790nofile.27912792Until one of these methods is called, all log calls will buffer messages ready2793to write out.27942795=cut2796sub new2797{2798my$class=shift;2799my$filename=shift;28002801my$self= {};28022803bless$self,$class;28042805if(defined($filename) )2806{2807open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2808}28092810return$self;2811}28122813=head2 setfile28142815This methods takes a filename, and attempts to open that file as the log file.2816If successful, all buffered data is written out to the file, and any further2817logging is written directly to the file.28182819=cut2820sub setfile2821{2822my$self=shift;2823my$filename=shift;28242825if(defined($filename) )2826{2827open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2828}28292830return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28312832while(my$line=shift@{$self->{buffer}} )2833{2834print{$self->{fh}}$line;2835}2836}28372838=head2 nofile28392840This method indicates no logging is going to be used. It flushes any entries in2841the internal buffer, and sets a flag to ensure no further data is put there.28422843=cut2844sub nofile2845{2846my$self=shift;28472848$self->{nolog} =1;28492850return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");28512852$self->{buffer} = [];2853}28542855=head2 _logopen28562857Internal method. Returns true if the log file is open, false otherwise.28582859=cut2860sub _logopen2861{2862my$self=shift;28632864return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2865return0;2866}28672868=head2 debug info warn fatal28692870These four methods are wrappers to _log. They provide the actual interface for2871logging data.28722873=cut2874sub debug {my$self=shift;$self->_log("debug",@_); }2875sub info {my$self=shift;$self->_log("info",@_); }2876subwarn{my$self=shift;$self->_log("warn",@_); }2877sub fatal {my$self=shift;$self->_log("fatal",@_); }28782879=head2 _log28802881This is an internal method called by the logging functions. It generates a2882timestamp and pushes the logged line either to file, or internal buffer.28832884=cut2885sub _log2886{2887my$self=shift;2888my$level=shift;28892890return if($self->{nolog} );28912892my@time=localtime;2893my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2894$time[5] +1900,2895$time[4] +1,2896$time[3],2897$time[2],2898$time[1],2899$time[0],2900uc$level,2901);29022903if($self->_logopen)2904{2905print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2906}else{2907push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2908}2909}29102911=head2 DESTROY29122913This method simply closes the file handle if one is open29142915=cut2916sub DESTROY2917{2918my$self=shift;29192920if($self->_logopen)2921{2922close$self->{fh};2923}2924}29252926package GITCVS::updater;29272928####2929#### Copyright The Open University UK - 2006.2930####2931#### Authors: Martyn Smith <martyn@catalyst.net.nz>2932#### Martin Langhoff <martin@laptop.org>2933####2934####29352936use strict;2937use warnings;2938use DBI;29392940=head1 METHODS29412942=cut29432944=head2 new29452946=cut2947sub new2948{2949my$class=shift;2950my$config=shift;2951my$module=shift;2952my$log=shift;29532954die"Need to specify a git repository"unless(defined($config)and-d $config);2955die"Need to specify a module"unless(defined($module) );29562957$class=ref($class) ||$class;29582959my$self= {};29602961bless$self,$class;29622963$self->{valid_tables} = {'revision'=>1,2964'revision_ix1'=>1,2965'revision_ix2'=>1,2966'head'=>1,2967'head_ix1'=>1,2968'properties'=>1,2969'commitmsgs'=>1};29702971$self->{module} =$module;2972$self->{git_path} =$config."/";29732974$self->{log} =$log;29752976die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );29772978$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2979$cfg->{gitcvs}{dbdriver} ||"SQLite";2980$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2981$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2982$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2983$cfg->{gitcvs}{dbuser} ||"";2984$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2985$cfg->{gitcvs}{dbpass} ||"";2986$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||2987$cfg->{gitcvs}{dbtablenameprefix} ||"";2988my%mapping= ( m =>$module,2989 a =>$state->{method},2990 u =>getlogin||getpwuid($<) || $<,2991 G =>$self->{git_path},2992 g => mangle_dirname($self->{git_path}),2993);2994$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2995$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;2996$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;2997$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});29982999die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;3000die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;3001$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",3002$self->{dbuser},3003$self->{dbpass});3004die"Error connecting to database\n"unlessdefined$self->{dbh};30053006$self->{tables} = {};3007foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )3008{3009$self->{tables}{$table} =1;3010}30113012# Construct the revision table if required3013# The revision table stores an entry for each file, each time that file3014# changes.3015# numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )3016# This is not sufficient to support "-r {commithash}" for any3017# files except files that were modified by that commit (also,3018# some places in the code ignore/effectively strip out -r in3019# some cases, before it gets passed to getmeta()).3020# The "filehash" field typically has a git blob hash, but can also3021# be set to "dead" to indicate that the given version of the file3022# should not exist in the sandbox.3023unless($self->{tables}{$self->tablename("revision")} )3024{3025my$tablename=$self->tablename("revision");3026my$ix1name=$self->tablename("revision_ix1");3027my$ix2name=$self->tablename("revision_ix2");3028$self->{dbh}->do("3029 CREATE TABLE$tablename(3030 name TEXT NOT NULL,3031 revision INTEGER NOT NULL,3032 filehash TEXT NOT NULL,3033 commithash TEXT NOT NULL,3034 author TEXT NOT NULL,3035 modified TEXT NOT NULL,3036 mode TEXT NOT NULL3037 )3038 ");3039$self->{dbh}->do("3040 CREATE INDEX$ix1name3041 ON$tablename(name,revision)3042 ");3043$self->{dbh}->do("3044 CREATE INDEX$ix2name3045 ON$tablename(name,commithash)3046 ");3047}30483049# Construct the head table if required3050# The head table (along with the "last_commit" entry in the property3051# table) is the persisted working state of the "sub update" subroutine.3052# All of it's data is read entirely first, and completely recreated3053# last, every time "sub update" runs.3054# This is also used by "sub getmeta" when it is asked for the latest3055# version of a file (as opposed to some specific version).3056# Another way of thinking about it is as a single slice out of3057# "revisions", giving just the most recent revision information for3058# each file.3059unless($self->{tables}{$self->tablename("head")} )3060{3061my$tablename=$self->tablename("head");3062my$ix1name=$self->tablename("head_ix1");3063$self->{dbh}->do("3064 CREATE TABLE$tablename(3065 name TEXT NOT NULL,3066 revision INTEGER NOT NULL,3067 filehash TEXT NOT NULL,3068 commithash TEXT NOT NULL,3069 author TEXT NOT NULL,3070 modified TEXT NOT NULL,3071 mode TEXT NOT NULL3072 )3073 ");3074$self->{dbh}->do("3075 CREATE INDEX$ix1name3076 ON$tablename(name)3077 ");3078}30793080# Construct the properties table if required3081# - "last_commit" - Used by "sub update".3082unless($self->{tables}{$self->tablename("properties")} )3083{3084my$tablename=$self->tablename("properties");3085$self->{dbh}->do("3086 CREATE TABLE$tablename(3087 key TEXT NOT NULL PRIMARY KEY,3088 value TEXT3089 )3090 ");3091}30923093# Construct the commitmsgs table if required3094# The commitmsgs table is only used for merge commits, since3095# "sub update" will only keep one branch of parents. Shortlogs3096# for ignored commits (i.e. not on the chosen branch) will be used3097# to construct a replacement "collapsed" merge commit message,3098# which will be stored in this table. See also "sub commitmessage".3099unless($self->{tables}{$self->tablename("commitmsgs")} )3100{3101my$tablename=$self->tablename("commitmsgs");3102$self->{dbh}->do("3103 CREATE TABLE$tablename(3104 key TEXT NOT NULL PRIMARY KEY,3105 value TEXT3106 )3107 ");3108}31093110return$self;3111}31123113=head2 tablename31143115=cut3116sub tablename3117{3118my$self=shift;3119my$name=shift;31203121if(exists$self->{valid_tables}{$name}) {3122return$self->{dbtablenameprefix} .$name;3123}else{3124returnundef;3125}3126}31273128=head2 update31293130Bring the database up to date with the latest changes from3131the git repository.31323133Internal working state is read out of the "head" table and the3134"last_commit" property, then it updates "revisions" based on that, and3135finally it writes the new internal state back to the "head" table3136so it can be used as a starting point the next time update is called.31373138=cut3139sub update3140{3141my$self=shift;31423143# first lets get the commit list3144$ENV{GIT_DIR} =$self->{git_path};31453146my$commitsha1=`git rev-parse$self->{module}`;3147chomp$commitsha1;31483149my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3150unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3151{3152die("Invalid module '$self->{module}'");3153}315431553156my$git_log;3157my$lastcommit=$self->_get_prop("last_commit");31583159if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3160return1;3161}31623163# Start exclusive lock here...3164$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";31653166# TODO: log processing is memory bound3167# if we can parse into a 2nd file that is in reverse order3168# we can probably do something really efficient3169my@git_log_params= ('--pretty','--parents','--topo-order');31703171if(defined$lastcommit) {3172push@git_log_params,"$lastcommit..$self->{module}";3173}else{3174push@git_log_params,$self->{module};3175}3176# git-rev-list is the backend / plumbing version of git-log3177open(GITLOG,'-|','git','rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";31783179my@commits;31803181my%commit= ();31823183while( <GITLOG> )3184{3185chomp;3186if(m/^commit\s+(.*)$/) {3187# on ^commit lines put the just seen commit in the stack3188# and prime things for the next one3189if(keys%commit) {3190my%copy=%commit;3191unshift@commits, \%copy;3192%commit= ();3193}3194my@parents=split(m/\s+/,$1);3195$commit{hash} =shift@parents;3196$commit{parents} = \@parents;3197}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3198# on rfc822-like lines seen before we see any message,3199# lowercase the entry and put it in the hash as key-value3200$commit{lc($1)} =$2;3201}else{3202# message lines - skip initial empty line3203# and trim whitespace3204if(!exists($commit{message}) &&m/^\s*$/) {3205# define it to mark the end of headers3206$commit{message} ='';3207next;3208}3209s/^\s+//;s/\s+$//;# trim ws3210$commit{message} .=$_."\n";3211}3212}3213close GITLOG;32143215unshift@commits, \%commitif(keys%commit);32163217# Now all the commits are in the @commits bucket3218# ordered by time DESC. for each commit that needs processing,3219# determine whether it's following the last head we've seen or if3220# it's on its own branch, grab a file list, and add whatever's changed3221# NOTE: $lastcommit refers to the last commit from previous run3222# $lastpicked is the last commit we picked in this run3223my$lastpicked;3224my$head= {};3225if(defined$lastcommit) {3226$lastpicked=$lastcommit;3227}32283229my$committotal=scalar(@commits);3230my$commitcount=0;32313232# Load the head table into $head (for cached lookups during the update process)3233foreachmy$file( @{$self->gethead()} )3234{3235$head->{$file->{name}} =$file;3236}32373238foreachmy$commit(@commits)3239{3240$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3241if(defined$lastpicked)3242{3243if(!in_array($lastpicked, @{$commit->{parents}}))3244{3245# skip, we'll see this delta3246# as part of a merge later3247# warn "skipping off-track $commit->{hash}\n";3248next;3249}elsif(@{$commit->{parents}} >1) {3250# it is a merge commit, for each parent that is3251# not $lastpicked (not given a CVS revision number),3252# see if we can get a log3253# from the merge-base to that parent to put it3254# in the message as a merge summary.3255my@parents= @{$commit->{parents}};3256foreachmy$parent(@parents) {3257if($parenteq$lastpicked) {3258next;3259}3260# git-merge-base can potentially (but rarely) throw3261# several candidate merge bases. let's assume3262# that the first one is the best one.3263my$base=eval{3264 safe_pipe_capture('git','merge-base',3265$lastpicked,$parent);3266};3267# The two branches may not be related at all,3268# in which case merge base simply fails to find3269# any, but that's Ok.3270next if($@);32713272chomp$base;3273if($base) {3274my@merged;3275# print "want to log between $base $parent \n";3276open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3277or die"Cannot call git-log:$!";3278my$mergedhash;3279while(<GITLOG>) {3280chomp;3281if(!defined$mergedhash) {3282if(m/^commit\s+(.+)$/) {3283$mergedhash=$1;3284}else{3285next;3286}3287}else{3288# grab the first line that looks non-rfc8223289# aka has content after leading space3290if(m/^\s+(\S.*)$/) {3291my$title=$1;3292$title=substr($title,0,100);# truncate3293unshift@merged,"$mergedhash$title";3294undef$mergedhash;3295}3296}3297}3298close GITLOG;3299if(@merged) {3300$commit->{mergemsg} =$commit->{message};3301$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3302foreachmy$summary(@merged) {3303$commit->{mergemsg} .="\t$summary\n";3304}3305$commit->{mergemsg} .="\n\n";3306# print "Message for $commit->{hash} \n$commit->{mergemsg}";3307}3308}3309}3310}3311}33123313# convert the date to CVS-happy format3314$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);33153316if(defined($lastpicked) )3317{3318my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3319local($/) ="\0";3320while( <FILELIST> )3321{3322chomp;3323unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)3324{3325die("Couldn't process git-diff-tree line :$_");3326}3327my($mode,$hash,$change) = ($1,$2,$3);3328my$name= <FILELIST>;3329chomp($name);33303331# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");33323333my$git_perms="";3334$git_perms.="r"if($mode&4);3335$git_perms.="w"if($mode&2);3336$git_perms.="x"if($mode&1);3337$git_perms="rw"if($git_permseq"");33383339if($changeeq"D")3340{3341#$log->debug("DELETE $name");3342$head->{$name} = {3343 name =>$name,3344 revision =>$head->{$name}{revision} +1,3345 filehash =>"deleted",3346 commithash =>$commit->{hash},3347 modified =>$commit->{date},3348 author =>$commit->{author},3349 mode =>$git_perms,3350};3351$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3352}3353elsif($changeeq"M"||$changeeq"T")3354{3355#$log->debug("MODIFIED $name");3356$head->{$name} = {3357 name =>$name,3358 revision =>$head->{$name}{revision} +1,3359 filehash =>$hash,3360 commithash =>$commit->{hash},3361 modified =>$commit->{date},3362 author =>$commit->{author},3363 mode =>$git_perms,3364};3365$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3366}3367elsif($changeeq"A")3368{3369#$log->debug("ADDED $name");3370$head->{$name} = {3371 name =>$name,3372 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3373 filehash =>$hash,3374 commithash =>$commit->{hash},3375 modified =>$commit->{date},3376 author =>$commit->{author},3377 mode =>$git_perms,3378};3379$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3380}3381else3382{3383$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3384die;3385}3386}3387close FILELIST;3388}else{3389# this is used to detect files removed from the repo3390my$seen_files= {};33913392my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3393local$/="\0";3394while( <FILELIST> )3395{3396chomp;3397unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3398{3399die("Couldn't process git-ls-tree line :$_");3400}34013402my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);34033404$seen_files->{$git_filename} =1;34053406my($oldhash,$oldrevision,$oldmode) = (3407$head->{$git_filename}{filehash},3408$head->{$git_filename}{revision},3409$head->{$git_filename}{mode}3410);34113412if($git_perms=~/^\d\d\d(\d)\d\d/o)3413{3414$git_perms="";3415$git_perms.="r"if($1&4);3416$git_perms.="w"if($1&2);3417$git_perms.="x"if($1&1);3418}else{3419$git_perms="rw";3420}34213422# unless the file exists with the same hash, we need to update it ...3423unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)3424{3425my$newrevision= ($oldrevisionor0) +1;34263427$head->{$git_filename} = {3428 name =>$git_filename,3429 revision =>$newrevision,3430 filehash =>$git_hash,3431 commithash =>$commit->{hash},3432 modified =>$commit->{date},3433 author =>$commit->{author},3434 mode =>$git_perms,3435};343634373438$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3439}3440}3441close FILELIST;34423443# Detect deleted files3444foreachmy$file(keys%$head)3445{3446unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3447{3448$head->{$file}{revision}++;3449$head->{$file}{filehash} ="deleted";3450$head->{$file}{commithash} =$commit->{hash};3451$head->{$file}{modified} =$commit->{date};3452$head->{$file}{author} =$commit->{author};34533454$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});3455}3456}3457# END : "Detect deleted files"3458}345934603461if(exists$commit->{mergemsg})3462{3463$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3464}34653466$lastpicked=$commit->{hash};34673468$self->_set_prop("last_commit",$commit->{hash});3469}34703471$self->delete_head();3472foreachmy$file(keys%$head)3473{3474$self->insert_head(3475$file,3476$head->{$file}{revision},3477$head->{$file}{filehash},3478$head->{$file}{commithash},3479$head->{$file}{modified},3480$head->{$file}{author},3481$head->{$file}{mode},3482);3483}3484# invalidate the gethead cache3485$self->{gethead_cache} =undef;348634873488# Ending exclusive lock here3489$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3490}34913492sub insert_rev3493{3494my$self=shift;3495my$name=shift;3496my$revision=shift;3497my$filehash=shift;3498my$commithash=shift;3499my$modified=shift;3500my$author=shift;3501my$mode=shift;3502my$tablename=$self->tablename("revision");35033504my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3505$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3506}35073508sub insert_mergelog3509{3510my$self=shift;3511my$key=shift;3512my$value=shift;3513my$tablename=$self->tablename("commitmsgs");35143515my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3516$insert_mergelog->execute($key,$value);3517}35183519sub delete_head3520{3521my$self=shift;3522my$tablename=$self->tablename("head");35233524my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3525$delete_head->execute();3526}35273528sub insert_head3529{3530my$self=shift;3531my$name=shift;3532my$revision=shift;3533my$filehash=shift;3534my$commithash=shift;3535my$modified=shift;3536my$author=shift;3537my$mode=shift;3538my$tablename=$self->tablename("head");35393540my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3541$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3542}35433544sub _get_prop3545{3546my$self=shift;3547my$key=shift;3548my$tablename=$self->tablename("properties");35493550my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3551$db_query->execute($key);3552my($value) =$db_query->fetchrow_array;35533554return$value;3555}35563557sub _set_prop3558{3559my$self=shift;3560my$key=shift;3561my$value=shift;3562my$tablename=$self->tablename("properties");35633564my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3565$db_query->execute($value,$key);35663567unless($db_query->rows)3568{3569$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3570$db_query->execute($key,$value);3571}35723573return$value;3574}35753576=head2 gethead35773578=cut35793580sub gethead3581{3582my$self=shift;3583my$tablename=$self->tablename("head");35843585return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );35863587my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3588$db_query->execute();35893590my$tree= [];3591while(my$file=$db_query->fetchrow_hashref)3592{3593push@$tree,$file;3594}35953596$self->{gethead_cache} =$tree;35973598return$tree;3599}36003601=head2 getlog36023603See also gethistorydense().36043605=cut36063607sub getlog3608{3609my$self=shift;3610my$filename=shift;3611my$tablename=$self->tablename("revision");36123613my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3614$db_query->execute($filename);36153616my$tree= [];3617while(my$file=$db_query->fetchrow_hashref)3618{3619push@$tree,$file;3620}36213622return$tree;3623}36243625=head2 getmeta36263627This function takes a filename (with path) argument and returns a hashref of3628metadata for that file.36293630=cut36313632sub getmeta3633{3634my$self=shift;3635my$filename=shift;3636my$revision=shift;3637my$tablename_rev=$self->tablename("revision");3638my$tablename_head=$self->tablename("head");36393640my$db_query;3641if(defined($revision)and$revision=~/^\d+$/)3642{3643$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND revision=?",{},1);3644$db_query->execute($filename,$revision);3645}3646elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)3647{3648$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND commithash=?",{},1);3649$db_query->execute($filename,$revision);3650}else{3651$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_headWHERE name=?",{},1);3652$db_query->execute($filename);3653}36543655return$db_query->fetchrow_hashref;3656}36573658=head2 commitmessage36593660this function takes a commithash and returns the commit message for that commit36613662=cut3663sub commitmessage3664{3665my$self=shift;3666my$commithash=shift;3667my$tablename=$self->tablename("commitmsgs");36683669die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);36703671my$db_query;3672$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3673$db_query->execute($commithash);36743675my($message) =$db_query->fetchrow_array;36763677if(defined($message) )3678{3679$message.=" "if($message=~/\n$/);3680return$message;3681}36823683my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);3684shift@lineswhile($lines[0] =~/\S/);3685$message=join("",@lines);3686$message.=" "if($message=~/\n$/);3687return$message;3688}36893690=head2 gethistorydense36913692This function takes a filename (with path) argument and returns an arrayofarrays3693containing revision,filehash,commithash ordered by revision descending.36943695This version of gethistory skips deleted entries -- so it is useful for annotate.3696The 'dense' part is a reference to a '--dense' option available for git-rev-list3697and other git tools that depend on it.36983699See also getlog().37003701=cut3702sub gethistorydense3703{3704my$self=shift;3705my$filename=shift;3706my$tablename=$self->tablename("revision");37073708my$db_query;3709$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);3710$db_query->execute($filename);37113712return$db_query->fetchall_arrayref;3713}37143715=head2 in_array()37163717from Array::PAT - mimics the in_array() function3718found in PHP. Yuck but works for small arrays.37193720=cut3721sub in_array3722{3723my($check,@array) =@_;3724my$retval=0;3725foreachmy$test(@array){3726if($checkeq$test){3727$retval=1;3728}3729}3730return$retval;3731}37323733=head2 safe_pipe_capture37343735an alternative to `command` that allows input to be passed as an array3736to work around shell problems with weird characters in arguments37373738=cut3739sub safe_pipe_capture {37403741my@output;37423743if(my$pid=open my$child,'-|') {3744@output= (<$child>);3745close$childor die join(' ',@_).":$!$?";3746}else{3747exec(@_)or die"$!$?";# exec() can fail the executable can't be found3748}3749returnwantarray?@output:join('',@output);3750}37513752=head2 mangle_dirname37533754create a string from a directory name that is suitable to use as3755part of a filename, mainly by converting all chars except \w.- to _37563757=cut3758sub mangle_dirname {3759my$dirname=shift;3760return unlessdefined$dirname;37613762$dirname=~s/[^\w.-]/_/g;37633764return$dirname;3765}37663767=head2 mangle_tablename37683769create a string from a that is suitable to use as part of an SQL table3770name, mainly by converting all chars except \w to _37713772=cut3773sub mangle_tablename {3774my$tablename=shift;3775return unlessdefined$tablename;37763777$tablename=~s/[^\w_]/_/g;37783779return$tablename;3780}378137821;