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::Basename; 25 26my$log= GITCVS::log->new(); 27my$cfg; 28 29my$DATE_LIST= { 30 Jan =>"01", 31 Feb =>"02", 32 Mar =>"03", 33 Apr =>"04", 34 May =>"05", 35 Jun =>"06", 36 Jul =>"07", 37 Aug =>"08", 38 Sep =>"09", 39 Oct =>"10", 40 Nov =>"11", 41 Dec =>"12", 42}; 43 44# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 45$| =1; 46 47#### Definition and mappings of functions #### 48 49my$methods= { 50'Root'=> \&req_Root, 51'Valid-responses'=> \&req_Validresponses, 52'valid-requests'=> \&req_validrequests, 53'Directory'=> \&req_Directory, 54'Entry'=> \&req_Entry, 55'Modified'=> \&req_Modified, 56'Unchanged'=> \&req_Unchanged, 57'Questionable'=> \&req_Questionable, 58'Argument'=> \&req_Argument, 59'Argumentx'=> \&req_Argument, 60'expand-modules'=> \&req_expandmodules, 61'add'=> \&req_add, 62'remove'=> \&req_remove, 63'co'=> \&req_co, 64'update'=> \&req_update, 65'ci'=> \&req_ci, 66'diff'=> \&req_diff, 67'log'=> \&req_log, 68'rlog'=> \&req_log, 69'tag'=> \&req_CATCHALL, 70'status'=> \&req_status, 71'admin'=> \&req_CATCHALL, 72'history'=> \&req_CATCHALL, 73'watchers'=> \&req_CATCHALL, 74'editors'=> \&req_CATCHALL, 75'annotate'=> \&req_annotate, 76'Global_option'=> \&req_Globaloption, 77#'annotate' => \&req_CATCHALL, 78}; 79 80############################################## 81 82 83# $state holds all the bits of information the clients sends us that could 84# potentially be useful when it comes to actually _doing_ something. 85my$state= { prependdir =>''}; 86$log->info("--------------- STARTING -----------------"); 87 88my$TEMP_DIR= tempdir( CLEANUP =>1); 89$log->debug("Temporary directory is '$TEMP_DIR'"); 90 91# if we are called with a pserver argument, 92# deal with the authentication cat before entering the 93# main loop 94$state->{method} ='ext'; 95if(@ARGV&&$ARGV[0]eq'pserver') { 96$state->{method} ='pserver'; 97my$line= <STDIN>;chomp$line; 98unless($lineeq'BEGIN AUTH REQUEST') { 99die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 100} 101$line= <STDIN>;chomp$line; 102 req_Root('root',$line)# reuse Root 103or die"E Invalid root$line\n"; 104$line= <STDIN>;chomp$line; 105unless($lineeq'anonymous') { 106print"E Only anonymous user allowed via pserver\n"; 107print"I HATE YOU\n"; 108} 109$line= <STDIN>;chomp$line;# validate the password? 110$line= <STDIN>;chomp$line; 111unless($lineeq'END AUTH REQUEST') { 112die"E Do not understand$line-- expecting END AUTH REQUEST\n"; 113} 114print"I LOVE YOU\n"; 115# and now back to our regular programme... 116} 117 118# Keep going until the client closes the connection 119while(<STDIN>) 120{ 121chomp; 122 123# Check to see if we've seen this method, and call appropriate function. 124if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 125{ 126# use the $methods hash to call the appropriate sub for this command 127#$log->info("Method : $1"); 128&{$methods->{$1}}($1,$2); 129}else{ 130# log fatal because we don't understand this function. If this happens 131# we're fairly screwed because we don't know if the client is expecting 132# a response. If it is, the client will hang, we'll hang, and the whole 133# thing will be custard. 134$log->fatal("Don't understand command$_\n"); 135die("Unknown command$_"); 136} 137} 138 139$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 140$log->info("--------------- FINISH -----------------"); 141 142# Magic catchall method. 143# This is the method that will handle all commands we haven't yet 144# implemented. It simply sends a warning to the log file indicating a 145# command that hasn't been implemented has been invoked. 146sub req_CATCHALL 147{ 148my($cmd,$data) =@_; 149$log->warn("Unhandled command : req_$cmd:$data"); 150} 151 152 153# Root pathname \n 154# Response expected: no. Tell the server which CVSROOT to use. Note that 155# pathname is a local directory and not a fully qualified CVSROOT variable. 156# pathname must already exist; if creating a new root, use the init 157# request, not Root. pathname does not include the hostname of the server, 158# how to access the server, etc.; by the time the CVS protocol is in use, 159# connection, authentication, etc., are already taken care of. The Root 160# request must be sent only once, and it must be sent before any requests 161# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 162sub req_Root 163{ 164my($cmd,$data) =@_; 165$log->debug("req_Root :$data"); 166 167$state->{CVSROOT} =$data; 168 169$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 170unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 171print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 172print"E\n"; 173print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 174return0; 175} 176 177my@gitvars=`git-config -l`; 178if($?) { 179print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 180print"E\n"; 181print"error 1 - problem executing git-config\n"; 182return0; 183} 184foreachmy$line(@gitvars) 185{ 186next unless($line=~/^(.*?)\.(.*?)(?:\.(.*?))?=(.*)$/); 187unless($3) { 188$cfg->{$1}{$2} =$4; 189}else{ 190$cfg->{$1}{$2}{$3} =$4; 191} 192} 193 194unless( ($cfg->{gitcvs}{$state->{method}}{enabled} 195and$cfg->{gitcvs}{$state->{method}}{enabled} =~/^\s*(1|true|yes)\s*$/i) 196or($cfg->{gitcvs}{enabled} 197and$cfg->{gitcvs}{enabled} =~/^\s*(1|true|yes)\s*$/i) ) 198{ 199print"E GITCVS emulation needs to be enabled on this repo\n"; 200print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 201print"E\n"; 202print"error 1 GITCVS emulation disabled\n"; 203return0; 204} 205 206my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 207if($logfile) 208{ 209$log->setfile($logfile); 210}else{ 211$log->nofile(); 212} 213 214return1; 215} 216 217# Global_option option \n 218# Response expected: no. Transmit one of the global options `-q', `-Q', 219# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 220# variations (such as combining of options) are allowed. For graceful 221# handling of valid-requests, it is probably better to make new global 222# options separate requests, rather than trying to add them to this 223# request. 224sub req_Globaloption 225{ 226my($cmd,$data) =@_; 227$log->debug("req_Globaloption :$data"); 228$state->{globaloptions}{$data} =1; 229} 230 231# Valid-responses request-list \n 232# Response expected: no. Tell the server what responses the client will 233# accept. request-list is a space separated list of tokens. 234sub req_Validresponses 235{ 236my($cmd,$data) =@_; 237$log->debug("req_Validresponses :$data"); 238 239# TODO : re-enable this, currently it's not particularly useful 240#$state->{validresponses} = [ split /\s+/, $data ]; 241} 242 243# valid-requests \n 244# Response expected: yes. Ask the server to send back a Valid-requests 245# response. 246sub req_validrequests 247{ 248my($cmd,$data) =@_; 249 250$log->debug("req_validrequests"); 251 252$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 253$log->debug("SEND : ok"); 254 255print"Valid-requests ".join(" ",keys%$methods) ."\n"; 256print"ok\n"; 257} 258 259# Directory local-directory \n 260# Additional data: repository \n. Response expected: no. Tell the server 261# what directory to use. The repository should be a directory name from a 262# previous server response. Note that this both gives a default for Entry 263# and Modified and also for ci and the other commands; normal usage is to 264# send Directory for each directory in which there will be an Entry or 265# Modified, and then a final Directory for the original directory, then the 266# command. The local-directory is relative to the top level at which the 267# command is occurring (i.e. the last Directory which is sent before the 268# command); to indicate that top level, `.' should be sent for 269# local-directory. 270sub req_Directory 271{ 272my($cmd,$data) =@_; 273 274my$repository= <STDIN>; 275chomp$repository; 276 277 278$state->{localdir} =$data; 279$state->{repository} =$repository; 280$state->{path} =$repository; 281$state->{path} =~s/^$state->{CVSROOT}\///; 282$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 283$state->{path} .="/"if($state->{path} =~ /\S/ ); 284 285$state->{directory} =$state->{localdir}; 286$state->{directory} =""if($state->{directory}eq"."); 287$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 288 289if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 290{ 291$log->info("Setting prepend to '$state->{path}'"); 292$state->{prependdir} =$state->{path}; 293foreachmy$entry(keys%{$state->{entries}} ) 294{ 295$state->{entries}{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 296delete$state->{entries}{$entry}; 297} 298} 299 300if(defined($state->{prependdir} ) ) 301{ 302$log->debug("Prepending '$state->{prependdir}' to state|directory"); 303$state->{directory} =$state->{prependdir} .$state->{directory} 304} 305$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 306} 307 308# Entry entry-line \n 309# Response expected: no. Tell the server what version of a file is on the 310# local machine. The name in entry-line is a name relative to the directory 311# most recently specified with Directory. If the user is operating on only 312# some files in a directory, Entry requests for only those files need be 313# included. If an Entry request is sent without Modified, Is-modified, or 314# Unchanged, it means the file is lost (does not exist in the working 315# directory). If both Entry and one of Modified, Is-modified, or Unchanged 316# are sent for the same file, Entry must be sent first. For a given file, 317# one can send Modified, Is-modified, or Unchanged, but not more than one 318# of these three. 319sub req_Entry 320{ 321my($cmd,$data) =@_; 322 323#$log->debug("req_Entry : $data"); 324 325my@data=split(/\//,$data); 326 327$state->{entries}{$state->{directory}.$data[1]} = { 328 revision =>$data[2], 329 conflict =>$data[3], 330 options =>$data[4], 331 tag_or_date =>$data[5], 332}; 333 334$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 335} 336 337# Questionable filename \n 338# Response expected: no. Additional data: no. Tell the server to check 339# whether filename should be ignored, and if not, next time the server 340# sends responses, send (in a M response) `?' followed by the directory and 341# filename. filename must not contain `/'; it needs to be a file in the 342# directory named by the most recent Directory request. 343sub req_Questionable 344{ 345my($cmd,$data) =@_; 346 347$log->debug("req_Questionable :$data"); 348$state->{entries}{$state->{directory}.$data}{questionable} =1; 349} 350 351# add \n 352# Response expected: yes. Add a file or directory. This uses any previous 353# Argument, Directory, Entry, or Modified requests, if they have been sent. 354# The last Directory sent specifies the working directory at the time of 355# the operation. To add a directory, send the directory to be added using 356# Directory and Argument requests. 357sub req_add 358{ 359my($cmd,$data) =@_; 360 361 argsplit("add"); 362 363my$addcount=0; 364 365foreachmy$filename( @{$state->{args}} ) 366{ 367$filename= filecleanup($filename); 368 369unless(defined($state->{entries}{$filename}{modified_filename} ) ) 370{ 371print"E cvs add: nothing known about `$filename'\n"; 372next; 373} 374# TODO : check we're not squashing an already existing file 375if(defined($state->{entries}{$filename}{revision} ) ) 376{ 377print"E cvs add: `$filename' has already been entered\n"; 378next; 379} 380 381my($filepart,$dirpart) = filenamesplit($filename,1); 382 383print"E cvs add: scheduling file `$filename' for addition\n"; 384 385print"Checked-in$dirpart\n"; 386print"$filename\n"; 387my$kopts= kopts_from_path($filepart); 388print"/$filepart/0//$kopts/\n"; 389 390$addcount++; 391} 392 393if($addcount==1) 394{ 395print"E cvs add: use `cvs commit' to add this file permanently\n"; 396} 397elsif($addcount>1) 398{ 399print"E cvs add: use `cvs commit' to add these files permanently\n"; 400} 401 402print"ok\n"; 403} 404 405# remove \n 406# Response expected: yes. Remove a file. This uses any previous Argument, 407# Directory, Entry, or Modified requests, if they have been sent. The last 408# Directory sent specifies the working directory at the time of the 409# operation. Note that this request does not actually do anything to the 410# repository; the only effect of a successful remove request is to supply 411# the client with a new entries line containing `-' to indicate a removed 412# file. In fact, the client probably could perform this operation without 413# contacting the server, although using remove may cause the server to 414# perform a few more checks. The client sends a subsequent ci request to 415# actually record the removal in the repository. 416sub req_remove 417{ 418my($cmd,$data) =@_; 419 420 argsplit("remove"); 421 422# Grab a handle to the SQLite db and do any necessary updates 423my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 424$updater->update(); 425 426#$log->debug("add state : " . Dumper($state)); 427 428my$rmcount=0; 429 430foreachmy$filename( @{$state->{args}} ) 431{ 432$filename= filecleanup($filename); 433 434if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 435{ 436print"E cvs remove: file `$filename' still in working directory\n"; 437next; 438} 439 440my$meta=$updater->getmeta($filename); 441my$wrev= revparse($filename); 442 443unless(defined($wrev) ) 444{ 445print"E cvs remove: nothing known about `$filename'\n"; 446next; 447} 448 449if(defined($wrev)and$wrev<0) 450{ 451print"E cvs remove: file `$filename' already scheduled for removal\n"; 452next; 453} 454 455unless($wrev==$meta->{revision} ) 456{ 457# TODO : not sure if the format of this message is quite correct. 458print"E cvs remove: Up to date check failed for `$filename'\n"; 459next; 460} 461 462 463my($filepart,$dirpart) = filenamesplit($filename,1); 464 465print"E cvs remove: scheduling `$filename' for removal\n"; 466 467print"Checked-in$dirpart\n"; 468print"$filename\n"; 469my$kopts= kopts_from_path($filepart); 470print"/$filepart/-1.$wrev//$kopts/\n"; 471 472$rmcount++; 473} 474 475if($rmcount==1) 476{ 477print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 478} 479elsif($rmcount>1) 480{ 481print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 482} 483 484print"ok\n"; 485} 486 487# Modified filename \n 488# Response expected: no. Additional data: mode, \n, file transmission. Send 489# the server a copy of one locally modified file. filename is a file within 490# the most recent directory sent with Directory; it must not contain `/'. 491# If the user is operating on only some files in a directory, only those 492# files need to be included. This can also be sent without Entry, if there 493# is no entry for the file. 494sub req_Modified 495{ 496my($cmd,$data) =@_; 497 498my$mode= <STDIN>; 499chomp$mode; 500my$size= <STDIN>; 501chomp$size; 502 503# Grab config information 504my$blocksize=8192; 505my$bytesleft=$size; 506my$tmp; 507 508# Get a filehandle/name to write it to 509my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 510 511# Loop over file data writing out to temporary file. 512while($bytesleft) 513{ 514$blocksize=$bytesleftif($bytesleft<$blocksize); 515read STDIN,$tmp,$blocksize; 516print$fh $tmp; 517$bytesleft-=$blocksize; 518} 519 520close$fh; 521 522# Ensure we have something sensible for the file mode 523if($mode=~/u=(\w+)/) 524{ 525$mode=$1; 526}else{ 527$mode="rw"; 528} 529 530# Save the file data in $state 531$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 532$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 533$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 534$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 535 536 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 537} 538 539# Unchanged filename\n 540# Response expected: no. Tell the server that filename has not been 541# modified in the checked out directory. The filename is a file within the 542# most recent directory sent with Directory; it must not contain `/'. 543sub req_Unchanged 544{ 545 my ($cmd,$data) =@_; 546 547$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 548 549 #$log->debug("req_Unchanged :$data"); 550} 551 552# Argument text\n 553# Response expected: no. Save argument for use in a subsequent command. 554# Arguments accumulate until an argument-using command is given, at which 555# point they are forgotten. 556# Argumentx text\n 557# Response expected: no. Append\nfollowed by text to the current argument 558# being saved. 559sub req_Argument 560{ 561 my ($cmd,$data) =@_; 562 563 # Argumentx means: append to last Argument (with a newline in front) 564 565$log->debug("$cmd:$data"); 566 567 if ($cmdeq 'Argumentx') { 568 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 569 } else { 570 push @{$state->{arguments}},$data; 571 } 572} 573 574# expand-modules\n 575# Response expected: yes. Expand the modules which are specified in the 576# arguments. Returns the data in Module-expansion responses. Note that the 577# server can assume that this is checkout or export, not rtag or rdiff; the 578# latter do not access the working directory and thus have no need to 579# expand modules on the client side. Expand may not be the best word for 580# what this request does. It does not necessarily tell you all the files 581# contained in a module, for example. Basically it is a way of telling you 582# which working directories the server needs to know about in order to 583# handle a checkout of the specified modules. For example, suppose that the 584# server has a module defined by 585# aliasmodule -a 1dir 586# That is, one can check out aliasmodule and it will take 1dir in the 587# repository and check it out to 1dir in the working directory. Now suppose 588# the client already has this module checked out and is planning on using 589# the co request to update it. Without using expand-modules, the client 590# would have two bad choices: it could either send information about all 591# working directories under the current directory, which could be 592# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 593# stands for 1dir, and neglect to send information for 1dir, which would 594# lead to incorrect operation. With expand-modules, the client would first 595# ask for the module to be expanded: 596sub req_expandmodules 597{ 598 my ($cmd,$data) =@_; 599 600 argsplit(); 601 602$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 603 604 unless ( ref$state->{arguments} eq "ARRAY" ) 605 { 606 print "ok\n"; 607 return; 608 } 609 610 foreach my$module( @{$state->{arguments}} ) 611 { 612$log->debug("SEND : Module-expansion$module"); 613 print "Module-expansion$module\n"; 614 } 615 616 print "ok\n"; 617 statecleanup(); 618} 619 620# co\n 621# Response expected: yes. Get files from the repository. This uses any 622# previous Argument, Directory, Entry, or Modified requests, if they have 623# been sent. Arguments to this command are module names; the client cannot 624# know what directories they correspond to except by (1) just sending the 625# co request, and then seeing what directory names the server sends back in 626# its responses, and (2) the expand-modules request. 627sub req_co 628{ 629 my ($cmd,$data) =@_; 630 631 argsplit("co"); 632 633 my$module=$state->{args}[0]; 634 my$checkout_path=$module; 635 636 # use the user specified directory if we're given it 637$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 638 639$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 640 641$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 642 643$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 644 645# Grab a handle to the SQLite db and do any necessary updates 646my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 647$updater->update(); 648 649$checkout_path=~ s|/$||;# get rid of trailing slashes 650 651# Eclipse seems to need the Clear-sticky command 652# to prepare the 'Entries' file for the new directory. 653print"Clear-sticky$checkout_path/\n"; 654print$state->{CVSROOT} ."/$module/\n"; 655print"Clear-static-directory$checkout_path/\n"; 656print$state->{CVSROOT} ."/$module/\n"; 657print"Clear-sticky$checkout_path/\n";# yes, twice 658print$state->{CVSROOT} ."/$module/\n"; 659print"Template$checkout_path/\n"; 660print$state->{CVSROOT} ."/$module/\n"; 661print"0\n"; 662 663# instruct the client that we're checking out to $checkout_path 664print"E cvs checkout: Updating$checkout_path\n"; 665 666my%seendirs= (); 667my$lastdir=''; 668 669# recursive 670sub prepdir { 671my($dir,$repodir,$remotedir,$seendirs) =@_; 672my$parent= dirname($dir); 673$dir=~ s|/+$||; 674$repodir=~ s|/+$||; 675$remotedir=~ s|/+$||; 676$parent=~ s|/+$||; 677$log->debug("announcedir$dir,$repodir,$remotedir"); 678 679if($parenteq'.'||$parenteq'./') { 680$parent=''; 681} 682# recurse to announce unseen parents first 683if(length($parent) && !exists($seendirs->{$parent})) { 684 prepdir($parent,$repodir,$remotedir,$seendirs); 685} 686# Announce that we are going to modify at the parent level 687if($parent) { 688print"E cvs checkout: Updating$remotedir/$parent\n"; 689}else{ 690print"E cvs checkout: Updating$remotedir\n"; 691} 692print"Clear-sticky$remotedir/$parent/\n"; 693print"$repodir/$parent/\n"; 694 695print"Clear-static-directory$remotedir/$dir/\n"; 696print"$repodir/$dir/\n"; 697print"Clear-sticky$remotedir/$parent/\n";# yes, twice 698print"$repodir/$parent/\n"; 699print"Template$remotedir/$dir/\n"; 700print"$repodir/$dir/\n"; 701print"0\n"; 702 703$seendirs->{$dir} =1; 704} 705 706foreachmy$git( @{$updater->gethead} ) 707{ 708# Don't want to check out deleted files 709next if($git->{filehash}eq"deleted"); 710 711($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 712 713if(length($git->{dir}) &&$git->{dir}ne'./' 714&&$git->{dir}ne$lastdir) { 715unless(exists($seendirs{$git->{dir}})) { 716 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 717$checkout_path, \%seendirs); 718$lastdir=$git->{dir}; 719$seendirs{$git->{dir}} =1; 720} 721print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 722} 723 724# modification time of this file 725print"Mod-time$git->{modified}\n"; 726 727# print some information to the client 728if(defined($git->{dir} )and$git->{dir}ne"./") 729{ 730print"M U$checkout_path/$git->{dir}$git->{name}\n"; 731}else{ 732print"M U$checkout_path/$git->{name}\n"; 733} 734 735# instruct client we're sending a file to put in this path 736print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 737 738print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 739 740# this is an "entries" line 741my$kopts= kopts_from_path($git->{name}); 742print"/$git->{name}/1.$git->{revision}//$kopts/\n"; 743# permissions 744print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 745 746# transmit file 747 transmitfile($git->{filehash}); 748} 749 750print"ok\n"; 751 752 statecleanup(); 753} 754 755# update \n 756# Response expected: yes. Actually do a cvs update command. This uses any 757# previous Argument, Directory, Entry, or Modified requests, if they have 758# been sent. The last Directory sent specifies the working directory at the 759# time of the operation. The -I option is not used--files which the client 760# can decide whether to ignore are not mentioned and the client sends the 761# Questionable request for others. 762sub req_update 763{ 764my($cmd,$data) =@_; 765 766$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 767 768 argsplit("update"); 769 770# 771# It may just be a client exploring the available heads/modules 772# in that case, list them as top level directories and leave it 773# at that. Eclipse uses this technique to offer you a list of 774# projects (heads in this case) to checkout. 775# 776if($state->{module}eq'') { 777print"E cvs update: Updating .\n"; 778opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 779while(my$head=readdir(HEADS)) { 780if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 781print"E cvs update: New directory `$head'\n"; 782} 783} 784closedir HEADS; 785print"ok\n"; 786return1; 787} 788 789 790# Grab a handle to the SQLite db and do any necessary updates 791my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 792 793$updater->update(); 794 795 argsfromdir($updater); 796 797#$log->debug("update state : " . Dumper($state)); 798 799# foreach file specified on the command line ... 800foreachmy$filename( @{$state->{args}} ) 801{ 802$filename= filecleanup($filename); 803 804$log->debug("Processing file$filename"); 805 806# if we have a -C we should pretend we never saw modified stuff 807if(exists($state->{opt}{C} ) ) 808{ 809delete$state->{entries}{$filename}{modified_hash}; 810delete$state->{entries}{$filename}{modified_filename}; 811$state->{entries}{$filename}{unchanged} =1; 812} 813 814my$meta; 815if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 816{ 817$meta=$updater->getmeta($filename,$1); 818}else{ 819$meta=$updater->getmeta($filename); 820} 821 822if( !defined$meta) 823{ 824$meta= { 825 name =>$filename, 826 revision =>0, 827 filehash =>'added' 828}; 829} 830 831my$oldmeta=$meta; 832 833my$wrev= revparse($filename); 834 835# If the working copy is an old revision, lets get that version too for comparison. 836if(defined($wrev)and$wrev!=$meta->{revision} ) 837{ 838$oldmeta=$updater->getmeta($filename,$wrev); 839} 840 841#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 842 843# Files are up to date if the working copy and repo copy have the same revision, 844# and the working copy is unmodified _and_ the user hasn't specified -C 845next if(defined($wrev) 846and defined($meta->{revision}) 847and$wrev==$meta->{revision} 848and$state->{entries}{$filename}{unchanged} 849and not exists($state->{opt}{C} ) ); 850 851# If the working copy and repo copy have the same revision, 852# but the working copy is modified, tell the client it's modified 853if(defined($wrev) 854and defined($meta->{revision}) 855and$wrev==$meta->{revision} 856and not exists($state->{opt}{C} ) ) 857{ 858$log->info("Tell the client the file is modified"); 859print"MT text M\n"; 860print"MT fname$filename\n"; 861print"MT newline\n"; 862next; 863} 864 865if($meta->{filehash}eq"deleted") 866{ 867my($filepart,$dirpart) = filenamesplit($filename,1); 868 869$log->info("Removing '$filename' from working copy (no longer in the repo)"); 870 871print"E cvs update: `$filename' is no longer in the repository\n"; 872# Don't want to actually _DO_ the update if -n specified 873unless($state->{globaloptions}{-n} ) { 874print"Removed$dirpart\n"; 875print"$filepart\n"; 876} 877} 878elsif(not defined($state->{entries}{$filename}{modified_hash} ) 879or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} 880or$meta->{filehash}eq'added') 881{ 882# normal update, just send the new revision (either U=Update, 883# or A=Add, or R=Remove) 884if(defined($wrev) &&$wrev<0) 885{ 886$log->info("Tell the client the file is scheduled for removal"); 887print"MT text R\n"; 888print"MT fname$filename\n"; 889print"MT newline\n"; 890next; 891} 892elsif( (!defined($wrev) ||$wrev==0) && (!defined($meta->{revision}) ||$meta->{revision} ==0) ) 893{ 894$log->info("Tell the client the file is scheduled for addition"); 895print"MT text A\n"; 896print"MT fname$filename\n"; 897print"MT newline\n"; 898next; 899 900} 901else{ 902$log->info("Updating '$filename' to ".$meta->{revision}); 903print"MT +updated\n"; 904print"MT text U\n"; 905print"MT fname$filename\n"; 906print"MT newline\n"; 907print"MT -updated\n"; 908} 909 910my($filepart,$dirpart) = filenamesplit($filename,1); 911 912# Don't want to actually _DO_ the update if -n specified 913unless($state->{globaloptions}{-n} ) 914{ 915if(defined($wrev) ) 916{ 917# instruct client we're sending a file to put in this path as a replacement 918print"Update-existing$dirpart\n"; 919$log->debug("Updating existing file 'Update-existing$dirpart'"); 920}else{ 921# instruct client we're sending a file to put in this path as a new file 922print"Clear-static-directory$dirpart\n"; 923print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 924print"Clear-sticky$dirpart\n"; 925print$state->{CVSROOT} ."/$state->{module}/$dirpart\n"; 926 927$log->debug("Creating new file 'Created$dirpart'"); 928print"Created$dirpart\n"; 929} 930print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 931 932# this is an "entries" line 933my$kopts= kopts_from_path($filepart); 934$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 935print"/$filepart/1.$meta->{revision}//$kopts/\n"; 936 937# permissions 938$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 939print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 940 941# transmit file 942 transmitfile($meta->{filehash}); 943} 944}else{ 945$log->info("Updating '$filename'"); 946my($filepart,$dirpart) = filenamesplit($meta->{name},1); 947 948my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 949 950chdir$dir; 951my$file_local=$filepart.".mine"; 952system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 953my$file_old=$filepart.".".$oldmeta->{revision}; 954 transmitfile($oldmeta->{filehash},$file_old); 955my$file_new=$filepart.".".$meta->{revision}; 956 transmitfile($meta->{filehash},$file_new); 957 958# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 959$log->info("Merging$file_local,$file_old,$file_new"); 960print"M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into$filename\n"; 961 962$log->debug("Temporary directory for merge is$dir"); 963 964my$return=system("git","merge-file",$file_local,$file_old,$file_new); 965$return>>=8; 966 967if($return==0) 968{ 969$log->info("Merged successfully"); 970print"M M$filename\n"; 971$log->debug("Merged$dirpart"); 972 973# Don't want to actually _DO_ the update if -n specified 974unless($state->{globaloptions}{-n} ) 975{ 976print"Merged$dirpart\n"; 977$log->debug($state->{CVSROOT} ."/$state->{module}/$filename"); 978print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 979my$kopts= kopts_from_path($filepart); 980$log->debug("/$filepart/1.$meta->{revision}//$kopts/"); 981print"/$filepart/1.$meta->{revision}//$kopts/\n"; 982} 983} 984elsif($return==1) 985{ 986$log->info("Merged with conflicts"); 987print"E cvs update: conflicts found in$filename\n"; 988print"M C$filename\n"; 989 990# Don't want to actually _DO_ the update if -n specified 991unless($state->{globaloptions}{-n} ) 992{ 993print"Merged$dirpart\n"; 994print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 995my$kopts= kopts_from_path($filepart); 996print"/$filepart/1.$meta->{revision}/+/$kopts/\n"; 997} 998} 999else1000{1001$log->warn("Merge failed");1002next;1003}10041005# Don't want to actually _DO_ the update if -n specified1006unless($state->{globaloptions}{-n} )1007{1008# permissions1009$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1010print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";10111012# transmit file, format is single integer on a line by itself (file1013# size) followed by the file contents1014# TODO : we should copy files in blocks1015my$data=`cat$file_local`;1016$log->debug("File size : " . length($data));1017 print length($data) . "\n";1018 print$data;1019 }10201021 chdir "/";1022 }10231024 }10251026 print "ok\n";1027}10281029sub req_ci1030{1031 my ($cmd,$data) =@_;10321033 argsplit("ci");10341035 #$log->debug("State : " . Dumper($state));10361037$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));10381039 if ($state->{method} eq 'pserver')1040 {1041 print "error 1 pserver access cannot commit\n";1042 exit;1043 }10441045 if ( -e$state->{CVSROOT} . "/index" )1046 {1047$log->warn("file 'index' already exists in the git repository");1048 print "error 1 Index already exists in git repo\n";1049 exit;1050 }10511052 # Grab a handle to the SQLite db and do any necessary updates1053 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1054$updater->update();10551056 my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1057 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 );1058$log->info("Lockless commit start, basing commit on '$tmpdir', index file is '$file_index'");10591060$ENV{GIT_DIR} =$state->{CVSROOT} . "/";1061$ENV{GIT_INDEX_FILE} =$file_index;10621063 # Remember where the head was at the beginning.1064 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1065 chomp$parenthash;1066 if ($parenthash!~ /^[0-9a-f]{40}$/) {1067 print "error 1 pserver cannot find the current HEAD of module";1068 exit;1069 }10701071 chdir$tmpdir;10721073 # populate the temporary index based1074 system("git-read-tree",$parenthash);1075 unless ($?== 0)1076 {1077 die "Error running git-read-tree$state->{module}$file_index$!";1078 }1079$log->info("Created index '$file_index' with for head$state->{module} - exit status$?");10801081 my@committedfiles= ();1082 my%oldmeta;10831084 # foreach file specified on the command line ...1085 foreach my$filename( @{$state->{args}} )1086 {1087 my$committedfile=$filename;1088$filename= filecleanup($filename);10891090 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );10911092 my$meta=$updater->getmeta($filename);1093$oldmeta{$filename} =$meta;10941095 my$wrev= revparse($filename);10961097 my ($filepart,$dirpart) = filenamesplit($filename);10981099 # do a checkout of the file if it part of this tree1100 if ($wrev) {1101 system('git-checkout-index', '-f', '-u',$filename);1102 unless ($?== 0) {1103 die "Error running git-checkout-index -f -u$filename:$!";1104 }1105 }11061107 my$addflag= 0;1108 my$rmflag= 0;1109$rmflag= 1 if ( defined($wrev) and$wrev< 0 );1110$addflag= 1 unless ( -e$filename);11111112 # Do up to date checking1113 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) )1114 {1115 # fail everything if an up to date check fails1116 print "error 1 Up to date check failed for$filename\n";1117 chdir "/";1118 exit;1119 }11201121 push@committedfiles,$committedfile;1122$log->info("Committing$filename");11231124 system("mkdir","-p",$dirpart) unless ( -d$dirpart);11251126 unless ($rmflag)1127 {1128$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1129 rename$state->{entries}{$filename}{modified_filename},$filename;11301131 # Calculate modes to remove1132 my$invmode= "";1133 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }11341135$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1136 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1137 }11381139 if ($rmflag)1140 {1141$log->info("Removing file '$filename'");1142 unlink($filename);1143 system("git-update-index", "--remove",$filename);1144 }1145 elsif ($addflag)1146 {1147$log->info("Adding file '$filename'");1148 system("git-update-index", "--add",$filename);1149 } else {1150$log->info("Updating file '$filename'");1151 system("git-update-index",$filename);1152 }1153 }11541155 unless ( scalar(@committedfiles) > 0 )1156 {1157 print "E No files to commit\n";1158 print "ok\n";1159 chdir "/";1160 return;1161 }11621163 my$treehash= `git-write-tree`;1164 chomp$treehash;11651166$log->debug("Treehash :$treehash, Parenthash :$parenthash");11671168 # write our commit message out if we have one ...1169 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1170 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1171 print$msg_fh"\n\nvia git-CVS emulator\n";1172 close$msg_fh;11731174 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1175chomp($commithash);1176$log->info("Commit hash :$commithash");11771178unless($commithash=~/[a-zA-Z0-9]{40}/)1179{1180$log->warn("Commit failed (Invalid commit hash)");1181print"error 1 Commit failed (unknown reason)\n";1182chdir"/";1183exit;1184}11851186# Check that this is allowed, just as we would with a receive-pack1187my@cmd= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1188$parenthash,$commithash);1189if( -x $cmd[0] ) {1190unless(system(@cmd) ==0)1191{1192$log->warn("Commit failed (update hook declined to update ref)");1193print"error 1 Commit failed (update hook declined)\n";1194chdir"/";1195exit;1196}1197}11981199if(system(qw(git update-ref -m),"cvsserver ci",1200"refs/heads/$state->{module}",$commithash,$parenthash)) {1201$log->warn("update-ref for$state->{module} failed.");1202print"error 1 Cannot commit -- update first\n";1203exit;1204}12051206$updater->update();12071208# foreach file specified on the command line ...1209foreachmy$filename(@committedfiles)1210{1211$filename= filecleanup($filename);12121213my$meta=$updater->getmeta($filename);1214unless(defined$meta->{revision}) {1215$meta->{revision} =1;1216}12171218my($filepart,$dirpart) = filenamesplit($filename,1);12191220$log->debug("Checked-in$dirpart:$filename");12211222print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1223if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1224{1225print"M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";1226print"Remove-entry$dirpart\n";1227print"$filename\n";1228}else{1229if($meta->{revision} ==1) {1230print"M initial revision: 1.1\n";1231}else{1232print"M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";1233}1234print"Checked-in$dirpart\n";1235print"$filename\n";1236my$kopts= kopts_from_path($filepart);1237print"/$filepart/1.$meta->{revision}//$kopts/\n";1238}1239}12401241chdir"/";1242print"ok\n";1243}12441245sub req_status1246{1247my($cmd,$data) =@_;12481249 argsplit("status");12501251$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1252#$log->debug("status state : " . Dumper($state));12531254# Grab a handle to the SQLite db and do any necessary updates1255my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1256$updater->update();12571258# if no files were specified, we need to work out what files we should be providing status on ...1259 argsfromdir($updater);12601261# foreach file specified on the command line ...1262foreachmy$filename( @{$state->{args}} )1263{1264$filename= filecleanup($filename);12651266my$meta=$updater->getmeta($filename);1267my$oldmeta=$meta;12681269my$wrev= revparse($filename);12701271# If the working copy is an old revision, lets get that version too for comparison.1272if(defined($wrev)and$wrev!=$meta->{revision} )1273{1274$oldmeta=$updater->getmeta($filename,$wrev);1275}12761277# TODO : All possible statuses aren't yet implemented1278my$status;1279# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1280$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1281and1282( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1283or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1284);12851286# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1287$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1288and1289($state->{entries}{$filename}{unchanged}1290or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1291);12921293# Need checkout if it exists in the repo but doesn't have a working copy1294$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );12951296# Locally modified if working copy and repo copy have the same revision but there are local changes1297$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );12981299# Needs Merge if working copy revision is less than repo copy and there are local changes1300$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );13011302$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1303$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1304$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1305$status||="File had conflicts on merge"if(0);13061307$status||="Unknown";13081309print"M ===================================================================\n";1310print"M File:$filename\tStatus:$status\n";1311if(defined($state->{entries}{$filename}{revision}) )1312{1313print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1314}else{1315print"M Working revision:\tNo entry for$filename\n";1316}1317if(defined($meta->{revision}) )1318{1319print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1320print"M Sticky Tag:\t\t(none)\n";1321print"M Sticky Date:\t\t(none)\n";1322print"M Sticky Options:\t\t(none)\n";1323}else{1324print"M Repository revision:\tNo revision control file\n";1325}1326print"M\n";1327}13281329print"ok\n";1330}13311332sub req_diff1333{1334my($cmd,$data) =@_;13351336 argsplit("diff");13371338$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1339#$log->debug("status state : " . Dumper($state));13401341my($revision1,$revision2);1342if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1343{1344$revision1=$state->{opt}{r}[0];1345$revision2=$state->{opt}{r}[1];1346}else{1347$revision1=$state->{opt}{r};1348}13491350$revision1=~s/^1\.//if(defined($revision1) );1351$revision2=~s/^1\.//if(defined($revision2) );13521353$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );13541355# Grab a handle to the SQLite db and do any necessary updates1356my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1357$updater->update();13581359# if no files were specified, we need to work out what files we should be providing status on ...1360 argsfromdir($updater);13611362# foreach file specified on the command line ...1363foreachmy$filename( @{$state->{args}} )1364{1365$filename= filecleanup($filename);13661367my($fh,$file1,$file2,$meta1,$meta2,$filediff);13681369my$wrev= revparse($filename);13701371# We need _something_ to diff against1372next unless(defined($wrev) );13731374# if we have a -r switch, use it1375if(defined($revision1) )1376{1377(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1378$meta1=$updater->getmeta($filename,$revision1);1379unless(defined($meta1)and$meta1->{filehash}ne"deleted")1380{1381print"E File$filenameat revision 1.$revision1doesn't exist\n";1382next;1383}1384 transmitfile($meta1->{filehash},$file1);1385}1386# otherwise we just use the working copy revision1387else1388{1389(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1390$meta1=$updater->getmeta($filename,$wrev);1391 transmitfile($meta1->{filehash},$file1);1392}13931394# if we have a second -r switch, use it too1395if(defined($revision2) )1396{1397(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1398$meta2=$updater->getmeta($filename,$revision2);13991400unless(defined($meta2)and$meta2->{filehash}ne"deleted")1401{1402print"E File$filenameat revision 1.$revision2doesn't exist\n";1403next;1404}14051406 transmitfile($meta2->{filehash},$file2);1407}1408# otherwise we just use the working copy1409else1410{1411$file2=$state->{entries}{$filename}{modified_filename};1412}14131414# if we have been given -r, and we don't have a $file2 yet, lets get one1415if(defined($revision1)and not defined($file2) )1416{1417(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1418$meta2=$updater->getmeta($filename,$wrev);1419 transmitfile($meta2->{filehash},$file2);1420}14211422# We need to have retrieved something useful1423next unless(defined($meta1) );14241425# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1426next if(not defined($meta2)and$wrev==$meta1->{revision}1427and1428( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1429or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1430);14311432# Apparently we only show diffs for locally modified files1433next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );14341435print"M Index:$filename\n";1436print"M ===================================================================\n";1437print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1438print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1439print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1440print"M diff ";1441foreachmy$opt(keys%{$state->{opt}} )1442{1443if(ref$state->{opt}{$opt}eq"ARRAY")1444{1445foreachmy$value( @{$state->{opt}{$opt}} )1446{1447print"-$opt$value";1448}1449}else{1450print"-$opt";1451print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1452}1453}1454print"$filename\n";14551456$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));14571458($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);14591460if(exists$state->{opt}{u} )1461{1462system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1463}else{1464system("diff$file1$file2>$filediff");1465}14661467while( <$fh> )1468{1469print"M$_";1470}1471close$fh;1472}14731474print"ok\n";1475}14761477sub req_log1478{1479my($cmd,$data) =@_;14801481 argsplit("log");14821483$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1484#$log->debug("log state : " . Dumper($state));14851486my($minrev,$maxrev);1487if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1488{1489my$control=$2;1490$minrev=$1;1491$maxrev=$3;1492$minrev=~s/^1\.//if(defined($minrev) );1493$maxrev=~s/^1\.//if(defined($maxrev) );1494$minrev++if(defined($minrev)and$controleq"::");1495}14961497# Grab a handle to the SQLite db and do any necessary updates1498my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1499$updater->update();15001501# if no files were specified, we need to work out what files we should be providing status on ...1502 argsfromdir($updater);15031504# foreach file specified on the command line ...1505foreachmy$filename( @{$state->{args}} )1506{1507$filename= filecleanup($filename);15081509my$headmeta=$updater->getmeta($filename);15101511my$revisions=$updater->getlog($filename);1512my$totalrevisions=scalar(@$revisions);15131514if(defined($minrev) )1515{1516$log->debug("Removing revisions less than$minrev");1517while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1518{1519pop@$revisions;1520}1521}1522if(defined($maxrev) )1523{1524$log->debug("Removing revisions greater than$maxrev");1525while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1526{1527shift@$revisions;1528}1529}15301531next unless(scalar(@$revisions) );15321533print"M\n";1534print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1535print"M Working file:$filename\n";1536print"M head: 1.$headmeta->{revision}\n";1537print"M branch:\n";1538print"M locks: strict\n";1539print"M access list:\n";1540print"M symbolic names:\n";1541print"M keyword substitution: kv\n";1542print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1543print"M description:\n";15441545foreachmy$revision(@$revisions)1546{1547print"M ----------------------------\n";1548print"M revision 1.$revision->{revision}\n";1549# reformat the date for log output1550$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}) );1551$revision->{author} =~s/\s+.*//;1552$revision->{author} =~s/^(.{8}).*/$1/;1553print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1554my$commitmessage=$updater->commitmessage($revision->{commithash});1555$commitmessage=~s/^/M /mg;1556print$commitmessage."\n";1557}1558print"M =============================================================================\n";1559}15601561print"ok\n";1562}15631564sub req_annotate1565{1566my($cmd,$data) =@_;15671568 argsplit("annotate");15691570$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1571#$log->debug("status state : " . Dumper($state));15721573# Grab a handle to the SQLite db and do any necessary updates1574my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1575$updater->update();15761577# if no files were specified, we need to work out what files we should be providing annotate on ...1578 argsfromdir($updater);15791580# we'll need a temporary checkout dir1581my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1582my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1583$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");15841585$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1586$ENV{GIT_INDEX_FILE} =$file_index;15871588chdir$tmpdir;15891590# foreach file specified on the command line ...1591foreachmy$filename( @{$state->{args}} )1592{1593$filename= filecleanup($filename);15941595my$meta=$updater->getmeta($filename);15961597next unless($meta->{revision} );15981599# get all the commits that this file was in1600# in dense format -- aka skip dead revisions1601my$revisions=$updater->gethistorydense($filename);1602my$lastseenin=$revisions->[0][2];16031604# populate the temporary index based on the latest commit were we saw1605# the file -- but do it cheaply without checking out any files1606# TODO: if we got a revision from the client, use that instead1607# to look up the commithash in sqlite (still good to default to1608# the current head as we do now)1609system("git-read-tree",$lastseenin);1610unless($?==0)1611{1612die"Error running git-read-tree$lastseenin$file_index$!";1613}1614$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");16151616# do a checkout of the file1617system('git-checkout-index','-f','-u',$filename);1618unless($?==0) {1619die"Error running git-checkout-index -f -u$filename:$!";1620}16211622$log->info("Annotate$filename");16231624# Prepare a file with the commits from the linearized1625# history that annotate should know about. This prevents1626# git-jsannotate telling us about commits we are hiding1627# from the client.16281629open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1630for(my$i=0;$i<@$revisions;$i++)1631{1632print ANNOTATEHINTS $revisions->[$i][2];1633if($i+1<@$revisions) {# have we got a parent?1634print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1635}1636print ANNOTATEHINTS "\n";1637}16381639print ANNOTATEHINTS "\n";1640close ANNOTATEHINTS;16411642my$annotatecmd='git-annotate';1643open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1644or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1645my$metadata= {};1646print"E Annotations for$filename\n";1647print"E ***************\n";1648while( <ANNOTATE> )1649{1650if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1651{1652my$commithash=$1;1653my$data=$2;1654unless(defined($metadata->{$commithash} ) )1655{1656$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1657$metadata->{$commithash}{author} =~s/\s+.*//;1658$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1659$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1660}1661printf("M 1.%-5d (%-8s%10s):%s\n",1662$metadata->{$commithash}{revision},1663$metadata->{$commithash}{author},1664$metadata->{$commithash}{modified},1665$data1666);1667}else{1668$log->warn("Error in annotate output! LINE:$_");1669print"E Annotate error\n";1670next;1671}1672}1673close ANNOTATE;1674}16751676# done; get out of the tempdir1677chdir"/";16781679print"ok\n";16801681}16821683# This method takes the state->{arguments} array and produces two new arrays.1684# The first is $state->{args} which is everything before the '--' argument, and1685# the second is $state->{files} which is everything after it.1686sub argsplit1687{1688return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");16891690my$type=shift;16911692$state->{args} = [];1693$state->{files} = [];1694$state->{opt} = {};16951696if(defined($type) )1697{1698my$opt= {};1699$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");1700$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1701$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");1702$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1703$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1704$opt= { k =>1, m =>1}if($typeeq"add");1705$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1706$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");170717081709while(scalar( @{$state->{arguments}} ) >0)1710{1711my$arg=shift@{$state->{arguments}};17121713next if($argeq"--");1714next unless($arg=~/\S/);17151716# if the argument looks like a switch1717if($arg=~/^-(\w)(.*)/)1718{1719# if it's a switch that takes an argument1720if($opt->{$1} )1721{1722# If this switch has already been provided1723if($opt->{$1} >1and exists($state->{opt}{$1} ) )1724{1725$state->{opt}{$1} = [$state->{opt}{$1} ];1726if(length($2) >0)1727{1728push@{$state->{opt}{$1}},$2;1729}else{1730push@{$state->{opt}{$1}},shift@{$state->{arguments}};1731}1732}else{1733# if there's extra data in the arg, use that as the argument for the switch1734if(length($2) >0)1735{1736$state->{opt}{$1} =$2;1737}else{1738$state->{opt}{$1} =shift@{$state->{arguments}};1739}1740}1741}else{1742$state->{opt}{$1} =undef;1743}1744}1745else1746{1747push@{$state->{args}},$arg;1748}1749}1750}1751else1752{1753my$mode=0;17541755foreachmy$value( @{$state->{arguments}} )1756{1757if($valueeq"--")1758{1759$mode++;1760next;1761}1762push@{$state->{args}},$valueif($mode==0);1763push@{$state->{files}},$valueif($mode==1);1764}1765}1766}17671768# This method uses $state->{directory} to populate $state->{args} with a list of filenames1769sub argsfromdir1770{1771my$updater=shift;17721773$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");17741775return if(scalar( @{$state->{args}} ) >1);17761777my@gethead= @{$updater->gethead};17781779# push added files1780foreachmy$file(keys%{$state->{entries}}) {1781if(exists$state->{entries}{$file}{revision} &&1782$state->{entries}{$file}{revision} ==0)1783{1784push@gethead, { name =>$file, filehash =>'added'};1785}1786}17871788if(scalar(@{$state->{args}}) ==1)1789{1790my$arg=$state->{args}[0];1791$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );17921793$log->info("Only one arg specified, checking for directory expansion on '$arg'");17941795foreachmy$file(@gethead)1796{1797next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1798next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);1799push@{$state->{args}},$file->{name};1800}18011802shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);1803}else{1804$log->info("Only one arg specified, populating file list automatically");18051806$state->{args} = [];18071808foreachmy$file(@gethead)1809{1810next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1811next unless($file->{name} =~s/^$state->{prependdir}//);1812push@{$state->{args}},$file->{name};1813}1814}1815}18161817# This method cleans up the $state variable after a command that uses arguments has run1818sub statecleanup1819{1820$state->{files} = [];1821$state->{args} = [];1822$state->{arguments} = [];1823$state->{entries} = {};1824}18251826sub revparse1827{1828my$filename=shift;18291830returnundefunless(defined($state->{entries}{$filename}{revision} ) );18311832return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1833return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);18341835returnundef;1836}18371838# This method takes a file hash and does a CVS "file transfer" which transmits the1839# size of the file, and then the file contents.1840# If a second argument $targetfile is given, the file is instead written out to1841# a file by the name of $targetfile1842sub transmitfile1843{1844my$filehash=shift;1845my$targetfile=shift;18461847if(defined($filehash)and$filehasheq"deleted")1848{1849$log->warn("filehash is 'deleted'");1850return;1851}18521853die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);18541855my$type=`git-cat-file -t$filehash`;1856 chomp$type;18571858 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );18591860 my$size= `git-cat-file -s $filehash`;1861chomp$size;18621863$log->debug("transmitfile($filehash) size=$size, type=$type");18641865if(open my$fh,'-|',"git-cat-file","blob",$filehash)1866{1867if(defined($targetfile) )1868{1869open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1870print NEWFILE $_while( <$fh> );1871close NEWFILE;1872}else{1873print"$size\n";1874printwhile( <$fh> );1875}1876close$fhor die("Couldn't close filehandle for transmitfile()");1877}else{1878die("Couldn't execute git-cat-file");1879}1880}18811882# This method takes a file name, and returns ( $dirpart, $filepart ) which1883# refers to the directory portion and the file portion of the filename1884# respectively1885sub filenamesplit1886{1887my$filename=shift;1888my$fixforlocaldir=shift;18891890my($filepart,$dirpart) = ($filename,".");1891($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1892$dirpart.="/";18931894if($fixforlocaldir)1895{1896$dirpart=~s/^$state->{prependdir}//;1897}18981899return($filepart,$dirpart);1900}19011902sub filecleanup1903{1904my$filename=shift;19051906returnundefunless(defined($filename));1907if($filename=~/^\// )1908{1909print"E absolute filenames '$filename' not supported by server\n";1910returnundef;1911}19121913$filename=~s/^\.\///g;1914$filename=$state->{prependdir} .$filename;1915return$filename;1916}19171918# Given a path, this function returns a string containing the kopts1919# that should go into that path's Entries line. For example, a binary1920# file should get -kb.1921sub kopts_from_path1922{1923my($path) =@_;19241925# Once it exists, the git attributes system should be used to look up1926# what attributes apply to this path.19271928# Until then, take the setting from the config file1929unless(defined($cfg->{gitcvs}{allbinary} )and$cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i)1930{1931# Return "" to give no special treatment to any path1932return"";1933}else{1934# Alternatively, to have all files treated as if they are binary (which1935# is more like git itself), always return the "-kb" option1936return"-kb";1937}1938}19391940package GITCVS::log;19411942####1943#### Copyright The Open University UK - 2006.1944####1945#### Authors: Martyn Smith <martyn@catalyst.net.nz>1946#### Martin Langhoff <martin@catalyst.net.nz>1947####1948####19491950use strict;1951use warnings;19521953=head1 NAME19541955GITCVS::log19561957=head1 DESCRIPTION19581959This module provides very crude logging with a similar interface to1960Log::Log4perl19611962=head1 METHODS19631964=cut19651966=head2 new19671968Creates a new log object, optionally you can specify a filename here to1969indicate the file to log to. If no log file is specified, you can specify one1970later with method setfile, or indicate you no longer want logging with method1971nofile.19721973Until one of these methods is called, all log calls will buffer messages ready1974to write out.19751976=cut1977sub new1978{1979my$class=shift;1980my$filename=shift;19811982my$self= {};19831984bless$self,$class;19851986if(defined($filename) )1987{1988open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1989}19901991return$self;1992}19931994=head2 setfile19951996This methods takes a filename, and attempts to open that file as the log file.1997If successful, all buffered data is written out to the file, and any further1998logging is written directly to the file.19992000=cut2001sub setfile2002{2003my$self=shift;2004my$filename=shift;20052006if(defined($filename) )2007{2008open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2009}20102011return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20122013while(my$line=shift@{$self->{buffer}} )2014{2015print{$self->{fh}}$line;2016}2017}20182019=head2 nofile20202021This method indicates no logging is going to be used. It flushes any entries in2022the internal buffer, and sets a flag to ensure no further data is put there.20232024=cut2025sub nofile2026{2027my$self=shift;20282029$self->{nolog} =1;20302031return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");20322033$self->{buffer} = [];2034}20352036=head2 _logopen20372038Internal method. Returns true if the log file is open, false otherwise.20392040=cut2041sub _logopen2042{2043my$self=shift;20442045return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");2046return0;2047}20482049=head2 debug info warn fatal20502051These four methods are wrappers to _log. They provide the actual interface for2052logging data.20532054=cut2055sub debug {my$self=shift;$self->_log("debug",@_); }2056sub info {my$self=shift;$self->_log("info",@_); }2057subwarn{my$self=shift;$self->_log("warn",@_); }2058sub fatal {my$self=shift;$self->_log("fatal",@_); }20592060=head2 _log20612062This is an internal method called by the logging functions. It generates a2063timestamp and pushes the logged line either to file, or internal buffer.20642065=cut2066sub _log2067{2068my$self=shift;2069my$level=shift;20702071return if($self->{nolog} );20722073my@time=localtime;2074my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",2075$time[5] +1900,2076$time[4] +1,2077$time[3],2078$time[2],2079$time[1],2080$time[0],2081uc$level,2082);20832084if($self->_logopen)2085{2086print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";2087}else{2088push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";2089}2090}20912092=head2 DESTROY20932094This method simply closes the file handle if one is open20952096=cut2097sub DESTROY2098{2099my$self=shift;21002101if($self->_logopen)2102{2103close$self->{fh};2104}2105}21062107package GITCVS::updater;21082109####2110#### Copyright The Open University UK - 2006.2111####2112#### Authors: Martyn Smith <martyn@catalyst.net.nz>2113#### Martin Langhoff <martin@catalyst.net.nz>2114####2115####21162117use strict;2118use warnings;2119use DBI;21202121=head1 METHODS21222123=cut21242125=head2 new21262127=cut2128sub new2129{2130my$class=shift;2131my$config=shift;2132my$module=shift;2133my$log=shift;21342135die"Need to specify a git repository"unless(defined($config)and-d $config);2136die"Need to specify a module"unless(defined($module) );21372138$class=ref($class) ||$class;21392140my$self= {};21412142bless$self,$class;21432144$self->{module} =$module;2145$self->{git_path} =$config."/";21462147$self->{log} =$log;21482149die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );21502151$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||2152$cfg->{gitcvs}{dbdriver} ||"dbi:SQLite";2153$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||2154$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";2155$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||2156$cfg->{gitcvs}{dbuser} ||"";2157$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||2158$cfg->{gitcvs}{dbpass} ||"";2159my%mapping= ( m =>$module,2160 a =>$state->{method},2161 u =>getlogin||getpwuid($<) || $<,2162 G =>$self->{git_path},2163 g => mangle_dirname($self->{git_path}),2164);2165$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;2166$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;21672168$self->{dbh} = DBI->connect("$self->{dbdriver}:dbname=$self->{dbname}",2169$self->{dbuser},2170$self->{dbpass});2171die"Error connecting to database\n"unlessdefined$self->{dbh};21722173$self->{tables} = {};2174foreachmy$table($self->{dbh}->tables)2175{2176$table=~s/^"//;2177$table=~s/"$//;2178$self->{tables}{$table} =1;2179}21802181# Construct the revision table if required2182unless($self->{tables}{revision} )2183{2184$self->{dbh}->do("2185 CREATE TABLE revision (2186 name TEXT NOT NULL,2187 revision INTEGER NOT NULL,2188 filehash TEXT NOT NULL,2189 commithash TEXT NOT NULL,2190 author TEXT NOT NULL,2191 modified TEXT NOT NULL,2192 mode TEXT NOT NULL2193 )2194 ");2195$self->{dbh}->do("2196 CREATE INDEX revision_ix12197 ON revision (name,revision)2198 ");2199$self->{dbh}->do("2200 CREATE INDEX revision_ix22201 ON revision (name,commithash)2202 ");2203}22042205# Construct the head table if required2206unless($self->{tables}{head} )2207{2208$self->{dbh}->do("2209 CREATE TABLE head (2210 name TEXT NOT NULL,2211 revision INTEGER NOT NULL,2212 filehash TEXT NOT NULL,2213 commithash TEXT NOT NULL,2214 author TEXT NOT NULL,2215 modified TEXT NOT NULL,2216 mode TEXT NOT NULL2217 )2218 ");2219$self->{dbh}->do("2220 CREATE INDEX head_ix12221 ON head (name)2222 ");2223}22242225# Construct the properties table if required2226unless($self->{tables}{properties} )2227{2228$self->{dbh}->do("2229 CREATE TABLE properties (2230 key TEXT NOT NULL PRIMARY KEY,2231 value TEXT2232 )2233 ");2234}22352236# Construct the commitmsgs table if required2237unless($self->{tables}{commitmsgs} )2238{2239$self->{dbh}->do("2240 CREATE TABLE commitmsgs (2241 key TEXT NOT NULL PRIMARY KEY,2242 value TEXT2243 )2244 ");2245}22462247return$self;2248}22492250=head2 update22512252=cut2253sub update2254{2255my$self=shift;22562257# first lets get the commit list2258$ENV{GIT_DIR} =$self->{git_path};22592260my$commitsha1=`git rev-parse$self->{module}`;2261chomp$commitsha1;22622263my$commitinfo=`git cat-file commit$self->{module} 2>&1`;2264unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2265{2266die("Invalid module '$self->{module}'");2267}226822692270my$git_log;2271my$lastcommit=$self->_get_prop("last_commit");22722273if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date2274return1;2275}22762277# Start exclusive lock here...2278$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";22792280# TODO: log processing is memory bound2281# if we can parse into a 2nd file that is in reverse order2282# we can probably do something really efficient2283my@git_log_params= ('--pretty','--parents','--topo-order');22842285if(defined$lastcommit) {2286push@git_log_params,"$lastcommit..$self->{module}";2287}else{2288push@git_log_params,$self->{module};2289}2290# git-rev-list is the backend / plumbing version of git-log2291open(GITLOG,'-|','git-rev-list',@git_log_params)or die"Cannot call git-rev-list:$!";22922293my@commits;22942295my%commit= ();22962297while( <GITLOG> )2298{2299chomp;2300if(m/^commit\s+(.*)$/) {2301# on ^commit lines put the just seen commit in the stack2302# and prime things for the next one2303if(keys%commit) {2304my%copy=%commit;2305unshift@commits, \%copy;2306%commit= ();2307}2308my@parents=split(m/\s+/,$1);2309$commit{hash} =shift@parents;2310$commit{parents} = \@parents;2311}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2312# on rfc822-like lines seen before we see any message,2313# lowercase the entry and put it in the hash as key-value2314$commit{lc($1)} =$2;2315}else{2316# message lines - skip initial empty line2317# and trim whitespace2318if(!exists($commit{message}) &&m/^\s*$/) {2319# define it to mark the end of headers2320$commit{message} ='';2321next;2322}2323s/^\s+//;s/\s+$//;# trim ws2324$commit{message} .=$_."\n";2325}2326}2327close GITLOG;23282329unshift@commits, \%commitif(keys%commit);23302331# Now all the commits are in the @commits bucket2332# ordered by time DESC. for each commit that needs processing,2333# determine whether it's following the last head we've seen or if2334# it's on its own branch, grab a file list, and add whatever's changed2335# NOTE: $lastcommit refers to the last commit from previous run2336# $lastpicked is the last commit we picked in this run2337my$lastpicked;2338my$head= {};2339if(defined$lastcommit) {2340$lastpicked=$lastcommit;2341}23422343my$committotal=scalar(@commits);2344my$commitcount=0;23452346# Load the head table into $head (for cached lookups during the update process)2347foreachmy$file( @{$self->gethead()} )2348{2349$head->{$file->{name}} =$file;2350}23512352foreachmy$commit(@commits)2353{2354$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2355if(defined$lastpicked)2356{2357if(!in_array($lastpicked, @{$commit->{parents}}))2358{2359# skip, we'll see this delta2360# as part of a merge later2361# warn "skipping off-track $commit->{hash}\n";2362next;2363}elsif(@{$commit->{parents}} >1) {2364# it is a merge commit, for each parent that is2365# not $lastpicked, see if we can get a log2366# from the merge-base to that parent to put it2367# in the message as a merge summary.2368my@parents= @{$commit->{parents}};2369foreachmy$parent(@parents) {2370# git-merge-base can potentially (but rarely) throw2371# several candidate merge bases. let's assume2372# that the first one is the best one.2373if($parenteq$lastpicked) {2374next;2375}2376open my$p,'git-merge-base '.$lastpicked.' '2377.$parent.'|';2378my@output= (<$p>);2379close$p;2380my$base=join('',@output);2381chomp$base;2382if($base) {2383my@merged;2384# print "want to log between $base $parent \n";2385open(GITLOG,'-|','git-log',"$base..$parent")2386or die"Cannot call git-log:$!";2387my$mergedhash;2388while(<GITLOG>) {2389chomp;2390if(!defined$mergedhash) {2391if(m/^commit\s+(.+)$/) {2392$mergedhash=$1;2393}else{2394next;2395}2396}else{2397# grab the first line that looks non-rfc8222398# aka has content after leading space2399if(m/^\s+(\S.*)$/) {2400my$title=$1;2401$title=substr($title,0,100);# truncate2402unshift@merged,"$mergedhash$title";2403undef$mergedhash;2404}2405}2406}2407close GITLOG;2408if(@merged) {2409$commit->{mergemsg} =$commit->{message};2410$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2411foreachmy$summary(@merged) {2412$commit->{mergemsg} .="\t$summary\n";2413}2414$commit->{mergemsg} .="\n\n";2415# print "Message for $commit->{hash} \n$commit->{mergemsg}";2416}2417}2418}2419}2420}24212422# convert the date to CVS-happy format2423$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);24242425if(defined($lastpicked) )2426{2427my$filepipe=open(FILELIST,'-|','git-diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2428local($/) ="\0";2429while( <FILELIST> )2430{2431chomp;2432unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o)2433{2434die("Couldn't process git-diff-tree line :$_");2435}2436my($mode,$hash,$change) = ($1,$2,$3);2437my$name= <FILELIST>;2438chomp($name);24392440# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");24412442my$git_perms="";2443$git_perms.="r"if($mode&4);2444$git_perms.="w"if($mode&2);2445$git_perms.="x"if($mode&1);2446$git_perms="rw"if($git_permseq"");24472448if($changeeq"D")2449{2450#$log->debug("DELETE $name");2451$head->{$name} = {2452 name =>$name,2453 revision =>$head->{$name}{revision} +1,2454 filehash =>"deleted",2455 commithash =>$commit->{hash},2456 modified =>$commit->{date},2457 author =>$commit->{author},2458 mode =>$git_perms,2459};2460$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2461}2462elsif($changeeq"M")2463{2464#$log->debug("MODIFIED $name");2465$head->{$name} = {2466 name =>$name,2467 revision =>$head->{$name}{revision} +1,2468 filehash =>$hash,2469 commithash =>$commit->{hash},2470 modified =>$commit->{date},2471 author =>$commit->{author},2472 mode =>$git_perms,2473};2474$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2475}2476elsif($changeeq"A")2477{2478#$log->debug("ADDED $name");2479$head->{$name} = {2480 name =>$name,2481 revision =>1,2482 filehash =>$hash,2483 commithash =>$commit->{hash},2484 modified =>$commit->{date},2485 author =>$commit->{author},2486 mode =>$git_perms,2487};2488$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2489}2490else2491{2492$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");2493die;2494}2495}2496close FILELIST;2497}else{2498# this is used to detect files removed from the repo2499my$seen_files= {};25002501my$filepipe=open(FILELIST,'-|','git-ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2502local$/="\0";2503while( <FILELIST> )2504{2505chomp;2506unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)2507{2508die("Couldn't process git-ls-tree line :$_");2509}25102511my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);25122513$seen_files->{$git_filename} =1;25142515my($oldhash,$oldrevision,$oldmode) = (2516$head->{$git_filename}{filehash},2517$head->{$git_filename}{revision},2518$head->{$git_filename}{mode}2519);25202521if($git_perms=~/^\d\d\d(\d)\d\d/o)2522{2523$git_perms="";2524$git_perms.="r"if($1&4);2525$git_perms.="w"if($1&2);2526$git_perms.="x"if($1&1);2527}else{2528$git_perms="rw";2529}25302531# unless the file exists with the same hash, we need to update it ...2532unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2533{2534my$newrevision= ($oldrevisionor0) +1;25352536$head->{$git_filename} = {2537 name =>$git_filename,2538 revision =>$newrevision,2539 filehash =>$git_hash,2540 commithash =>$commit->{hash},2541 modified =>$commit->{date},2542 author =>$commit->{author},2543 mode =>$git_perms,2544};254525462547$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2548}2549}2550close FILELIST;25512552# Detect deleted files2553foreachmy$file(keys%$head)2554{2555unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2556{2557$head->{$file}{revision}++;2558$head->{$file}{filehash} ="deleted";2559$head->{$file}{commithash} =$commit->{hash};2560$head->{$file}{modified} =$commit->{date};2561$head->{$file}{author} =$commit->{author};25622563$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2564}2565}2566# END : "Detect deleted files"2567}256825692570if(exists$commit->{mergemsg})2571{2572$self->insert_mergelog($commit->{hash},$commit->{mergemsg});2573}25742575$lastpicked=$commit->{hash};25762577$self->_set_prop("last_commit",$commit->{hash});2578}25792580$self->delete_head();2581foreachmy$file(keys%$head)2582{2583$self->insert_head(2584$file,2585$head->{$file}{revision},2586$head->{$file}{filehash},2587$head->{$file}{commithash},2588$head->{$file}{modified},2589$head->{$file}{author},2590$head->{$file}{mode},2591);2592}2593# invalidate the gethead cache2594$self->{gethead_cache} =undef;259525962597# Ending exclusive lock here2598$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2599}26002601sub insert_rev2602{2603my$self=shift;2604my$name=shift;2605my$revision=shift;2606my$filehash=shift;2607my$commithash=shift;2608my$modified=shift;2609my$author=shift;2610my$mode=shift;26112612my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2613$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2614}26152616sub insert_mergelog2617{2618my$self=shift;2619my$key=shift;2620my$value=shift;26212622my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2623$insert_mergelog->execute($key,$value);2624}26252626sub delete_head2627{2628my$self=shift;26292630my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2631$delete_head->execute();2632}26332634sub insert_head2635{2636my$self=shift;2637my$name=shift;2638my$revision=shift;2639my$filehash=shift;2640my$commithash=shift;2641my$modified=shift;2642my$author=shift;2643my$mode=shift;26442645my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2646$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);2647}26482649sub _headrev2650{2651my$self=shift;2652my$filename=shift;26532654my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2655$db_query->execute($filename);2656my($hash,$revision,$mode) =$db_query->fetchrow_array;26572658return($hash,$revision,$mode);2659}26602661sub _get_prop2662{2663my$self=shift;2664my$key=shift;26652666my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2667$db_query->execute($key);2668my($value) =$db_query->fetchrow_array;26692670return$value;2671}26722673sub _set_prop2674{2675my$self=shift;2676my$key=shift;2677my$value=shift;26782679my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2680$db_query->execute($value,$key);26812682unless($db_query->rows)2683{2684$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2685$db_query->execute($key,$value);2686}26872688return$value;2689}26902691=head2 gethead26922693=cut26942695sub gethead2696{2697my$self=shift;26982699return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );27002701my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2702$db_query->execute();27032704my$tree= [];2705while(my$file=$db_query->fetchrow_hashref)2706{2707push@$tree,$file;2708}27092710$self->{gethead_cache} =$tree;27112712return$tree;2713}27142715=head2 getlog27162717=cut27182719sub getlog2720{2721my$self=shift;2722my$filename=shift;27232724my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2725$db_query->execute($filename);27262727my$tree= [];2728while(my$file=$db_query->fetchrow_hashref)2729{2730push@$tree,$file;2731}27322733return$tree;2734}27352736=head2 getmeta27372738This function takes a filename (with path) argument and returns a hashref of2739metadata for that file.27402741=cut27422743sub getmeta2744{2745my$self=shift;2746my$filename=shift;2747my$revision=shift;27482749my$db_query;2750if(defined($revision)and$revision=~/^\d+$/)2751{2752$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2753$db_query->execute($filename,$revision);2754}2755elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2756{2757$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2758$db_query->execute($filename,$revision);2759}else{2760$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2761$db_query->execute($filename);2762}27632764return$db_query->fetchrow_hashref;2765}27662767=head2 commitmessage27682769this function takes a commithash and returns the commit message for that commit27702771=cut2772sub commitmessage2773{2774my$self=shift;2775my$commithash=shift;27762777die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);27782779my$db_query;2780$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2781$db_query->execute($commithash);27822783my($message) =$db_query->fetchrow_array;27842785if(defined($message) )2786{2787$message.=" "if($message=~/\n$/);2788return$message;2789}27902791my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2792shift@lineswhile($lines[0] =~/\S/);2793$message=join("",@lines);2794$message.=" "if($message=~/\n$/);2795return$message;2796}27972798=head2 gethistory27992800This function takes a filename (with path) argument and returns an arrayofarrays2801containing revision,filehash,commithash ordered by revision descending28022803=cut2804sub gethistory2805{2806my$self=shift;2807my$filename=shift;28082809my$db_query;2810$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2811$db_query->execute($filename);28122813return$db_query->fetchall_arrayref;2814}28152816=head2 gethistorydense28172818This function takes a filename (with path) argument and returns an arrayofarrays2819containing revision,filehash,commithash ordered by revision descending.28202821This version of gethistory skips deleted entries -- so it is useful for annotate.2822The 'dense' part is a reference to a '--dense' option available for git-rev-list2823and other git tools that depend on it.28242825=cut2826sub gethistorydense2827{2828my$self=shift;2829my$filename=shift;28302831my$db_query;2832$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2833$db_query->execute($filename);28342835return$db_query->fetchall_arrayref;2836}28372838=head2 in_array()28392840from Array::PAT - mimics the in_array() function2841found in PHP. Yuck but works for small arrays.28422843=cut2844sub in_array2845{2846my($check,@array) =@_;2847my$retval=0;2848foreachmy$test(@array){2849if($checkeq$test){2850$retval=1;2851}2852}2853return$retval;2854}28552856=head2 safe_pipe_capture28572858an alternative to `command` that allows input to be passed as an array2859to work around shell problems with weird characters in arguments28602861=cut2862sub safe_pipe_capture {28632864my@output;28652866if(my$pid=open my$child,'-|') {2867@output= (<$child>);2868close$childor die join(' ',@_).":$!$?";2869}else{2870exec(@_)or die"$!$?";# exec() can fail the executable can't be found2871}2872returnwantarray?@output:join('',@output);2873}28742875=head2 mangle_dirname28762877create a string from a directory name that is suitable to use as2878part of a filename, mainly by converting all chars except \w.- to _28792880=cut2881sub mangle_dirname {2882my$dirname=shift;2883return unlessdefined$dirname;28842885$dirname=~s/[^\w.-]/_/g;28862887return$dirname;2888}288928901;