1#!/usr/bin/perl 2 3#### 4#### This application is a CVS emulation layer for git. 5#### It is intended for clients to connect over SSH. 6#### See the documentation for more details. 7#### 8#### Copyright The Open University UK - 2006. 9#### 10#### Authors: Martyn Smith <martyn@catalyst.net.nz> 11#### Martin Langhoff <martin@catalyst.net.nz> 12#### 13#### 14#### Released under the GNU Public License, version 2. 15#### 16#### 17 18use strict; 19use warnings; 20use bytes; 21 22use Fcntl; 23use File::Temp qw/tempdir tempfile/; 24use File::Path qw/rmtree/; 25use File::Basename; 26use Getopt::Long qw(:config require_order no_ignore_case); 27 28my$VERSION='@@GIT_VERSION@@'; 29 30my$log= GITCVS::log->new(); 31my$cfg; 32 33my$DATE_LIST= { 34 Jan =>"01", 35 Feb =>"02", 36 Mar =>"03", 37 Apr =>"04", 38 May =>"05", 39 Jun =>"06", 40 Jul =>"07", 41 Aug =>"08", 42 Sep =>"09", 43 Oct =>"10", 44 Nov =>"11", 45 Dec =>"12", 46}; 47 48# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 49$| =1; 50 51#### Definition and mappings of functions #### 52 53my$methods= { 54'Root'=> \&req_Root, 55'Valid-responses'=> \&req_Validresponses, 56'valid-requests'=> \&req_validrequests, 57'Directory'=> \&req_Directory, 58'Entry'=> \&req_Entry, 59'Modified'=> \&req_Modified, 60'Unchanged'=> \&req_Unchanged, 61'Questionable'=> \&req_Questionable, 62'Argument'=> \&req_Argument, 63'Argumentx'=> \&req_Argument, 64'expand-modules'=> \&req_expandmodules, 65'add'=> \&req_add, 66'remove'=> \&req_remove, 67'co'=> \&req_co, 68'update'=> \&req_update, 69'ci'=> \&req_ci, 70'diff'=> \&req_diff, 71'log'=> \&req_log, 72'rlog'=> \&req_log, 73'tag'=> \&req_CATCHALL, 74'status'=> \&req_status, 75'admin'=> \&req_CATCHALL, 76'history'=> \&req_CATCHALL, 77'watchers'=> \&req_EMPTY, 78'editors'=> \&req_EMPTY, 79'noop'=> \&req_EMPTY, 80'annotate'=> \&req_annotate, 81'Global_option'=> \&req_Globaloption, 82#'annotate' => \&req_CATCHALL, 83}; 84 85############################################## 86 87 88# $state holds all the bits of information the clients sends us that could 89# potentially be useful when it comes to actually _doing_ something. 90my$state= { prependdir =>''}; 91 92# Work is for managing temporary working directory 93my$work= 94{ 95state=>undef,# undef, 1 (empty), 2 (with stuff) 96 workDir =>undef, 97index=>undef, 98 emptyDir =>undef, 99 tmpDir =>undef 100}; 101 102$log->info("--------------- STARTING -----------------"); 103 104my$usage= 105"Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n". 106" --base-path <path> : Prepend to requested CVSROOT\n". 107" Can be read from GIT_CVSSERVER_BASE_PATH\n". 108" --strict-paths : Don't allow recursing into subdirectories\n". 109" --export-all : Don't check for gitcvs.enabled in config\n". 110" --version, -V : Print version information and exit\n". 111" --help, -h, -H : Print usage information and exit\n". 112"\n". 113"<directory> ... is a list of allowed directories. If no directories\n". 114"are given, all are allowed. This is an additional restriction, gitcvs\n". 115"access still needs to be enabled by the gitcvs.enabled config option.\n". 116"Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n"; 117 118my@opts= ('help|h|H','version|V', 119'base-path=s','strict-paths','export-all'); 120GetOptions($state,@opts) 121or die$usage; 122 123if($state->{version}) { 124print"git-cvsserver version$VERSION\n"; 125exit; 126} 127if($state->{help}) { 128print$usage; 129exit; 130} 131 132my$TEMP_DIR= tempdir( CLEANUP =>1); 133$log->debug("Temporary directory is '$TEMP_DIR'"); 134 135$state->{method} ='ext'; 136if(@ARGV) { 137if($ARGV[0]eq'pserver') { 138$state->{method} ='pserver'; 139shift@ARGV; 140}elsif($ARGV[0]eq'server') { 141shift@ARGV; 142} 143} 144 145# everything else is a directory 146$state->{allowed_roots} = [@ARGV]; 147 148# don't export the whole system unless the users requests it 149if($state->{'export-all'} && !@{$state->{allowed_roots}}) { 150die"--export-all can only be used together with an explicit whitelist\n"; 151} 152 153# Environment handling for running under git-shell 154if(exists$ENV{GIT_CVSSERVER_BASE_PATH}) { 155if($state->{'base-path'}) { 156die"Cannot specify base path both ways.\n"; 157} 158my$base_path=$ENV{GIT_CVSSERVER_BASE_PATH}; 159$state->{'base-path'} =$base_path; 160$log->debug("Picked up base path '$base_path' from environment.\n"); 161} 162if(exists$ENV{GIT_CVSSERVER_ROOT}) { 163if(@{$state->{allowed_roots}}) { 164die"Cannot specify roots both ways:@ARGV\n"; 165} 166my$allowed_root=$ENV{GIT_CVSSERVER_ROOT}; 167$state->{allowed_roots} = [$allowed_root]; 168$log->debug("Picked up allowed root '$allowed_root' from environment.\n"); 169} 170 171# if we are called with a pserver argument, 172# deal with the authentication cat before entering the 173# main loop 174if($state->{method}eq'pserver') { 175my$line= <STDIN>;chomp$line; 176unless($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 177die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 178} 179my$request=$1; 180$line= <STDIN>;chomp$line; 181unless(req_Root('root',$line)) {# reuse Root 182print"E Invalid root$line\n"; 183exit1; 184} 185$line= <STDIN>;chomp$line; 186my$user=$line; 187$line= <STDIN>;chomp$line; 188my$password=$line; 189 190unless($usereq'anonymous') { 191# Trying to authenticate a user 192if(not exists$cfg->{gitcvs}->{users}) { 193print"E the repo config file needs a [gitcvs.users] section with user/password key-value pairs\n"; 194print"I HATE YOU\n"; 195exit1; 196}elsif(exists$cfg->{gitcvs}->{users}and not exists$cfg->{gitcvs}->{users}->{$user}) { 197#print "E the repo config file has a [gitcvs.users] section but the user $user is not defined in it\n"; 198print"I HATE YOU\n"; 199exit1; 200}else{ 201my$descrambled_password= descramble($password); 202my$cleartext_password=$cfg->{gitcvs}->{users}->{$user}; 203if($descrambled_passwordne$cleartext_password) { 204#print "E The password supplied for user $user was incorrect\n"; 205print"I HATE YOU\n"; 206exit1; 207} 208# else fall through to LOVE 209} 210} 211 212# For checking whether the user is anonymous on commit 213$state->{user} =$user; 214 215$line= <STDIN>;chomp$line; 216unless($lineeq"END$requestREQUEST") { 217die"E Do not understand$line-- expecting END$requestREQUEST\n"; 218} 219print"I LOVE YOU\n"; 220exit if$requesteq'VERIFICATION';# cvs login 221# and now back to our regular programme... 222} 223 224# Keep going until the client closes the connection 225while(<STDIN>) 226{ 227chomp; 228 229# Check to see if we've seen this method, and call appropriate function. 230if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 231{ 232# use the $methods hash to call the appropriate sub for this command 233#$log->info("Method : $1"); 234&{$methods->{$1}}($1,$2); 235}else{ 236# log fatal because we don't understand this function. If this happens 237# we're fairly screwed because we don't know if the client is expecting 238# a response. If it is, the client will hang, we'll hang, and the whole 239# thing will be custard. 240$log->fatal("Don't understand command$_\n"); 241die("Unknown command$_"); 242} 243} 244 245$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 246$log->info("--------------- FINISH -----------------"); 247 248chdir'/'; 249exit0; 250 251# Magic catchall method. 252# This is the method that will handle all commands we haven't yet 253# implemented. It simply sends a warning to the log file indicating a 254# command that hasn't been implemented has been invoked. 255sub req_CATCHALL 256{ 257my($cmd,$data) =@_; 258$log->warn("Unhandled command : req_$cmd:$data"); 259} 260 261# This method invariably succeeds with an empty response. 262sub req_EMPTY 263{ 264print"ok\n"; 265} 266 267# Root pathname \n 268# Response expected: no. Tell the server which CVSROOT to use. Note that 269# pathname is a local directory and not a fully qualified CVSROOT variable. 270# pathname must already exist; if creating a new root, use the init 271# request, not Root. pathname does not include the hostname of the server, 272# how to access the server, etc.; by the time the CVS protocol is in use, 273# connection, authentication, etc., are already taken care of. The Root 274# request must be sent only once, and it must be sent before any requests 275# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 276sub req_Root 277{ 278my($cmd,$data) =@_; 279$log->debug("req_Root :$data"); 280 281unless($data=~ m#^/#) { 282print"error 1 Root must be an absolute pathname\n"; 283return0; 284} 285 286my$cvsroot=$state->{'base-path'} ||''; 287$cvsroot=~ s#/+$##; 288$cvsroot.=$data; 289 290if($state->{CVSROOT} 291&& ($state->{CVSROOT}ne$cvsroot)) { 292print"error 1 Conflicting roots specified\n"; 293return0; 294} 295 296$state->{CVSROOT} =$cvsroot; 297 298$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 299 300if(@{$state->{allowed_roots}}) { 301my$allowed=0; 302foreachmy$dir(@{$state->{allowed_roots}}) { 303next unless$dir=~ m#^/#; 304$dir=~ s#/+$##; 305if($state->{'strict-paths'}) { 306if($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { 307$allowed=1; 308last; 309} 310}elsif($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { 311$allowed=1; 312last; 313} 314} 315 316unless($allowed) { 317print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 318print"E\n"; 319print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 320return0; 321} 322} 323 324unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 325print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 326print"E\n"; 327print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 328return0; 329} 330 331my@gitvars=`git config -l`; 332if($?) { 333print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 334print"E\n"; 335print"error 1 - problem executing git-config\n"; 336return0; 337} 338foreachmy$line(@gitvars) 339{ 340next unless($line=~/^(gitcvs)\.(?:(ext|pserver|users)\.)?([\w-]+)=(.*)$/); 341unless($2) { 342$cfg->{$1}{$3} =$4; 343}else{ 344$cfg->{$1}{$2}{$3} =$4; 345} 346} 347 348my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 349||$cfg->{gitcvs}{enabled}); 350unless($state->{'export-all'} || 351($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i)) { 352print"E GITCVS emulation needs to be enabled on this repo\n"; 353print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 354print"E\n"; 355print"error 1 GITCVS emulation disabled\n"; 356return0; 357} 358 359my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 360if($logfile) 361{ 362$log->setfile($logfile); 363}else{ 364$log->nofile(); 365} 366 367return1; 368} 369 370# Global_option option \n 371# Response expected: no. Transmit one of the global options `-q', `-Q', 372# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 373# variations (such as combining of options) are allowed. For graceful 374# handling of valid-requests, it is probably better to make new global 375# options separate requests, rather than trying to add them to this 376# request. 377sub req_Globaloption 378{ 379my($cmd,$data) =@_; 380$log->debug("req_Globaloption :$data"); 381$state->{globaloptions}{$data} =1; 382} 383 384# Valid-responses request-list \n 385# Response expected: no. Tell the server what responses the client will 386# accept. request-list is a space separated list of tokens. 387sub req_Validresponses 388{ 389my($cmd,$data) =@_; 390$log->debug("req_Validresponses :$data"); 391 392# TODO : re-enable this, currently it's not particularly useful 393#$state->{validresponses} = [ split /\s+/, $data ]; 394} 395 396# valid-requests \n 397# Response expected: yes. Ask the server to send back a Valid-requests 398# response. 399sub req_validrequests 400{ 401my($cmd,$data) =@_; 402 403$log->debug("req_validrequests"); 404 405$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 406$log->debug("SEND : ok"); 407 408print"Valid-requests ".join(" ",keys%$methods) ."\n"; 409print"ok\n"; 410} 411 412# Directory local-directory \n 413# Additional data: repository \n. Response expected: no. Tell the server 414# what directory to use. The repository should be a directory name from a 415# previous server response. Note that this both gives a default for Entry 416# and Modified and also for ci and the other commands; normal usage is to 417# send Directory for each directory in which there will be an Entry or 418# Modified, and then a final Directory for the original directory, then the 419# command. The local-directory is relative to the top level at which the 420# command is occurring (i.e. the last Directory which is sent before the 421# command); to indicate that top level, `.' should be sent for 422# local-directory. 423sub req_Directory 424{ 425my($cmd,$data) =@_; 426 427my$repository= <STDIN>; 428chomp$repository; 429 430 431$state->{localdir} =$data; 432$state->{repository} =$repository; 433$state->{path} =$repository; 434$state->{path} =~s/^\Q$state->{CVSROOT}\E\///; 435$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 436$state->{path} .="/"if($state->{path} =~ /\S/ ); 437 438$state->{directory} =$state->{localdir}; 439$state->{directory} =""if($state->{directory}eq"."); 440$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 441 442if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 443{ 444$log->info("Setting prepend to '$state->{path}'"); 445$state->{prependdir} =$state->{path}; 446foreachmy$entry(keys%{$state->{entries}} ) 447{ 448$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 449delete$state->{entries}{$entry}; 450} 451} 452 453if(defined($state->{prependdir} ) ) 454{ 455$log->debug("Prepending '$state->{prependdir}' to state|directory"); 456$state->{directory} =$state->{prependdir} .$state->{directory} 457} 458$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 459} 460 461# Entry entry-line \n 462# Response expected: no. Tell the server what version of a file is on the 463# local machine. The name in entry-line is a name relative to the directory 464# most recently specified with Directory. If the user is operating on only 465# some files in a directory, Entry requests for only those files need be 466# included. If an Entry request is sent without Modified, Is-modified, or 467# Unchanged, it means the file is lost (does not exist in the working 468# directory). If both Entry and one of Modified, Is-modified, or Unchanged 469# are sent for the same file, Entry must be sent first. For a given file, 470# one can send Modified, Is-modified, or Unchanged, but not more than one 471# of these three. 472sub req_Entry 473{ 474my($cmd,$data) =@_; 475 476#$log->debug("req_Entry : $data"); 477 478my@data=split(/\//,$data); 479 480$state->{entries}{$state->{directory}.$data[1]} = { 481 revision =>$data[2], 482 conflict =>$data[3], 483 options =>$data[4], 484 tag_or_date =>$data[5], 485}; 486 487$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 488} 489 490# Questionable filename \n 491# Response expected: no. Additional data: no. Tell the server to check 492# whether filename should be ignored, and if not, next time the server 493# sends responses, send (in a M response) `?' followed by the directory and 494# filename. filename must not contain `/'; it needs to be a file in the 495# directory named by the most recent Directory request. 496sub req_Questionable 497{ 498my($cmd,$data) =@_; 499 500$log->debug("req_Questionable :$data"); 501$state->{entries}{$state->{directory}.$data}{questionable} =1; 502} 503 504# add \n 505# Response expected: yes. Add a file or directory. This uses any previous 506# Argument, Directory, Entry, or Modified requests, if they have been sent. 507# The last Directory sent specifies the working directory at the time of 508# the operation. To add a directory, send the directory to be added using 509# Directory and Argument requests. 510sub req_add 511{ 512my($cmd,$data) =@_; 513 514 argsplit("add"); 515 516my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 517$updater->update(); 518 519 argsfromdir($updater); 520 521my$addcount=0; 522 523foreachmy$filename( @{$state->{args}} ) 524{ 525$filename= filecleanup($filename); 526 527my$meta=$updater->getmeta($filename); 528my$wrev= revparse($filename); 529 530if($wrev&&$meta&& ($wrev<0)) 531{ 532# previously removed file, add back 533$log->info("added file$filenamewas previously removed, send 1.$meta->{revision}"); 534 535print"MT +updated\n"; 536print"MT text U\n"; 537print"MT fname$filename\n"; 538print"MT newline\n"; 539print"MT -updated\n"; 540 541unless($state->{globaloptions}{-n} ) 542{ 543my($filepart,$dirpart) = filenamesplit($filename,1); 544 545print"Created$dirpart\n"; 546print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 547 548# this is an "entries" line 549my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 550$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 551print"/$filepart/1.$meta->{revision}//$kopts/\n"; 552# permissions 553$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 554print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 555# transmit file 556 transmitfile($meta->{filehash}); 557} 558 559next; 560} 561 562unless(defined($state->{entries}{$filename}{modified_filename} ) ) 563{ 564print"E cvs add: nothing known about `$filename'\n"; 565next; 566} 567# TODO : check we're not squashing an already existing file 568if(defined($state->{entries}{$filename}{revision} ) ) 569{ 570print"E cvs add: `$filename' has already been entered\n"; 571next; 572} 573 574my($filepart,$dirpart) = filenamesplit($filename,1); 575 576print"E cvs add: scheduling file `$filename' for addition\n"; 577 578print"Checked-in$dirpart\n"; 579print"$filename\n"; 580my$kopts= kopts_from_path($filename,"file", 581$state->{entries}{$filename}{modified_filename}); 582print"/$filepart/0//$kopts/\n"; 583 584my$requestedKopts=$state->{opt}{k}; 585if(defined($requestedKopts)) 586{ 587$requestedKopts="-k$requestedKopts"; 588} 589else 590{ 591$requestedKopts=""; 592} 593if($koptsne$requestedKopts) 594{ 595$log->warn("Ignoring requested -k='$requestedKopts'" 596." for '$filename'; detected -k='$kopts' instead"); 597#TODO: Also have option to send warning to user? 598} 599 600$addcount++; 601} 602 603if($addcount==1) 604{ 605print"E cvs add: use `cvs commit' to add this file permanently\n"; 606} 607elsif($addcount>1) 608{ 609print"E cvs add: use `cvs commit' to add these files permanently\n"; 610} 611 612print"ok\n"; 613} 614 615# remove \n 616# Response expected: yes. Remove a file. This uses any previous Argument, 617# Directory, Entry, or Modified requests, if they have been sent. The last 618# Directory sent specifies the working directory at the time of the 619# operation. Note that this request does not actually do anything to the 620# repository; the only effect of a successful remove request is to supply 621# the client with a new entries line containing `-' to indicate a removed 622# file. In fact, the client probably could perform this operation without 623# contacting the server, although using remove may cause the server to 624# perform a few more checks. The client sends a subsequent ci request to 625# actually record the removal in the repository. 626sub req_remove 627{ 628my($cmd,$data) =@_; 629 630 argsplit("remove"); 631 632# Grab a handle to the SQLite db and do any necessary updates 633my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 634$updater->update(); 635 636#$log->debug("add state : " . Dumper($state)); 637 638my$rmcount=0; 639 640foreachmy$filename( @{$state->{args}} ) 641{ 642$filename= filecleanup($filename); 643 644if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 645{ 646print"E cvs remove: file `$filename' still in working directory\n"; 647next; 648} 649 650my$meta=$updater->getmeta($filename); 651my$wrev= revparse($filename); 652 653unless(defined($wrev) ) 654{ 655print"E cvs remove: nothing known about `$filename'\n"; 656next; 657} 658 659if(defined($wrev)and$wrev<0) 660{ 661print"E cvs remove: file `$filename' already scheduled for removal\n"; 662next; 663} 664 665unless($wrev==$meta->{revision} ) 666{ 667# TODO : not sure if the format of this message is quite correct. 668print"E cvs remove: Up to date check failed for `$filename'\n"; 669next; 670} 671 672 673my($filepart,$dirpart) = filenamesplit($filename,1); 674 675print"E cvs remove: scheduling `$filename' for removal\n"; 676 677print"Checked-in$dirpart\n"; 678print"$filename\n"; 679my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 680print"/$filepart/-1.$wrev//$kopts/\n"; 681 682$rmcount++; 683} 684 685if($rmcount==1) 686{ 687print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 688} 689elsif($rmcount>1) 690{ 691print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 692} 693 694print"ok\n"; 695} 696 697# Modified filename \n 698# Response expected: no. Additional data: mode, \n, file transmission. Send 699# the server a copy of one locally modified file. filename is a file within 700# the most recent directory sent with Directory; it must not contain `/'. 701# If the user is operating on only some files in a directory, only those 702# files need to be included. This can also be sent without Entry, if there 703# is no entry for the file. 704sub req_Modified 705{ 706my($cmd,$data) =@_; 707 708my$mode= <STDIN>; 709defined$mode 710or(print"E end of file reading mode for$data\n"),return; 711chomp$mode; 712my$size= <STDIN>; 713defined$size 714or(print"E end of file reading size of$data\n"),return; 715chomp$size; 716 717# Grab config information 718my$blocksize=8192; 719my$bytesleft=$size; 720my$tmp; 721 722# Get a filehandle/name to write it to 723my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 724 725# Loop over file data writing out to temporary file. 726while($bytesleft) 727{ 728$blocksize=$bytesleftif($bytesleft<$blocksize); 729read STDIN,$tmp,$blocksize; 730print$fh $tmp; 731$bytesleft-=$blocksize; 732} 733 734close$fh 735or(print"E failed to write temporary,$filename:$!\n"),return; 736 737# Ensure we have something sensible for the file mode 738if($mode=~/u=(\w+)/) 739{ 740$mode=$1; 741}else{ 742$mode="rw"; 743} 744 745# Save the file data in $state 746$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 747$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 748$state->{entries}{$state->{directory}.$data}{modified_hash} =`git hash-object$filename`; 749$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 750 751 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 752} 753 754# Unchanged filename\n 755# Response expected: no. Tell the server that filename has not been 756# modified in the checked out directory. The filename is a file within the 757# most recent directory sent with Directory; it must not contain `/'. 758sub req_Unchanged 759{ 760 my ($cmd,$data) =@_; 761 762$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 763 764 #$log->debug("req_Unchanged :$data"); 765} 766 767# Argument text\n 768# Response expected: no. Save argument for use in a subsequent command. 769# Arguments accumulate until an argument-using command is given, at which 770# point they are forgotten. 771# Argumentx text\n 772# Response expected: no. Append\nfollowed by text to the current argument 773# being saved. 774sub req_Argument 775{ 776 my ($cmd,$data) =@_; 777 778 # Argumentx means: append to last Argument (with a newline in front) 779 780$log->debug("$cmd:$data"); 781 782 if ($cmdeq 'Argumentx') { 783 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 784 } else { 785 push @{$state->{arguments}},$data; 786 } 787} 788 789# expand-modules\n 790# Response expected: yes. Expand the modules which are specified in the 791# arguments. Returns the data in Module-expansion responses. Note that the 792# server can assume that this is checkout or export, not rtag or rdiff; the 793# latter do not access the working directory and thus have no need to 794# expand modules on the client side. Expand may not be the best word for 795# what this request does. It does not necessarily tell you all the files 796# contained in a module, for example. Basically it is a way of telling you 797# which working directories the server needs to know about in order to 798# handle a checkout of the specified modules. For example, suppose that the 799# server has a module defined by 800# aliasmodule -a 1dir 801# That is, one can check out aliasmodule and it will take 1dir in the 802# repository and check it out to 1dir in the working directory. Now suppose 803# the client already has this module checked out and is planning on using 804# the co request to update it. Without using expand-modules, the client 805# would have two bad choices: it could either send information about all 806# working directories under the current directory, which could be 807# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 808# stands for 1dir, and neglect to send information for 1dir, which would 809# lead to incorrect operation. With expand-modules, the client would first 810# ask for the module to be expanded: 811sub req_expandmodules 812{ 813 my ($cmd,$data) =@_; 814 815 argsplit(); 816 817$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 818 819 unless ( ref$state->{arguments} eq "ARRAY" ) 820 { 821 print "ok\n"; 822 return; 823 } 824 825 foreach my$module( @{$state->{arguments}} ) 826 { 827$log->debug("SEND : Module-expansion$module"); 828 print "Module-expansion$module\n"; 829 } 830 831 print "ok\n"; 832 statecleanup(); 833} 834 835# co\n 836# Response expected: yes. Get files from the repository. This uses any 837# previous Argument, Directory, Entry, or Modified requests, if they have 838# been sent. Arguments to this command are module names; the client cannot 839# know what directories they correspond to except by (1) just sending the 840# co request, and then seeing what directory names the server sends back in 841# its responses, and (2) the expand-modules request. 842sub req_co 843{ 844 my ($cmd,$data) =@_; 845 846 argsplit("co"); 847 848 # Provide list of modules, if -c was used. 849 if (exists$state->{opt}{c}) { 850 my$showref= `git show-ref --heads`; 851 for my$line(split '\n',$showref) { 852 if ($line=~ m% refs/heads/(.*)$%) { 853 print "M$1\t$1\n"; 854 } 855 } 856 print "ok\n"; 857 return 1; 858 } 859 860 my$module=$state->{args}[0]; 861$state->{module} =$module; 862 my$checkout_path=$module; 863 864 # use the user specified directory if we're given it 865$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 866 867$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 868 869$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 870 871$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 872 873# Grab a handle to the SQLite db and do any necessary updates 874my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 875$updater->update(); 876 877$checkout_path=~ s|/$||;# get rid of trailing slashes 878 879# Eclipse seems to need the Clear-sticky command 880# to prepare the 'Entries' file for the new directory. 881print"Clear-sticky$checkout_path/\n"; 882print$state->{CVSROOT} ."/$module/\n"; 883print"Clear-static-directory$checkout_path/\n"; 884print$state->{CVSROOT} ."/$module/\n"; 885print"Clear-sticky$checkout_path/\n";# yes, twice 886print$state->{CVSROOT} ."/$module/\n"; 887print"Template$checkout_path/\n"; 888print$state->{CVSROOT} ."/$module/\n"; 889print"0\n"; 890 891# instruct the client that we're checking out to $checkout_path 892print"E cvs checkout: Updating$checkout_path\n"; 893 894my%seendirs= (); 895my$lastdir=''; 896 897# recursive 898sub prepdir { 899my($dir,$repodir,$remotedir,$seendirs) =@_; 900my$parent= dirname($dir); 901$dir=~ s|/+$||; 902$repodir=~ s|/+$||; 903$remotedir=~ s|/+$||; 904$parent=~ s|/+$||; 905$log->debug("announcedir$dir,$repodir,$remotedir"); 906 907if($parenteq'.'||$parenteq'./') { 908$parent=''; 909} 910# recurse to announce unseen parents first 911if(length($parent) && !exists($seendirs->{$parent})) { 912 prepdir($parent,$repodir,$remotedir,$seendirs); 913} 914# Announce that we are going to modify at the parent level 915if($parent) { 916print"E cvs checkout: Updating$remotedir/$parent\n"; 917}else{ 918print"E cvs checkout: Updating$remotedir\n"; 919} 920print"Clear-sticky$remotedir/$parent/\n"; 921print"$repodir/$parent/\n"; 922 923print"Clear-static-directory$remotedir/$dir/\n"; 924print"$repodir/$dir/\n"; 925print"Clear-sticky$remotedir/$parent/\n";# yes, twice 926print"$repodir/$parent/\n"; 927print"Template$remotedir/$dir/\n"; 928print"$repodir/$dir/\n"; 929print"0\n"; 930 931$seendirs->{$dir} =1; 932} 933 934foreachmy$git( @{$updater->gethead} ) 935{ 936# Don't want to check out deleted files 937next if($git->{filehash}eq"deleted"); 938 939my$fullName=$git->{name}; 940($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 941 942if(length($git->{dir}) &&$git->{dir}ne'./' 943&&$git->{dir}ne$lastdir) { 944unless(exists($seendirs{$git->{dir}})) { 945 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 946$checkout_path, \%seendirs); 947$lastdir=$git->{dir}; 948$seendirs{$git->{dir}} =1; 949} 950print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 951} 952 953# modification time of this file 954print"Mod-time$git->{modified}\n"; 955 956# print some information to the client 957if(defined($git->{dir} )and$git->{dir}ne"./") 958{ 959print"M U$checkout_path/$git->{dir}$git->{name}\n"; 960}else{ 961print"M U$checkout_path/$git->{name}\n"; 962} 963 964# instruct client we're sending a file to put in this path 965print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 966 967print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 968 969# this is an "entries" line 970my$kopts= kopts_from_path($fullName,"sha1",$git->{filehash}); 971print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 972# permissions 973print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 974 975# transmit file 976 transmitfile($git->{filehash}); 977} 978 979print"ok\n"; 980 981 statecleanup(); 982} 983 984# update \n 985# Response expected: yes. Actually do a cvs update command. This uses any 986# previous Argument, Directory, Entry, or Modified requests, if they have 987# been sent. The last Directory sent specifies the working directory at the 988# time of the operation. The -I option is not used--files which the client 989# can decide whether to ignore are not mentioned and the client sends the 990# Questionable request for others. 991sub req_update 992{ 993my($cmd,$data) =@_; 994 995$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 996 997 argsplit("update"); 998 999#1000# It may just be a client exploring the available heads/modules1001# in that case, list them as top level directories and leave it1002# at that. Eclipse uses this technique to offer you a list of1003# projects (heads in this case) to checkout.1004#1005if($state->{module}eq'') {1006my$showref=`git show-ref --heads`;1007print"E cvs update: Updating .\n";1008formy$line(split'\n',$showref) {1009if($line=~ m% refs/heads/(.*)$%) {1010print"E cvs update: New directory `$1'\n";1011}1012}1013print"ok\n";1014return1;1015}101610171018# Grab a handle to the SQLite db and do any necessary updates1019my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);10201021$updater->update();10221023 argsfromdir($updater);10241025#$log->debug("update state : " . Dumper($state));10261027my$last_dirname="///";10281029# foreach file specified on the command line ...1030foreachmy$filename( @{$state->{args}} )1031{1032$filename= filecleanup($filename);10331034$log->debug("Processing file$filename");10351036unless($state->{globaloptions}{-Q} ||$state->{globaloptions}{-q} )1037{1038my$cur_dirname= dirname($filename);1039if($cur_dirnamene$last_dirname)1040{1041$last_dirname=$cur_dirname;1042if($cur_dirnameeq"")1043{1044$cur_dirname=".";1045}1046print"E cvs update: Updating$cur_dirname\n";1047}1048}10491050# if we have a -C we should pretend we never saw modified stuff1051if(exists($state->{opt}{C} ) )1052{1053delete$state->{entries}{$filename}{modified_hash};1054delete$state->{entries}{$filename}{modified_filename};1055$state->{entries}{$filename}{unchanged} =1;1056}10571058my$meta;1059if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/)1060{1061$meta=$updater->getmeta($filename,$1);1062}else{1063$meta=$updater->getmeta($filename);1064}10651066# If -p was given, "print" the contents of the requested revision.1067if(exists($state->{opt}{p} ) ) {1068if(defined($meta->{revision} ) ) {1069$log->info("Printing '$filename' revision ".$meta->{revision});10701071 transmitfile($meta->{filehash}, {print=>1});1072}10731074next;1075}10761077if( !defined$meta)1078{1079$meta= {1080 name =>$filename,1081 revision =>0,1082 filehash =>'added'1083};1084}10851086my$oldmeta=$meta;10871088my$wrev= revparse($filename);10891090# If the working copy is an old revision, lets get that version too for comparison.1091if(defined($wrev)and$wrev!=$meta->{revision} )1092{1093$oldmeta=$updater->getmeta($filename,$wrev);1094}10951096#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");10971098# Files are up to date if the working copy and repo copy have the same revision,1099# and the working copy is unmodified _and_ the user hasn't specified -C1100next if(defined($wrev)1101and defined($meta->{revision})1102and$wrev==$meta->{revision}1103and$state->{entries}{$filename}{unchanged}1104and not exists($state->{opt}{C} ) );11051106# If the working copy and repo copy have the same revision,1107# but the working copy is modified, tell the client it's modified1108if(defined($wrev)1109and defined($meta->{revision})1110and$wrev==$meta->{revision}1111and defined($state->{entries}{$filename}{modified_hash})1112and not exists($state->{opt}{C} ) )1113{1114$log->info("Tell the client the file is modified");1115print"MT text M\n";1116print"MT fname$filename\n";1117print"MT newline\n";1118next;1119}11201121if($meta->{filehash}eq"deleted")1122{1123my($filepart,$dirpart) = filenamesplit($filename,1);11241125$log->info("Removing '$filename' from working copy (no longer in the repo)");11261127print"E cvs update: `$filename' is no longer in the repository\n";1128# Don't want to actually _DO_ the update if -n specified1129unless($state->{globaloptions}{-n} ) {1130print"Removed$dirpart\n";1131print"$filepart\n";1132}1133}1134elsif(not defined($state->{entries}{$filename}{modified_hash} )1135or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1136or$meta->{filehash}eq'added')1137{1138# normal update, just send the new revision (either U=Update,1139# or A=Add, or R=Remove)1140if(defined($wrev) &&$wrev<0)1141{1142$log->info("Tell the client the file is scheduled for removal");1143print"MT text R\n";1144print"MT fname$filename\n";1145print"MT newline\n";1146next;1147}1148elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) )1149{1150$log->info("Tell the client the file is scheduled for addition");1151print"MT text A\n";1152print"MT fname$filename\n";1153print"MT newline\n";1154next;11551156}1157else{1158$log->info("Updating '$filename' to ".$meta->{revision});1159print"MT +updated\n";1160print"MT text U\n";1161print"MT fname$filename\n";1162print"MT newline\n";1163print"MT -updated\n";1164}11651166my($filepart,$dirpart) = filenamesplit($filename,1);11671168# Don't want to actually _DO_ the update if -n specified1169unless($state->{globaloptions}{-n} )1170{1171if(defined($wrev) )1172{1173# instruct client we're sending a file to put in this path as a replacement1174print"Update-existing$dirpart\n";1175$log->debug("Updating existing file 'Update-existing$dirpart'");1176}else{1177# instruct client we're sending a file to put in this path as a new file1178print"Clear-static-directory$dirpart\n";1179print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1180print"Clear-sticky$dirpart\n";1181print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";11821183$log->debug("Creating new file 'Created$dirpart'");1184print"Created$dirpart\n";1185}1186print$state->{CVSROOT} ."/$state->{module}/$filename\n";11871188# this is an "entries" line1189my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1190$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1191print"/$filepart/1.$meta->{revision}//$kopts/\n";11921193# permissions1194$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1195print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";11961197# transmit file1198 transmitfile($meta->{filehash});1199}1200}else{1201$log->info("Updating '$filename'");1202my($filepart,$dirpart) = filenamesplit($meta->{name},1);12031204my$mergeDir= setupTmpDir();12051206my$file_local=$filepart.".mine";1207my$mergedFile="$mergeDir/$file_local";1208system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1209my$file_old=$filepart.".".$oldmeta->{revision};1210 transmitfile($oldmeta->{filehash}, { targetfile =>$file_old});1211my$file_new=$filepart.".".$meta->{revision};1212 transmitfile($meta->{filehash}, { targetfile =>$file_new});12131214# we need to merge with the local changes ( M=successful merge, C=conflict merge )1215$log->info("Merging$file_local,$file_old,$file_new");1216print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n";12171218$log->debug("Temporary directory for merge is$mergeDir");12191220my$return=system("git","merge-file",$file_local,$file_old,$file_new);1221$return>>=8;12221223 cleanupTmpDir();12241225if($return==0)1226{1227$log->info("Merged successfully");1228print"M M$filename\n";1229$log->debug("Merged$dirpart");12301231# Don't want to actually _DO_ the update if -n specified1232unless($state->{globaloptions}{-n} )1233{1234print"Merged$dirpart\n";1235$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1236print$state->{CVSROOT} ."/$state->{module}/$filename\n";1237my$kopts= kopts_from_path("$dirpart/$filepart",1238"file",$mergedFile);1239$log->debug("/$filepart/1.$meta->{revision}//$kopts/");1240print"/$filepart/1.$meta->{revision}//$kopts/\n";1241}1242}1243elsif($return==1)1244{1245$log->info("Merged with conflicts");1246print"E cvs update: conflicts found in$filename\n";1247print"M C$filename\n";12481249# Don't want to actually _DO_ the update if -n specified1250unless($state->{globaloptions}{-n} )1251{1252print"Merged$dirpart\n";1253print$state->{CVSROOT} ."/$state->{module}/$filename\n";1254my$kopts= kopts_from_path("$dirpart/$filepart",1255"file",$mergedFile);1256print"/$filepart/1.$meta->{revision}/+/$kopts/\n";1257}1258}1259else1260{1261$log->warn("Merge failed");1262next;1263}12641265# Don't want to actually _DO_ the update if -n specified1266unless($state->{globaloptions}{-n} )1267{1268# permissions1269$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1270print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";12711272# transmit file, format is single integer on a line by itself (file1273# size) followed by the file contents1274# TODO : we should copy files in blocks1275my$data=`cat$mergedFile`;1276$log->debug("File size : " . length($data));1277 print length($data) . "\n";1278 print$data;1279 }1280 }12811282 }12831284 print "ok\n";1285}12861287sub req_ci1288{1289 my ($cmd,$data) =@_;12901291 argsplit("ci");12921293 #$log->debug("State : " . Dumper($state));12941295$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));12961297 if ($state->{method} eq 'pserver' and$state->{user} eq 'anonymous' )1298 {1299 print "error 1 anonymous user cannot commit via pserver\n";1300 cleanupWorkTree();1301 exit;1302 }13031304 if ( -e$state->{CVSROOT} . "/index" )1305 {1306$log->warn("file 'index' already exists in the git repository");1307 print "error 1 Index already exists in git repo\n";1308 cleanupWorkTree();1309 exit;1310 }13111312 # Grab a handle to the SQLite db and do any necessary updates1313 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1314$updater->update();13151316 # Remember where the head was at the beginning.1317 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1318 chomp$parenthash;1319 if ($parenthash!~ /^[0-9a-f]{40}$/) {1320 print "error 1 pserver cannot find the current HEAD of module";1321 cleanupWorkTree();1322 exit;1323 }13241325 setupWorkTree($parenthash);13261327$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");13281329$log->info("Created index '$work->{index}' for head$state->{module} - exit status$?");13301331 my@committedfiles= ();1332 my%oldmeta;13331334 # foreach file specified on the command line ...1335 foreach my$filename( @{$state->{args}} )1336 {1337 my$committedfile=$filename;1338$filename= filecleanup($filename);13391340 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );13411342 my$meta=$updater->getmeta($filename);1343$oldmeta{$filename} =$meta;13441345 my$wrev= revparse($filename);13461347 my ($filepart,$dirpart) = filenamesplit($filename);13481349 # do a checkout of the file if it is part of this tree1350 if ($wrev) {1351 system('git', 'checkout-index', '-f', '-u',$filename);1352 unless ($?== 0) {1353 die "Error running git-checkout-index -f -u$filename:$!";1354 }1355 }13561357 my$addflag= 0;1358 my$rmflag= 0;1359$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1360$addflag= 1 unless ( -e$filename);13611362 # Do up to date checking1363 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1364 {1365 # fail everything if an up to date check fails1366 print "error 1 Up to date check failed for$filename\n";1367 cleanupWorkTree();1368 exit;1369 }13701371 push@committedfiles,$committedfile;1372$log->info("Committing$filename");13731374 system("mkdir","-p",$dirpart) unless ( -d$dirpart);13751376 unless ($rmflag)1377 {1378$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1379 rename$state->{entries}{$filename}{modified_filename},$filename;13801381 # Calculate modes to remove1382 my$invmode= "";1383 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }13841385$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1386 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1387 }13881389 if ($rmflag)1390 {1391$log->info("Removing file '$filename'");1392 unlink($filename);1393 system("git", "update-index", "--remove",$filename);1394 }1395 elsif ($addflag)1396 {1397$log->info("Adding file '$filename'");1398 system("git", "update-index", "--add",$filename);1399 } else {1400$log->info("Updating file '$filename'");1401 system("git", "update-index",$filename);1402 }1403 }14041405 unless ( scalar(@committedfiles) > 0 )1406 {1407 print "E No files to commit\n";1408 print "ok\n";1409 cleanupWorkTree();1410 return;1411 }14121413 my$treehash= `git write-tree`;1414 chomp$treehash;14151416$log->debug("Treehash :$treehash, Parenthash :$parenthash");14171418 # write our commit message out if we have one ...1419 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1420 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1421 if ( defined ($cfg->{gitcvs}{commitmsgannotation} ) ) {1422 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/) {1423 print$msg_fh"\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"1424 }1425 } else {1426 print$msg_fh"\n\nvia git-CVS emulator\n";1427 }1428 close$msg_fh;14291430 my$commithash= `git commit-tree $treehash-p $parenthash<$msg_filename`;1431chomp($commithash);1432$log->info("Commit hash :$commithash");14331434unless($commithash=~/[a-zA-Z0-9]{40}/)1435{1436$log->warn("Commit failed (Invalid commit hash)");1437print"error 1 Commit failed (unknown reason)\n";1438 cleanupWorkTree();1439exit;1440}14411442### Emulate git-receive-pack by running hooks/update1443my@hook= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1444$parenthash,$commithash);1445if( -x $hook[0] ) {1446unless(system(@hook) ==0)1447{1448$log->warn("Commit failed (update hook declined to update ref)");1449print"error 1 Commit failed (update hook declined)\n";1450 cleanupWorkTree();1451exit;1452}1453}14541455### Update the ref1456if(system(qw(git update-ref -m),"cvsserver ci",1457"refs/heads/$state->{module}",$commithash,$parenthash)) {1458$log->warn("update-ref for$state->{module} failed.");1459print"error 1 Cannot commit -- update first\n";1460 cleanupWorkTree();1461exit;1462}14631464### Emulate git-receive-pack by running hooks/post-receive1465my$hook=$ENV{GIT_DIR}.'hooks/post-receive';1466if( -x $hook) {1467open(my$pipe,"|$hook") ||die"can't fork$!";14681469local$SIG{PIPE} =sub{die'pipe broke'};14701471print$pipe"$parenthash$commithashrefs/heads/$state->{module}\n";14721473close$pipe||die"bad pipe:$!$?";1474}14751476$updater->update();14771478### Then hooks/post-update1479$hook=$ENV{GIT_DIR}.'hooks/post-update';1480if(-x $hook) {1481system($hook,"refs/heads/$state->{module}");1482}14831484# foreach file specified on the command line ...1485foreachmy$filename(@committedfiles)1486{1487$filename= filecleanup($filename);14881489my$meta=$updater->getmeta($filename);1490unless(defined$meta->{revision}) {1491$meta->{revision} =1;1492}14931494my($filepart,$dirpart) = filenamesplit($filename,1);14951496$log->debug("Checked-in$dirpart:$filename");14971498print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1499if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1500{1501print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1502print"Remove-entry$dirpart\n";1503print"$filename\n";1504}else{1505if($meta->{revision} ==1) {1506print"M initial revision: 1.1\n";1507}else{1508print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1509}1510print"Checked-in$dirpart\n";1511print"$filename\n";1512my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1513print"/$filepart/1.$meta->{revision}//$kopts/\n";1514}1515}15161517 cleanupWorkTree();1518print"ok\n";1519}15201521sub req_status1522{1523my($cmd,$data) =@_;15241525 argsplit("status");15261527$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1528#$log->debug("status state : " . Dumper($state));15291530# Grab a handle to the SQLite db and do any necessary updates1531my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1532$updater->update();15331534# if no files were specified, we need to work out what files we should be providing status on ...1535 argsfromdir($updater);15361537# foreach file specified on the command line ...1538foreachmy$filename( @{$state->{args}} )1539{1540$filename= filecleanup($filename);15411542next ifexists($state->{opt}{l}) &&index($filename,'/',length($state->{prependdir})) >=0;15431544my$meta=$updater->getmeta($filename);1545my$oldmeta=$meta;15461547my$wrev= revparse($filename);15481549# If the working copy is an old revision, lets get that version too for comparison.1550if(defined($wrev)and$wrev!=$meta->{revision} )1551{1552$oldmeta=$updater->getmeta($filename,$wrev);1553}15541555# TODO : All possible statuses aren't yet implemented1556my$status;1557# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1558$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1559and1560( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1561or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1562);15631564# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1565$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1566and1567($state->{entries}{$filename}{unchanged}1568or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1569);15701571# Need checkout if it exists in the repo but doesn't have a working copy1572$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );15731574# Locally modified if working copy and repo copy have the same revision but there are local changes1575$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );15761577# Needs Merge if working copy revision is less than repo copy and there are local changes1578$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );15791580$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1581$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1582$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1583$status||="File had conflicts on merge"if(0);15841585$status||="Unknown";15861587my($filepart) = filenamesplit($filename);15881589print"M ===================================================================\n";1590print"M File:$filepart\tStatus:$status\n";1591if(defined($state->{entries}{$filename}{revision}) )1592{1593print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1594}else{1595print"M Working revision:\tNo entry for$filename\n";1596}1597if(defined($meta->{revision}) )1598{1599print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1600print"M Sticky Tag:\t\t(none)\n";1601print"M Sticky Date:\t\t(none)\n";1602print"M Sticky Options:\t\t(none)\n";1603}else{1604print"M Repository revision:\tNo revision control file\n";1605}1606print"M\n";1607}16081609print"ok\n";1610}16111612sub req_diff1613{1614my($cmd,$data) =@_;16151616 argsplit("diff");16171618$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1619#$log->debug("status state : " . Dumper($state));16201621my($revision1,$revision2);1622if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1623{1624$revision1=$state->{opt}{r}[0];1625$revision2=$state->{opt}{r}[1];1626}else{1627$revision1=$state->{opt}{r};1628}16291630$revision1=~s/^1\.//if(defined($revision1) );1631$revision2=~s/^1\.//if(defined($revision2) );16321633$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );16341635# Grab a handle to the SQLite db and do any necessary updates1636my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1637$updater->update();16381639# if no files were specified, we need to work out what files we should be providing status on ...1640 argsfromdir($updater);16411642# foreach file specified on the command line ...1643foreachmy$filename( @{$state->{args}} )1644{1645$filename= filecleanup($filename);16461647my($fh,$file1,$file2,$meta1,$meta2,$filediff);16481649my$wrev= revparse($filename);16501651# We need _something_ to diff against1652next unless(defined($wrev) );16531654# if we have a -r switch, use it1655if(defined($revision1) )1656{1657(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1658$meta1=$updater->getmeta($filename,$revision1);1659unless(defined($meta1)and$meta1->{filehash}ne"deleted")1660{1661print"E File$filenameat revision 1.$revision1doesn't exist\n";1662next;1663}1664 transmitfile($meta1->{filehash}, { targetfile =>$file1});1665}1666# otherwise we just use the working copy revision1667else1668{1669(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1670$meta1=$updater->getmeta($filename,$wrev);1671 transmitfile($meta1->{filehash}, { targetfile =>$file1});1672}16731674# if we have a second -r switch, use it too1675if(defined($revision2) )1676{1677(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1678$meta2=$updater->getmeta($filename,$revision2);16791680unless(defined($meta2)and$meta2->{filehash}ne"deleted")1681{1682print"E File$filenameat revision 1.$revision2doesn't exist\n";1683next;1684}16851686 transmitfile($meta2->{filehash}, { targetfile =>$file2});1687}1688# otherwise we just use the working copy1689else1690{1691$file2=$state->{entries}{$filename}{modified_filename};1692}16931694# if we have been given -r, and we don't have a $file2 yet, lets get one1695if(defined($revision1)and not defined($file2) )1696{1697(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1698$meta2=$updater->getmeta($filename,$wrev);1699 transmitfile($meta2->{filehash}, { targetfile =>$file2});1700}17011702# We need to have retrieved something useful1703next unless(defined($meta1) );17041705# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1706next if(not defined($meta2)and$wrev==$meta1->{revision}1707and1708( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1709or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1710);17111712# Apparently we only show diffs for locally modified files1713next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );17141715print"M Index:$filename\n";1716print"M ===================================================================\n";1717print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1718print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1719print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1720print"M diff ";1721foreachmy$opt(keys%{$state->{opt}} )1722{1723if(ref$state->{opt}{$opt}eq"ARRAY")1724{1725foreachmy$value( @{$state->{opt}{$opt}} )1726{1727print"-$opt$value";1728}1729}else{1730print"-$opt";1731print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1732}1733}1734print"$filename\n";17351736$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));17371738($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);17391740if(exists$state->{opt}{u} )1741{1742system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1743}else{1744system("diff$file1$file2>$filediff");1745}17461747while( <$fh> )1748{1749print"M$_";1750}1751close$fh;1752}17531754print"ok\n";1755}17561757sub req_log1758{1759my($cmd,$data) =@_;17601761 argsplit("log");17621763$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1764#$log->debug("log state : " . Dumper($state));17651766my($minrev,$maxrev);1767if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1768{1769my$control=$2;1770$minrev=$1;1771$maxrev=$3;1772$minrev=~s/^1\.//if(defined($minrev) );1773$maxrev=~s/^1\.//if(defined($maxrev) );1774$minrev++if(defined($minrev)and$controleq"::");1775}17761777# Grab a handle to the SQLite db and do any necessary updates1778my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1779$updater->update();17801781# if no files were specified, we need to work out what files we should be providing status on ...1782 argsfromdir($updater);17831784# foreach file specified on the command line ...1785foreachmy$filename( @{$state->{args}} )1786{1787$filename= filecleanup($filename);17881789my$headmeta=$updater->getmeta($filename);17901791my$revisions=$updater->getlog($filename);1792my$totalrevisions=scalar(@$revisions);17931794if(defined($minrev) )1795{1796$log->debug("Removing revisions less than$minrev");1797while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1798{1799pop@$revisions;1800}1801}1802if(defined($maxrev) )1803{1804$log->debug("Removing revisions greater than$maxrev");1805while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1806{1807shift@$revisions;1808}1809}18101811next unless(scalar(@$revisions) );18121813print"M\n";1814print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1815print"M Working file:$filename\n";1816print"M head: 1.$headmeta->{revision}\n";1817print"M branch:\n";1818print"M locks: strict\n";1819print"M access list:\n";1820print"M symbolic names:\n";1821print"M keyword substitution: kv\n";1822print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1823print"M description:\n";18241825foreachmy$revision(@$revisions)1826{1827print"M ----------------------------\n";1828print"M revision 1.$revision->{revision}\n";1829# reformat the date for log output1830$revision->{modified} =sprintf('%04d/%02d/%02d%s',$3,$DATE_LIST->{$2},$1,$4)if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and defined($DATE_LIST->{$2}) );1831$revision->{author} = cvs_author($revision->{author});1832print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1833my$commitmessage=$updater->commitmessage($revision->{commithash});1834$commitmessage=~s/^/M /mg;1835print$commitmessage."\n";1836}1837print"M =============================================================================\n";1838}18391840print"ok\n";1841}18421843sub req_annotate1844{1845my($cmd,$data) =@_;18461847 argsplit("annotate");18481849$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1850#$log->debug("status state : " . Dumper($state));18511852# Grab a handle to the SQLite db and do any necessary updates1853my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1854$updater->update();18551856# if no files were specified, we need to work out what files we should be providing annotate on ...1857 argsfromdir($updater);18581859# we'll need a temporary checkout dir1860 setupWorkTree();18611862$log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");18631864# foreach file specified on the command line ...1865foreachmy$filename( @{$state->{args}} )1866{1867$filename= filecleanup($filename);18681869my$meta=$updater->getmeta($filename);18701871next unless($meta->{revision} );18721873# get all the commits that this file was in1874# in dense format -- aka skip dead revisions1875my$revisions=$updater->gethistorydense($filename);1876my$lastseenin=$revisions->[0][2];18771878# populate the temporary index based on the latest commit were we saw1879# the file -- but do it cheaply without checking out any files1880# TODO: if we got a revision from the client, use that instead1881# to look up the commithash in sqlite (still good to default to1882# the current head as we do now)1883system("git","read-tree",$lastseenin);1884unless($?==0)1885{1886print"E error running git-read-tree$lastseenin$ENV{GIT_INDEX_FILE}$!\n";1887return;1888}1889$log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit$lastseenin- exit status$?");18901891# do a checkout of the file1892system('git','checkout-index','-f','-u',$filename);1893unless($?==0) {1894print"E error running git-checkout-index -f -u$filename:$!\n";1895return;1896}18971898$log->info("Annotate$filename");18991900# Prepare a file with the commits from the linearized1901# history that annotate should know about. This prevents1902# git-jsannotate telling us about commits we are hiding1903# from the client.19041905my$a_hints="$work->{workDir}/.annotate_hints";1906if(!open(ANNOTATEHINTS,'>',$a_hints)) {1907print"E failed to open '$a_hints' for writing:$!\n";1908return;1909}1910for(my$i=0;$i<@$revisions;$i++)1911{1912print ANNOTATEHINTS $revisions->[$i][2];1913if($i+1<@$revisions) {# have we got a parent?1914print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1915}1916print ANNOTATEHINTS "\n";1917}19181919print ANNOTATEHINTS "\n";1920close ANNOTATEHINTS1921or(print"E failed to write$a_hints:$!\n"),return;19221923my@cmd= (qw(git annotate -l -S),$a_hints,$filename);1924if(!open(ANNOTATE,"-|",@cmd)) {1925print"E error invoking ".join(' ',@cmd) .":$!\n";1926return;1927}1928my$metadata= {};1929print"E Annotations for$filename\n";1930print"E ***************\n";1931while( <ANNOTATE> )1932{1933if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1934{1935my$commithash=$1;1936my$data=$2;1937unless(defined($metadata->{$commithash} ) )1938{1939$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1940$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});1941$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1942}1943printf("M 1.%-5d (%-8s%10s):%s\n",1944$metadata->{$commithash}{revision},1945$metadata->{$commithash}{author},1946$metadata->{$commithash}{modified},1947$data1948);1949}else{1950$log->warn("Error in annotate output! LINE:$_");1951print"E Annotate error\n";1952next;1953}1954}1955close ANNOTATE;1956}19571958# done; get out of the tempdir1959 cleanupWorkTree();19601961print"ok\n";19621963}19641965# This method takes the state->{arguments} array and produces two new arrays.1966# The first is $state->{args} which is everything before the '--' argument, and1967# the second is $state->{files} which is everything after it.1968sub argsplit1969{1970$state->{args} = [];1971$state->{files} = [];1972$state->{opt} = {};19731974return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");19751976my$type=shift;19771978if(defined($type) )1979{1980my$opt= {};1981$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");1982$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1983$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");1984$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1985$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1986$opt= { k =>1, m =>1}if($typeeq"add");1987$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1988$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");198919901991while(scalar( @{$state->{arguments}} ) >0)1992{1993my$arg=shift@{$state->{arguments}};19941995next if($argeq"--");1996next unless($arg=~/\S/);19971998# if the argument looks like a switch1999if($arg=~/^-(\w)(.*)/)2000{2001# if it's a switch that takes an argument2002if($opt->{$1} )2003{2004# If this switch has already been provided2005if($opt->{$1} >1and exists($state->{opt}{$1} ) )2006{2007$state->{opt}{$1} = [$state->{opt}{$1} ];2008if(length($2) >0)2009{2010push@{$state->{opt}{$1}},$2;2011}else{2012push@{$state->{opt}{$1}},shift@{$state->{arguments}};2013}2014}else{2015# if there's extra data in the arg, use that as the argument for the switch2016if(length($2) >0)2017{2018$state->{opt}{$1} =$2;2019}else{2020$state->{opt}{$1} =shift@{$state->{arguments}};2021}2022}2023}else{2024$state->{opt}{$1} =undef;2025}2026}2027else2028{2029push@{$state->{args}},$arg;2030}2031}2032}2033else2034{2035my$mode=0;20362037foreachmy$value( @{$state->{arguments}} )2038{2039if($valueeq"--")2040{2041$mode++;2042next;2043}2044push@{$state->{args}},$valueif($mode==0);2045push@{$state->{files}},$valueif($mode==1);2046}2047}2048}20492050# This method uses $state->{directory} to populate $state->{args} with a list of filenames2051sub argsfromdir2052{2053my$updater=shift;20542055$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");20562057return if(scalar( @{$state->{args}} ) >1);20582059my@gethead= @{$updater->gethead};20602061# push added files2062foreachmy$file(keys%{$state->{entries}}) {2063if(exists$state->{entries}{$file}{revision} &&2064$state->{entries}{$file}{revision} ==0)2065{2066push@gethead, { name =>$file, filehash =>'added'};2067}2068}20692070if(scalar(@{$state->{args}}) ==1)2071{2072my$arg=$state->{args}[0];2073$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );20742075$log->info("Only one arg specified, checking for directory expansion on '$arg'");20762077foreachmy$file(@gethead)2078{2079next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2080next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);2081push@{$state->{args}},$file->{name};2082}20832084shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);2085}else{2086$log->info("Only one arg specified, populating file list automatically");20872088$state->{args} = [];20892090foreachmy$file(@gethead)2091{2092next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2093next unless($file->{name} =~s/^$state->{prependdir}//);2094push@{$state->{args}},$file->{name};2095}2096}2097}20982099# This method cleans up the $state variable after a command that uses arguments has run2100sub statecleanup2101{2102$state->{files} = [];2103$state->{args} = [];2104$state->{arguments} = [];2105$state->{entries} = {};2106}21072108sub revparse2109{2110my$filename=shift;21112112returnundefunless(defined($state->{entries}{$filename}{revision} ) );21132114return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);2115return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);21162117returnundef;2118}21192120# This method takes a file hash and does a CVS "file transfer". Its2121# exact behaviour depends on a second, optional hash table argument:2122# - If $options->{targetfile}, dump the contents to that file;2123# - If $options->{print}, use M/MT to transmit the contents one line2124# at a time;2125# - Otherwise, transmit the size of the file, followed by the file2126# contents.2127sub transmitfile2128{2129my$filehash=shift;2130my$options=shift;21312132if(defined($filehash)and$filehasheq"deleted")2133{2134$log->warn("filehash is 'deleted'");2135return;2136}21372138die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);21392140my$type=`git cat-file -t$filehash`;2141 chomp$type;21422143 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );21442145 my$size= `git cat-file -s $filehash`;2146chomp$size;21472148$log->debug("transmitfile($filehash) size=$size, type=$type");21492150if(open my$fh,'-|',"git","cat-file","blob",$filehash)2151{2152if(defined($options->{targetfile} ) )2153{2154my$targetfile=$options->{targetfile};2155open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2156print NEWFILE $_while( <$fh> );2157close NEWFILE or die("Failed to write '$targetfile':$!");2158}elsif(defined($options->{print} ) &&$options->{print} ) {2159while( <$fh> ) {2160if(/\n\z/) {2161print'M ',$_;2162}else{2163print'MT text ',$_,"\n";2164}2165}2166}else{2167print"$size\n";2168printwhile( <$fh> );2169}2170close$fhor die("Couldn't close filehandle for transmitfile():$!");2171}else{2172die("Couldn't execute git-cat-file");2173}2174}21752176# This method takes a file name, and returns ( $dirpart, $filepart ) which2177# refers to the directory portion and the file portion of the filename2178# respectively2179sub filenamesplit2180{2181my$filename=shift;2182my$fixforlocaldir=shift;21832184my($filepart,$dirpart) = ($filename,".");2185($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2186$dirpart.="/";21872188if($fixforlocaldir)2189{2190$dirpart=~s/^$state->{prependdir}//;2191}21922193return($filepart,$dirpart);2194}21952196sub filecleanup2197{2198my$filename=shift;21992200returnundefunless(defined($filename));2201if($filename=~/^\// )2202{2203print"E absolute filenames '$filename' not supported by server\n";2204returnundef;2205}22062207$filename=~s/^\.\///g;2208$filename=$state->{prependdir} .$filename;2209return$filename;2210}22112212sub validateGitDir2213{2214if( !defined($state->{CVSROOT}) )2215{2216print"error 1 CVSROOT not specified\n";2217 cleanupWorkTree();2218exit;2219}2220if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2221{2222print"error 1 Internally inconsistent CVSROOT\n";2223 cleanupWorkTree();2224exit;2225}2226}22272228# Setup working directory in a work tree with the requested version2229# loaded in the index.2230sub setupWorkTree2231{2232my($ver) =@_;22332234 validateGitDir();22352236if( (defined($work->{state}) &&$work->{state} !=1) ||2237defined($work->{tmpDir}) )2238{2239$log->warn("Bad work tree state management");2240print"error 1 Internal setup multiple work trees without cleanup\n";2241 cleanupWorkTree();2242exit;2243}22442245$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);22462247if( !defined($work->{index}) )2248{2249(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2250}22512252chdir$work->{workDir}or2253die"Unable to chdir to$work->{workDir}\n";22542255$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");22562257$ENV{GIT_WORK_TREE} =".";2258$ENV{GIT_INDEX_FILE} =$work->{index};2259$work->{state} =2;22602261if($ver)2262{2263system("git","read-tree",$ver);2264unless($?==0)2265{2266$log->warn("Error running git-read-tree");2267die"Error running git-read-tree$verin$work->{workDir}$!\n";2268}2269}2270# else # req_annotate reads tree for each file2271}22722273# Ensure current directory is in some kind of working directory,2274# with a recent version loaded in the index.2275sub ensureWorkTree2276{2277if(defined($work->{tmpDir}) )2278{2279$log->warn("Bad work tree state management [ensureWorkTree()]");2280print"error 1 Internal setup multiple dirs without cleanup\n";2281 cleanupWorkTree();2282exit;2283}2284if($work->{state} )2285{2286return;2287}22882289 validateGitDir();22902291if( !defined($work->{emptyDir}) )2292{2293$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2294}2295chdir$work->{emptyDir}or2296die"Unable to chdir to$work->{emptyDir}\n";22972298my$ver=`git show-ref -s refs/heads/$state->{module}`;2299chomp$ver;2300if($ver!~/^[0-9a-f]{40}$/)2301{2302$log->warn("Error from git show-ref -s refs/head$state->{module}");2303print"error 1 cannot find the current HEAD of module";2304 cleanupWorkTree();2305exit;2306}23072308if( !defined($work->{index}) )2309{2310(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2311}23122313$ENV{GIT_WORK_TREE} =".";2314$ENV{GIT_INDEX_FILE} =$work->{index};2315$work->{state} =1;23162317system("git","read-tree",$ver);2318unless($?==0)2319{2320die"Error running git-read-tree$ver$!\n";2321}2322}23232324# Cleanup working directory that is not needed any longer.2325sub cleanupWorkTree2326{2327if( !$work->{state} )2328{2329return;2330}23312332chdir"/"or die"Unable to chdir '/'\n";23332334if(defined($work->{workDir}) )2335{2336 rmtree($work->{workDir} );2337undef$work->{workDir};2338}2339undef$work->{state};2340}23412342# Setup a temporary directory (not a working tree), typically for2343# merging dirty state as in req_update.2344sub setupTmpDir2345{2346$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2347chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";23482349return$work->{tmpDir};2350}23512352# Clean up a previously setupTmpDir. Restore previous work tree if2353# appropriate.2354sub cleanupTmpDir2355{2356if( !defined($work->{tmpDir}) )2357{2358$log->warn("cleanup tmpdir that has not been setup");2359die"Cleanup tmpDir that has not been setup\n";2360}2361if(defined($work->{state}) )2362{2363if($work->{state} ==1)2364{2365chdir$work->{emptyDir}or2366die"Unable to chdir to$work->{emptyDir}\n";2367}2368elsif($work->{state} ==2)2369{2370chdir$work->{workDir}or2371die"Unable to chdir to$work->{emptyDir}\n";2372}2373else2374{2375$log->warn("Inconsistent work dir state");2376die"Inconsistent work dir state\n";2377}2378}2379else2380{2381chdir"/"or die"Unable to chdir '/'\n";2382}2383}23842385# Given a path, this function returns a string containing the kopts2386# that should go into that path's Entries line. For example, a binary2387# file should get -kb.2388sub kopts_from_path2389{2390my($path,$srcType,$name) =@_;23912392if(defined($cfg->{gitcvs}{usecrlfattr} )and2393$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2394{2395my($val) = check_attr("crlf",$path);2396if($valeq"set")2397{2398return"";2399}2400elsif($valeq"unset")2401{2402return"-kb"2403}2404else2405{2406$log->info("Unrecognized check_attr crlf$path:$val");2407}2408}24092410if(defined($cfg->{gitcvs}{allbinary} ) )2411{2412if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2413{2414return"-kb";2415}2416elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2417{2418if($srcTypeeq"sha1Or-k"&&2419!defined($name) )2420{2421my($ret)=$state->{entries}{$path}{options};2422if( !defined($ret) )2423{2424$ret=$state->{opt}{k};2425if(defined($ret))2426{2427$ret="-k$ret";2428}2429else2430{2431$ret="";2432}2433}2434if( ! ($ret=~/^(|-kb|-kkv|-kkvl|-kk|-ko|-kv)$/) )2435{2436print"E Bad -k option\n";2437$log->warn("Bad -k option:$ret");2438die"Error: Bad -k option:$ret\n";2439}24402441return$ret;2442}2443else2444{2445if( is_binary($srcType,$name) )2446{2447$log->debug("... as binary");2448return"-kb";2449}2450else2451{2452$log->debug("... as text");2453}2454}2455}2456}2457# Return "" to give no special treatment to any path2458return"";2459}24602461sub check_attr2462{2463my($attr,$path) =@_;2464 ensureWorkTree();2465if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2466{2467my$val= <$fh>;2468close$fh;2469$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2470return$val;2471}2472else2473{2474returnundef;2475}2476}24772478# This should have the same heuristics as convert.c:is_binary() and related.2479# Note that the bare CR test is done by callers in convert.c.2480sub is_binary2481{2482my($srcType,$name) =@_;2483$log->debug("is_binary($srcType,$name)");24842485# Minimize amount of interpreted code run in the inner per-character2486# loop for large files, by totalling each character value and2487# then analyzing the totals.2488my@counts;2489my$i;2490for($i=0;$i<256;$i++)2491{2492$counts[$i]=0;2493}24942495my$fh= open_blob_or_die($srcType,$name);2496my$line;2497while(defined($line=<$fh>) )2498{2499# Any '\0' and bare CR are considered binary.2500if($line=~/\0|(\r[^\n])/)2501{2502close($fh);2503return1;2504}25052506# Count up each character in the line:2507my$len=length($line);2508for($i=0;$i<$len;$i++)2509{2510$counts[ord(substr($line,$i,1))]++;2511}2512}2513close$fh;25142515# Don't count CR and LF as either printable/nonprintable2516$counts[ord("\n")]=0;2517$counts[ord("\r")]=0;25182519# Categorize individual character count into printable and nonprintable:2520my$printable=0;2521my$nonprintable=0;2522for($i=0;$i<256;$i++)2523{2524if($i<32&&2525$i!=ord("\b") &&2526$i!=ord("\t") &&2527$i!=033&&# ESC2528$i!=014)# FF2529{2530$nonprintable+=$counts[$i];2531}2532elsif($i==127)# DEL2533{2534$nonprintable+=$counts[$i];2535}2536else2537{2538$printable+=$counts[$i];2539}2540}25412542return($printable>>7) <$nonprintable;2543}25442545# Returns open file handle. Possible invocations:2546# - open_blob_or_die("file",$filename);2547# - open_blob_or_die("sha1",$filehash);2548sub open_blob_or_die2549{2550my($srcType,$name) =@_;2551my($fh);2552if($srcTypeeq"file")2553{2554if( !open$fh,"<",$name)2555{2556$log->warn("Unable to open file$name:$!");2557die"Unable to open file$name:$!\n";2558}2559}2560elsif($srcTypeeq"sha1"||$srcTypeeq"sha1Or-k")2561{2562unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2563{2564$log->warn("Need filehash");2565die"Need filehash\n";2566}25672568my$type=`git cat-file -t$name`;2569 chomp$type;25702571 unless ( defined ($type) and$typeeq "blob" )2572 {2573$log->warn("Invalid type '$type' for '$name'");2574 die ( "Invalid type '$type' (expected 'blob')" )2575 }25762577 my$size= `git cat-file -s $name`;2578chomp$size;25792580$log->debug("open_blob_or_die($name) size=$size, type=$type");25812582unless(open$fh,'-|',"git","cat-file","blob",$name)2583{2584$log->warn("Unable to open sha1$name");2585die"Unable to open sha1$name\n";2586}2587}2588else2589{2590$log->warn("Unknown type of blob source:$srcType");2591die"Unknown type of blob source:$srcType\n";2592}2593return$fh;2594}25952596# Generate a CVS author name from Git author information, by taking the local2597# part of the email address and replacing characters not in the Portable2598# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2599# Login names are Unix login names, which should be restricted to this2600# character set.2601sub cvs_author2602{2603my$author_line=shift;2604(my$author) =$author_line=~/<([^@>]*)/;26052606$author=~s/[^-a-zA-Z0-9_.]/_/g;2607$author=~s/^-/_/;26082609$author;2610}261126122613sub descramble2614{2615# This table is from src/scramble.c in the CVS source2616my@SHIFTS= (26170,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,261816,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2619114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2620111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,262141,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2622125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,262336,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,262458,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2625225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2626199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2627174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2628207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2629192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2630227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2631182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2632243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522633);2634my($str) =@_;26352636# This should never happen, the same password format (A) bas been2637# used by CVS since the beginning of time2638die"invalid password format$1"unlesssubstr($str,0,1)eq'A';26392640my@str=unpack"C*",substr($str,1);2641my$ret=join'',map{chr$SHIFTS[$_] }@str;2642return$ret;2643}264426452646package GITCVS::log;26472648####2649#### Copyright The Open University UK - 2006.2650####2651#### Authors: Martyn Smith <martyn@catalyst.net.nz>2652#### Martin Langhoff <martin@catalyst.net.nz>2653####2654####26552656use strict;2657use warnings;26582659=head1 NAME26602661GITCVS::log26622663=head1 DESCRIPTION26642665This module provides very crude logging with a similar interface to2666Log::Log4perl26672668=head1 METHODS26692670=cut26712672=head2 new26732674Creates a new log object, optionally you can specify a filename here to2675indicate the file to log to. If no log file is specified, you can specify one2676later with method setfile, or indicate you no longer want logging with method2677nofile.26782679Until one of these methods is called, all log calls will buffer messages ready2680to write out.26812682=cut2683sub new2684{2685my$class=shift;2686my$filename=shift;26872688my$self= {};26892690bless$self,$class;26912692if(defined($filename) )2693{2694open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2695}26962697return$self;2698}26992700=head2 setfile27012702This methods takes a filename, and attempts to open that file as the log file.2703If successful, all buffered data is written out to the file, and any further2704logging is written directly to the file.27052706=cut2707sub setfile2708{2709my$self=shift;2710my$filename=shift;27112712if(defined($filename) )2713{2714open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2715}27162717return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");27182719while(my$line=shift@{$self->{buffer}} )2720{2721print{$self->{fh}}$line;2722}2723}27242725=head2 nofile27262727This method indicates no logging is going to be used. It flushes any entries in2728the internal buffer, and sets a flag to ensure no further data is put there.27292730=cut2731sub nofile2732{2733my$self=shift;27342735$self->{nolog} =1;27362737return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");27382739$self->{buffer} = [];2740}27412742=head2 _logopen27432744Internal method. Returns true if the log file is open, false otherwise.27452746=cut2747sub _logopen2748{2749my$self=shift;27502751return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2752return0;2753}27542755=head2 debug info warn fatal27562757These four methods are wrappers to _log. They provide the actual interface for2758logging data.27592760=cut2761sub debug {my$self=shift;$self->_log("debug",@_); }2762sub info {my$self=shift;$self->_log("info",@_); }2763subwarn{my$self=shift;$self->_log("warn",@_); }2764sub fatal {my$self=shift;$self->_log("fatal",@_); }27652766=head2 _log27672768This is an internal method called by the logging functions. It generates a2769timestamp and pushes the logged line either to file, or internal buffer.27702771=cut2772sub _log2773{2774my$self=shift;2775my$level=shift;27762777return if($self->{nolog} );27782779my@time=localtime;2780my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2781$time[5] +1900,2782$time[4] +1,2783$time[3],2784$time[2],2785$time[1],2786$time[0],2787uc$level,2788);27892790if($self->_logopen)2791{2792print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2793}else{2794push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2795}2796}27972798=head2 DESTROY27992800This method simply closes the file handle if one is open28012802=cut2803sub DESTROY2804{2805my$self=shift;28062807if($self->_logopen)2808{2809close$self->{fh};2810}2811}28122813package GITCVS::updater;28142815####2816#### Copyright The Open University UK - 2006.2817####2818#### Authors: Martyn Smith <martyn@catalyst.net.nz>2819#### Martin Langhoff <martin@catalyst.net.nz>2820####2821####28222823use strict;2824use warnings;2825use DBI;28262827=head1 METHODS28282829=cut28302831=head2 new28322833=cut2834sub new2835{2836my$class=shift;2837my$config=shift;2838my$module=shift;2839my$log=shift;28402841die"Need to specify a git repository"unless(defined($config)and-d $config);2842die"Need to specify a module"unless(defined($module) );28432844$class=ref($class) ||$class;28452846my$self= {};28472848bless$self,$class;28492850$self->{valid_tables} = {'revision'=>1,2851'revision_ix1'=>1,2852'revision_ix2'=>1,2853'head'=>1,2854'head_ix1'=>1,2855'properties'=>1,2856'commitmsgs'=>1};28572858$self->{module} =$module;2859$self->{git_path} =$config."/";28602861$self->{log} =$log;28622863die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );28642865$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2866$cfg->{gitcvs}{dbdriver} ||"SQLite";2867$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2868$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2869$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2870$cfg->{gitcvs}{dbuser} ||"";2871$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2872$cfg->{gitcvs}{dbpass} ||"";2873$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||2874$cfg->{gitcvs}{dbtablenameprefix} ||"";2875my%mapping= ( m =>$module,2876 a =>$state->{method},2877 u =>getlogin||getpwuid($<) || $<,2878 G =>$self->{git_path},2879 g => mangle_dirname($self->{git_path}),2880);2881$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2882$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;2883$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;2884$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});28852886die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;2887die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;2888$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",2889$self->{dbuser},2890$self->{dbpass});2891die"Error connecting to database\n"unlessdefined$self->{dbh};28922893$self->{tables} = {};2894foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )2895{2896$self->{tables}{$table} =1;2897}28982899# Construct the revision table if required2900unless($self->{tables}{$self->tablename("revision")} )2901{2902my$tablename=$self->tablename("revision");2903my$ix1name=$self->tablename("revision_ix1");2904my$ix2name=$self->tablename("revision_ix2");2905$self->{dbh}->do("2906 CREATE TABLE$tablename(2907 name TEXT NOT NULL,2908 revision INTEGER NOT NULL,2909 filehash TEXT NOT NULL,2910 commithash TEXT NOT NULL,2911 author TEXT NOT NULL,2912 modified TEXT NOT NULL,2913 mode TEXT NOT NULL2914 )2915 ");2916$self->{dbh}->do("2917 CREATE INDEX$ix1name2918 ON$tablename(name,revision)2919 ");2920$self->{dbh}->do("2921 CREATE INDEX$ix2name2922 ON$tablename(name,commithash)2923 ");2924}29252926# Construct the head table if required2927unless($self->{tables}{$self->tablename("head")} )2928{2929my$tablename=$self->tablename("head");2930my$ix1name=$self->tablename("head_ix1");2931$self->{dbh}->do("2932 CREATE TABLE$tablename(2933 name TEXT NOT NULL,2934 revision INTEGER NOT NULL,2935 filehash TEXT NOT NULL,2936 commithash TEXT NOT NULL,2937 author TEXT NOT NULL,2938 modified TEXT NOT NULL,2939 mode TEXT NOT NULL2940 )2941 ");2942$self->{dbh}->do("2943 CREATE INDEX$ix1name2944 ON$tablename(name)2945 ");2946}29472948# Construct the properties table if required2949unless($self->{tables}{$self->tablename("properties")} )2950{2951my$tablename=$self->tablename("properties");2952$self->{dbh}->do("2953 CREATE TABLE$tablename(2954 key TEXT NOT NULL PRIMARY KEY,2955 value TEXT2956 )2957 ");2958}29592960# Construct the commitmsgs table if required2961unless($self->{tables}{$self->tablename("commitmsgs")} )2962{2963my$tablename=$self->tablename("commitmsgs");2964$self->{dbh}->do("2965 CREATE TABLE$tablename(2966 key TEXT NOT NULL PRIMARY KEY,2967 value TEXT2968 )2969 ");2970}29712972return$self;2973}29742975=head2 tablename29762977=cut2978sub tablename2979{2980my$self=shift;2981my$name=shift;29822983if(exists$self->{valid_tables}{$name}) {2984return$self->{dbtablenameprefix} .$name;2985}else{2986returnundef;2987}2988}29892990=head2 update29912992=cut2993sub update2994{2995my$self=shift;29962997# first lets get the commit list2998$ENV{GIT_DIR} =$self->{git_path};29993000my$commitsha1=`git rev-parse$self->{module}`;3001chomp$commitsha1;30023003my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3004unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3005{3006die("Invalid module '$self->{module}'");3007}300830093010my$git_log;3011my$lastcommit=$self->_get_prop("last_commit");30123013if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3014return1;3015}30163017# Start exclusive lock here...3018$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";30193020# TODO: log processing is memory bound3021# if we can parse into a 2nd file that is in reverse order3022# we can probably do something really efficient3023my@git_log_params= ('--pretty','--parents','--topo-order');30243025if(defined$lastcommit) {3026push@git_log_params,"$lastcommit..$self->{module}";3027}else{3028push@git_log_params,$self->{module};3029}3030# git-rev-list is the backend / plumbing version of git-log3031open(GITLOG,'-|','git','rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";30323033my@commits;30343035my%commit= ();30363037while( <GITLOG> )3038{3039chomp;3040if(m/^commit\s+(.*)$/) {3041# on ^commit lines put the just seen commit in the stack3042# and prime things for the next one3043if(keys%commit) {3044my%copy=%commit;3045unshift@commits, \%copy;3046%commit= ();3047}3048my@parents=split(m/\s+/,$1);3049$commit{hash} =shift@parents;3050$commit{parents} = \@parents;3051}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3052# on rfc822-like lines seen before we see any message,3053# lowercase the entry and put it in the hash as key-value3054$commit{lc($1)} =$2;3055}else{3056# message lines - skip initial empty line3057# and trim whitespace3058if(!exists($commit{message}) &&m/^\s*$/) {3059# define it to mark the end of headers3060$commit{message} ='';3061next;3062}3063s/^\s+//;s/\s+$//;# trim ws3064$commit{message} .=$_."\n";3065}3066}3067close GITLOG;30683069unshift@commits, \%commitif(keys%commit);30703071# Now all the commits are in the @commits bucket3072# ordered by time DESC. for each commit that needs processing,3073# determine whether it's following the last head we've seen or if3074# it's on its own branch, grab a file list, and add whatever's changed3075# NOTE: $lastcommit refers to the last commit from previous run3076# $lastpicked is the last commit we picked in this run3077my$lastpicked;3078my$head= {};3079if(defined$lastcommit) {3080$lastpicked=$lastcommit;3081}30823083my$committotal=scalar(@commits);3084my$commitcount=0;30853086# Load the head table into $head (for cached lookups during the update process)3087foreachmy$file( @{$self->gethead()} )3088{3089$head->{$file->{name}} =$file;3090}30913092foreachmy$commit(@commits)3093{3094$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3095if(defined$lastpicked)3096{3097if(!in_array($lastpicked, @{$commit->{parents}}))3098{3099# skip, we'll see this delta3100# as part of a merge later3101# warn "skipping off-track $commit->{hash}\n";3102next;3103}elsif(@{$commit->{parents}} >1) {3104# it is a merge commit, for each parent that is3105# not $lastpicked, see if we can get a log3106# from the merge-base to that parent to put it3107# in the message as a merge summary.3108my@parents= @{$commit->{parents}};3109foreachmy$parent(@parents) {3110# git-merge-base can potentially (but rarely) throw3111# several candidate merge bases. let's assume3112# that the first one is the best one.3113if($parenteq$lastpicked) {3114next;3115}3116my$base=eval{3117 safe_pipe_capture('git','merge-base',3118$lastpicked,$parent);3119};3120# The two branches may not be related at all,3121# in which case merge base simply fails to find3122# any, but that's Ok.3123next if($@);31243125chomp$base;3126if($base) {3127my@merged;3128# print "want to log between $base $parent \n";3129open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3130or die"Cannot call git-log:$!";3131my$mergedhash;3132while(<GITLOG>) {3133chomp;3134if(!defined$mergedhash) {3135if(m/^commit\s+(.+)$/) {3136$mergedhash=$1;3137}else{3138next;3139}3140}else{3141# grab the first line that looks non-rfc8223142# aka has content after leading space3143if(m/^\s+(\S.*)$/) {3144my$title=$1;3145$title=substr($title,0,100);# truncate3146unshift@merged,"$mergedhash$title";3147undef$mergedhash;3148}3149}3150}3151close GITLOG;3152if(@merged) {3153$commit->{mergemsg} =$commit->{message};3154$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3155foreachmy$summary(@merged) {3156$commit->{mergemsg} .="\t$summary\n";3157}3158$commit->{mergemsg} .="\n\n";3159# print "Message for $commit->{hash} \n$commit->{mergemsg}";3160}3161}3162}3163}3164}31653166# convert the date to CVS-happy format3167$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);31683169if(defined($lastpicked) )3170{3171my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3172local($/) ="\0";3173while( <FILELIST> )3174{3175chomp;3176unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)3177{3178die("Couldn't process git-diff-tree line :$_");3179}3180my($mode,$hash,$change) = ($1,$2,$3);3181my$name= <FILELIST>;3182chomp($name);31833184# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");31853186my$git_perms="";3187$git_perms.="r"if($mode&4);3188$git_perms.="w"if($mode&2);3189$git_perms.="x"if($mode&1);3190$git_perms="rw"if($git_permseq"");31913192if($changeeq"D")3193{3194#$log->debug("DELETE $name");3195$head->{$name} = {3196 name =>$name,3197 revision =>$head->{$name}{revision} +1,3198 filehash =>"deleted",3199 commithash =>$commit->{hash},3200 modified =>$commit->{date},3201 author =>$commit->{author},3202 mode =>$git_perms,3203};3204$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3205}3206elsif($changeeq"M"||$changeeq"T")3207{3208#$log->debug("MODIFIED $name");3209$head->{$name} = {3210 name =>$name,3211 revision =>$head->{$name}{revision} +1,3212 filehash =>$hash,3213 commithash =>$commit->{hash},3214 modified =>$commit->{date},3215 author =>$commit->{author},3216 mode =>$git_perms,3217};3218$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3219}3220elsif($changeeq"A")3221{3222#$log->debug("ADDED $name");3223$head->{$name} = {3224 name =>$name,3225 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3226 filehash =>$hash,3227 commithash =>$commit->{hash},3228 modified =>$commit->{date},3229 author =>$commit->{author},3230 mode =>$git_perms,3231};3232$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3233}3234else3235{3236$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3237die;3238}3239}3240close FILELIST;3241}else{3242# this is used to detect files removed from the repo3243my$seen_files= {};32443245my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3246local$/="\0";3247while( <FILELIST> )3248{3249chomp;3250unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3251{3252die("Couldn't process git-ls-tree line :$_");3253}32543255my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);32563257$seen_files->{$git_filename} =1;32583259my($oldhash,$oldrevision,$oldmode) = (3260$head->{$git_filename}{filehash},3261$head->{$git_filename}{revision},3262$head->{$git_filename}{mode}3263);32643265if($git_perms=~/^\d\d\d(\d)\d\d/o)3266{3267$git_perms="";3268$git_perms.="r"if($1&4);3269$git_perms.="w"if($1&2);3270$git_perms.="x"if($1&1);3271}else{3272$git_perms="rw";3273}32743275# unless the file exists with the same hash, we need to update it ...3276unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)3277{3278my$newrevision= ($oldrevisionor0) +1;32793280$head->{$git_filename} = {3281 name =>$git_filename,3282 revision =>$newrevision,3283 filehash =>$git_hash,3284 commithash =>$commit->{hash},3285 modified =>$commit->{date},3286 author =>$commit->{author},3287 mode =>$git_perms,3288};328932903291$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);3292}3293}3294close FILELIST;32953296# Detect deleted files3297foreachmy$file(keys%$head)3298{3299unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3300{3301$head->{$file}{revision}++;3302$head->{$file}{filehash} ="deleted";3303$head->{$file}{commithash} =$commit->{hash};3304$head->{$file}{modified} =$commit->{date};3305$head->{$file}{author} =$commit->{author};33063307$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});3308}3309}3310# END : "Detect deleted files"3311}331233133314if(exists$commit->{mergemsg})3315{3316$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3317}33183319$lastpicked=$commit->{hash};33203321$self->_set_prop("last_commit",$commit->{hash});3322}33233324$self->delete_head();3325foreachmy$file(keys%$head)3326{3327$self->insert_head(3328$file,3329$head->{$file}{revision},3330$head->{$file}{filehash},3331$head->{$file}{commithash},3332$head->{$file}{modified},3333$head->{$file}{author},3334$head->{$file}{mode},3335);3336}3337# invalidate the gethead cache3338$self->{gethead_cache} =undef;333933403341# Ending exclusive lock here3342$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3343}33443345sub insert_rev3346{3347my$self=shift;3348my$name=shift;3349my$revision=shift;3350my$filehash=shift;3351my$commithash=shift;3352my$modified=shift;3353my$author=shift;3354my$mode=shift;3355my$tablename=$self->tablename("revision");33563357my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3358$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3359}33603361sub insert_mergelog3362{3363my$self=shift;3364my$key=shift;3365my$value=shift;3366my$tablename=$self->tablename("commitmsgs");33673368my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3369$insert_mergelog->execute($key,$value);3370}33713372sub delete_head3373{3374my$self=shift;3375my$tablename=$self->tablename("head");33763377my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3378$delete_head->execute();3379}33803381sub insert_head3382{3383my$self=shift;3384my$name=shift;3385my$revision=shift;3386my$filehash=shift;3387my$commithash=shift;3388my$modified=shift;3389my$author=shift;3390my$mode=shift;3391my$tablename=$self->tablename("head");33923393my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3394$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3395}33963397sub _headrev3398{3399my$self=shift;3400my$filename=shift;3401my$tablename=$self->tablename("head");34023403my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM$tablenameWHERE name=?",{},1);3404$db_query->execute($filename);3405my($hash,$revision,$mode) =$db_query->fetchrow_array;34063407return($hash,$revision,$mode);3408}34093410sub _get_prop3411{3412my$self=shift;3413my$key=shift;3414my$tablename=$self->tablename("properties");34153416my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3417$db_query->execute($key);3418my($value) =$db_query->fetchrow_array;34193420return$value;3421}34223423sub _set_prop3424{3425my$self=shift;3426my$key=shift;3427my$value=shift;3428my$tablename=$self->tablename("properties");34293430my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3431$db_query->execute($value,$key);34323433unless($db_query->rows)3434{3435$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3436$db_query->execute($key,$value);3437}34383439return$value;3440}34413442=head2 gethead34433444=cut34453446sub gethead3447{3448my$self=shift;3449my$tablename=$self->tablename("head");34503451return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );34523453my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3454$db_query->execute();34553456my$tree= [];3457while(my$file=$db_query->fetchrow_hashref)3458{3459push@$tree,$file;3460}34613462$self->{gethead_cache} =$tree;34633464return$tree;3465}34663467=head2 getlog34683469=cut34703471sub getlog3472{3473my$self=shift;3474my$filename=shift;3475my$tablename=$self->tablename("revision");34763477my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3478$db_query->execute($filename);34793480my$tree= [];3481while(my$file=$db_query->fetchrow_hashref)3482{3483push@$tree,$file;3484}34853486return$tree;3487}34883489=head2 getmeta34903491This function takes a filename (with path) argument and returns a hashref of3492metadata for that file.34933494=cut34953496sub getmeta3497{3498my$self=shift;3499my$filename=shift;3500my$revision=shift;3501my$tablename_rev=$self->tablename("revision");3502my$tablename_head=$self->tablename("head");35033504my$db_query;3505if(defined($revision)and$revision=~/^\d+$/)3506{3507$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND revision=?",{},1);3508$db_query->execute($filename,$revision);3509}3510elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)3511{3512$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_revWHERE name=? AND commithash=?",{},1);3513$db_query->execute($filename,$revision);3514}else{3515$db_query=$self->{dbh}->prepare_cached("SELECT * FROM$tablename_headWHERE name=?",{},1);3516$db_query->execute($filename);3517}35183519return$db_query->fetchrow_hashref;3520}35213522=head2 commitmessage35233524this function takes a commithash and returns the commit message for that commit35253526=cut3527sub commitmessage3528{3529my$self=shift;3530my$commithash=shift;3531my$tablename=$self->tablename("commitmsgs");35323533die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);35343535my$db_query;3536$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3537$db_query->execute($commithash);35383539my($message) =$db_query->fetchrow_array;35403541if(defined($message) )3542{3543$message.=" "if($message=~/\n$/);3544return$message;3545}35463547my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);3548shift@lineswhile($lines[0] =~/\S/);3549$message=join("",@lines);3550$message.=" "if($message=~/\n$/);3551return$message;3552}35533554=head2 gethistory35553556This function takes a filename (with path) argument and returns an arrayofarrays3557containing revision,filehash,commithash ordered by revision descending35583559=cut3560sub gethistory3561{3562my$self=shift;3563my$filename=shift;3564my$tablename=$self->tablename("revision");35653566my$db_query;3567$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);3568$db_query->execute($filename);35693570return$db_query->fetchall_arrayref;3571}35723573=head2 gethistorydense35743575This function takes a filename (with path) argument and returns an arrayofarrays3576containing revision,filehash,commithash ordered by revision descending.35773578This version of gethistory skips deleted entries -- so it is useful for annotate.3579The 'dense' part is a reference to a '--dense' option available for git-rev-list3580and other git tools that depend on it.35813582=cut3583sub gethistorydense3584{3585my$self=shift;3586my$filename=shift;3587my$tablename=$self->tablename("revision");35883589my$db_query;3590$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);3591$db_query->execute($filename);35923593return$db_query->fetchall_arrayref;3594}35953596=head2 in_array()35973598from Array::PAT - mimics the in_array() function3599found in PHP. Yuck but works for small arrays.36003601=cut3602sub in_array3603{3604my($check,@array) =@_;3605my$retval=0;3606foreachmy$test(@array){3607if($checkeq$test){3608$retval=1;3609}3610}3611return$retval;3612}36133614=head2 safe_pipe_capture36153616an alternative to `command` that allows input to be passed as an array3617to work around shell problems with weird characters in arguments36183619=cut3620sub safe_pipe_capture {36213622my@output;36233624if(my$pid=open my$child,'-|') {3625@output= (<$child>);3626close$childor die join(' ',@_).":$!$?";3627}else{3628exec(@_)or die"$!$?";# exec() can fail the executable can't be found3629}3630returnwantarray?@output:join('',@output);3631}36323633=head2 mangle_dirname36343635create a string from a directory name that is suitable to use as3636part of a filename, mainly by converting all chars except \w.- to _36373638=cut3639sub mangle_dirname {3640my$dirname=shift;3641return unlessdefined$dirname;36423643$dirname=~s/[^\w.-]/_/g;36443645return$dirname;3646}36473648=head2 mangle_tablename36493650create a string from a that is suitable to use as part of an SQL table3651name, mainly by converting all chars except \w to _36523653=cut3654sub mangle_tablename {3655my$tablename=shift;3656return unlessdefined$tablename;36573658$tablename=~s/[^\w_]/_/g;36593660return$tablename;3661}366236631;