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; 20 21use Fcntl; 22use File::Temp qw/tempdir tempfile/; 23use File::Basename; 24 25my$log= GITCVS::log->new(); 26my$cfg; 27 28my$DATE_LIST= { 29 Jan =>"01", 30 Feb =>"02", 31 Mar =>"03", 32 Apr =>"04", 33 May =>"05", 34 Jun =>"06", 35 Jul =>"07", 36 Aug =>"08", 37 Sep =>"09", 38 Oct =>"10", 39 Nov =>"11", 40 Dec =>"12", 41}; 42 43# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 44$| =1; 45 46#### Definition and mappings of functions #### 47 48my$methods= { 49'Root'=> \&req_Root, 50'Valid-responses'=> \&req_Validresponses, 51'valid-requests'=> \&req_validrequests, 52'Directory'=> \&req_Directory, 53'Entry'=> \&req_Entry, 54'Modified'=> \&req_Modified, 55'Unchanged'=> \&req_Unchanged, 56'Questionable'=> \&req_Questionable, 57'Argument'=> \&req_Argument, 58'Argumentx'=> \&req_Argument, 59'expand-modules'=> \&req_expandmodules, 60'add'=> \&req_add, 61'remove'=> \&req_remove, 62'co'=> \&req_co, 63'update'=> \&req_update, 64'ci'=> \&req_ci, 65'diff'=> \&req_diff, 66'log'=> \&req_log, 67'rlog'=> \&req_log, 68'tag'=> \&req_CATCHALL, 69'status'=> \&req_status, 70'admin'=> \&req_CATCHALL, 71'history'=> \&req_CATCHALL, 72'watchers'=> \&req_CATCHALL, 73'editors'=> \&req_CATCHALL, 74'annotate'=> \&req_annotate, 75'Global_option'=> \&req_Globaloption, 76#'annotate' => \&req_CATCHALL, 77}; 78 79############################################## 80 81 82# $state holds all the bits of information the clients sends us that could 83# potentially be useful when it comes to actually _doing_ something. 84my$state= {}; 85$log->info("--------------- STARTING -----------------"); 86 87my$TEMP_DIR= tempdir( CLEANUP =>1); 88$log->debug("Temporary directory is '$TEMP_DIR'"); 89 90# Keep going until the client closes the connection 91while(<STDIN>) 92{ 93chomp; 94 95# Check to see if we've seen this method, and call appropiate function. 96if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 97{ 98# use the $methods hash to call the appropriate sub for this command 99#$log->info("Method : $1"); 100&{$methods->{$1}}($1,$2); 101}else{ 102# log fatal because we don't understand this function. If this happens 103# we're fairly screwed because we don't know if the client is expecting 104# a response. If it is, the client will hang, we'll hang, and the whole 105# thing will be custard. 106$log->fatal("Don't understand command$_\n"); 107die("Unknown command$_"); 108} 109} 110 111$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 112$log->info("--------------- FINISH -----------------"); 113 114# Magic catchall method. 115# This is the method that will handle all commands we haven't yet 116# implemented. It simply sends a warning to the log file indicating a 117# command that hasn't been implemented has been invoked. 118sub req_CATCHALL 119{ 120my($cmd,$data) =@_; 121$log->warn("Unhandled command : req_$cmd:$data"); 122} 123 124 125# Root pathname \n 126# Response expected: no. Tell the server which CVSROOT to use. Note that 127# pathname is a local directory and not a fully qualified CVSROOT variable. 128# pathname must already exist; if creating a new root, use the init 129# request, not Root. pathname does not include the hostname of the server, 130# how to access the server, etc.; by the time the CVS protocol is in use, 131# connection, authentication, etc., are already taken care of. The Root 132# request must be sent only once, and it must be sent before any requests 133# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 134sub req_Root 135{ 136my($cmd,$data) =@_; 137$log->debug("req_Root :$data"); 138 139$state->{CVSROOT} =$data; 140 141$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 142 143foreachmy$line(`git-var -l`) 144{ 145next unless($line=~/^(.*?)\.(.*?)=(.*)$/); 146$cfg->{$1}{$2} =$3; 147} 148 149unless(defined($cfg->{gitcvs}{enabled} )and$cfg->{gitcvs}{enabled} =~/^\s*(1|true|yes)\s*$/i) 150{ 151print"E GITCVS emulation needs to be enabled on this repo\n"; 152print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 153print"E\n"; 154print"error 1 GITCVS emulation disabled\n"; 155} 156 157if(defined($cfg->{gitcvs}{logfile} ) ) 158{ 159$log->setfile($cfg->{gitcvs}{logfile}); 160}else{ 161$log->nofile(); 162} 163} 164 165# Global_option option \n 166# Response expected: no. Transmit one of the global options `-q', `-Q', 167# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 168# variations (such as combining of options) are allowed. For graceful 169# handling of valid-requests, it is probably better to make new global 170# options separate requests, rather than trying to add them to this 171# request. 172sub req_Globaloption 173{ 174my($cmd,$data) =@_; 175$log->debug("req_Globaloption :$data"); 176 177# TODO : is this data useful ??? 178} 179 180# Valid-responses request-list \n 181# Response expected: no. Tell the server what responses the client will 182# accept. request-list is a space separated list of tokens. 183sub req_Validresponses 184{ 185my($cmd,$data) =@_; 186$log->debug("req_Validrepsonses :$data"); 187 188# TODO : re-enable this, currently it's not particularly useful 189#$state->{validresponses} = [ split /\s+/, $data ]; 190} 191 192# valid-requests \n 193# Response expected: yes. Ask the server to send back a Valid-requests 194# response. 195sub req_validrequests 196{ 197my($cmd,$data) =@_; 198 199$log->debug("req_validrequests"); 200 201$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 202$log->debug("SEND : ok"); 203 204print"Valid-requests ".join(" ",keys%$methods) ."\n"; 205print"ok\n"; 206} 207 208# Directory local-directory \n 209# Additional data: repository \n. Response expected: no. Tell the server 210# what directory to use. The repository should be a directory name from a 211# previous server response. Note that this both gives a default for Entry 212# and Modified and also for ci and the other commands; normal usage is to 213# send Directory for each directory in which there will be an Entry or 214# Modified, and then a final Directory for the original directory, then the 215# command. The local-directory is relative to the top level at which the 216# command is occurring (i.e. the last Directory which is sent before the 217# command); to indicate that top level, `.' should be sent for 218# local-directory. 219sub req_Directory 220{ 221my($cmd,$data) =@_; 222 223my$repository= <STDIN>; 224chomp$repository; 225 226 227$state->{localdir} =$data; 228$state->{repository} =$repository; 229$state->{directory} =$repository; 230$state->{directory} =~s/^$state->{CVSROOT}\///; 231$state->{module} =$1if($state->{directory} =~s/^(.*?)(\/|$)//); 232$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 233 234$log->debug("req_Directory : localdir=$datarepository=$repositorydirectory=$state->{directory} module=$state->{module}"); 235} 236 237# Entry entry-line \n 238# Response expected: no. Tell the server what version of a file is on the 239# local machine. The name in entry-line is a name relative to the directory 240# most recently specified with Directory. If the user is operating on only 241# some files in a directory, Entry requests for only those files need be 242# included. If an Entry request is sent without Modified, Is-modified, or 243# Unchanged, it means the file is lost (does not exist in the working 244# directory). If both Entry and one of Modified, Is-modified, or Unchanged 245# are sent for the same file, Entry must be sent first. For a given file, 246# one can send Modified, Is-modified, or Unchanged, but not more than one 247# of these three. 248sub req_Entry 249{ 250my($cmd,$data) =@_; 251 252$log->debug("req_Entry :$data"); 253 254my@data=split(/\//,$data); 255 256$state->{entries}{$state->{directory}.$data[1]} = { 257 revision =>$data[2], 258 conflict =>$data[3], 259 options =>$data[4], 260 tag_or_date =>$data[5], 261}; 262} 263 264# add \n 265# Response expected: yes. Add a file or directory. This uses any previous 266# Argument, Directory, Entry, or Modified requests, if they have been sent. 267# The last Directory sent specifies the working directory at the time of 268# the operation. To add a directory, send the directory to be added using 269# Directory and Argument requests. 270sub req_add 271{ 272my($cmd,$data) =@_; 273 274 argsplit("add"); 275 276my$addcount=0; 277 278foreachmy$filename( @{$state->{args}} ) 279{ 280$filename= filecleanup($filename); 281 282unless(defined($state->{entries}{$filename}{modified_filename} ) ) 283{ 284print"E cvs add: nothing known about `$filename'\n"; 285next; 286} 287# TODO : check we're not squashing an already existing file 288if(defined($state->{entries}{$filename}{revision} ) ) 289{ 290print"E cvs add: `$filename' has already been entered\n"; 291next; 292} 293 294 295my($filepart,$dirpart) = filenamesplit($filename); 296 297print"E cvs add: scheduling file `$filename' for addition\n"; 298 299print"Checked-in$dirpart\n"; 300print"$filename\n"; 301print"/$filepart/0///\n"; 302 303$addcount++; 304} 305 306if($addcount==1) 307{ 308print"E cvs add: use `cvs commit' to add this file permanently\n"; 309} 310elsif($addcount>1) 311{ 312print"E cvs add: use `cvs commit' to add these files permanently\n"; 313} 314 315print"ok\n"; 316} 317 318# remove \n 319# Response expected: yes. Remove a file. This uses any previous Argument, 320# Directory, Entry, or Modified requests, if they have been sent. The last 321# Directory sent specifies the working directory at the time of the 322# operation. Note that this request does not actually do anything to the 323# repository; the only effect of a successful remove request is to supply 324# the client with a new entries line containing `-' to indicate a removed 325# file. In fact, the client probably could perform this operation without 326# contacting the server, although using remove may cause the server to 327# perform a few more checks. The client sends a subsequent ci request to 328# actually record the removal in the repository. 329sub req_remove 330{ 331my($cmd,$data) =@_; 332 333 argsplit("remove"); 334 335# Grab a handle to the SQLite db and do any necessary updates 336my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 337$updater->update(); 338 339#$log->debug("add state : " . Dumper($state)); 340 341my$rmcount=0; 342 343foreachmy$filename( @{$state->{args}} ) 344{ 345$filename= filecleanup($filename); 346 347if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 348{ 349print"E cvs remove: file `$filename' still in working directory\n"; 350next; 351} 352 353my$meta=$updater->getmeta($filename); 354my$wrev= revparse($filename); 355 356unless(defined($wrev) ) 357{ 358print"E cvs remove: nothing known about `$filename'\n"; 359next; 360} 361 362if(defined($wrev)and$wrev<0) 363{ 364print"E cvs remove: file `$filename' already scheduled for removal\n"; 365next; 366} 367 368unless($wrev==$meta->{revision} ) 369{ 370# TODO : not sure if the format of this message is quite correct. 371print"E cvs remove: Up to date check failed for `$filename'\n"; 372next; 373} 374 375 376my($filepart,$dirpart) = filenamesplit($filename); 377 378print"E cvs remove: scheduling `$filename' for removal\n"; 379 380print"Checked-in$dirpart\n"; 381print"$filename\n"; 382print"/$filepart/-1.$wrev///\n"; 383 384$rmcount++; 385} 386 387if($rmcount==1) 388{ 389print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 390} 391elsif($rmcount>1) 392{ 393print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 394} 395 396print"ok\n"; 397} 398 399# Modified filename \n 400# Response expected: no. Additional data: mode, \n, file transmission. Send 401# the server a copy of one locally modified file. filename is a file within 402# the most recent directory sent with Directory; it must not contain `/'. 403# If the user is operating on only some files in a directory, only those 404# files need to be included. This can also be sent without Entry, if there 405# is no entry for the file. 406sub req_Modified 407{ 408my($cmd,$data) =@_; 409 410my$mode= <STDIN>; 411chomp$mode; 412my$size= <STDIN>; 413chomp$size; 414 415# Grab config information 416my$blocksize=8192; 417my$bytesleft=$size; 418my$tmp; 419 420# Get a filehandle/name to write it to 421my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 422 423# Loop over file data writing out to temporary file. 424while($bytesleft) 425{ 426$blocksize=$bytesleftif($bytesleft<$blocksize); 427read STDIN,$tmp,$blocksize; 428print$fh $tmp; 429$bytesleft-=$blocksize; 430} 431 432close$fh; 433 434# Ensure we have something sensible for the file mode 435if($mode=~/u=(\w+)/) 436{ 437$mode=$1; 438}else{ 439$mode="rw"; 440} 441 442# Save the file data in $state 443$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 444$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 445$state->{entries}{$state->{directory}.$data}{modified_hash} =`git-hash-object$filename`; 446$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 447 448 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 449} 450 451# Unchanged filename\n 452# Response expected: no. Tell the server that filename has not been 453# modified in the checked out directory. The filename is a file within the 454# most recent directory sent with Directory; it must not contain `/'. 455sub req_Unchanged 456{ 457 my ($cmd,$data) =@_; 458 459$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 460 461 #$log->debug("req_Unchanged :$data"); 462} 463 464# Questionable filename\n 465# Response expected: no. Additional data: no. 466# Tell the server to check whether filename should be ignored, 467# and if not, next time the server sends responses, send (in 468# a M response) `?' followed by the directory and filename. 469# filename must not contain `/'; it needs to be a file in the 470# directory named by the most recent Directory request. 471sub req_Questionable 472{ 473my($cmd,$data) =@_; 474 475$state->{entries}{$state->{directory}.$data}{questionable} =1; 476 477#$log->debug("req_Questionable : $data"); 478} 479 480# Argument text \n 481# Response expected: no. Save argument for use in a subsequent command. 482# Arguments accumulate until an argument-using command is given, at which 483# point they are forgotten. 484# Argumentx text \n 485# Response expected: no. Append \n followed by text to the current argument 486# being saved. 487sub req_Argument 488{ 489my($cmd,$data) =@_; 490 491# TODO : Not quite sure how Argument and Argumentx differ, but I assume 492# it's for multi-line arguments ... somehow ... 493 494$log->debug("$cmd:$data"); 495 496push@{$state->{arguments}},$data; 497} 498 499# expand-modules \n 500# Response expected: yes. Expand the modules which are specified in the 501# arguments. Returns the data in Module-expansion responses. Note that the 502# server can assume that this is checkout or export, not rtag or rdiff; the 503# latter do not access the working directory and thus have no need to 504# expand modules on the client side. Expand may not be the best word for 505# what this request does. It does not necessarily tell you all the files 506# contained in a module, for example. Basically it is a way of telling you 507# which working directories the server needs to know about in order to 508# handle a checkout of the specified modules. For example, suppose that the 509# server has a module defined by 510# aliasmodule -a 1dir 511# That is, one can check out aliasmodule and it will take 1dir in the 512# repository and check it out to 1dir in the working directory. Now suppose 513# the client already has this module checked out and is planning on using 514# the co request to update it. Without using expand-modules, the client 515# would have two bad choices: it could either send information about all 516# working directories under the current directory, which could be 517# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 518# stands for 1dir, and neglect to send information for 1dir, which would 519# lead to incorrect operation. With expand-modules, the client would first 520# ask for the module to be expanded: 521sub req_expandmodules 522{ 523my($cmd,$data) =@_; 524 525 argsplit(); 526 527$log->debug("req_expandmodules : ". (defined($data) ?$data:"[NULL]") ); 528 529unless(ref$state->{arguments}eq"ARRAY") 530{ 531print"ok\n"; 532return; 533} 534 535foreachmy$module( @{$state->{arguments}} ) 536{ 537$log->debug("SEND : Module-expansion$module"); 538print"Module-expansion$module\n"; 539} 540 541print"ok\n"; 542 statecleanup(); 543} 544 545# co \n 546# Response expected: yes. Get files from the repository. This uses any 547# previous Argument, Directory, Entry, or Modified requests, if they have 548# been sent. Arguments to this command are module names; the client cannot 549# know what directories they correspond to except by (1) just sending the 550# co request, and then seeing what directory names the server sends back in 551# its responses, and (2) the expand-modules request. 552sub req_co 553{ 554my($cmd,$data) =@_; 555 556 argsplit("co"); 557 558my$module=$state->{args}[0]; 559my$checkout_path=$module; 560 561# use the user specified directory if we're given it 562$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 563 564$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 565 566$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 567 568$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 569 570# Grab a handle to the SQLite db and do any necessary updates 571my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 572$updater->update(); 573 574# instruct the client that we're checking out to $checkout_path 575print"E cvs server: updating$checkout_path\n"; 576 577foreachmy$git( @{$updater->gethead} ) 578{ 579# Don't want to check out deleted files 580next if($git->{filehash}eq"deleted"); 581 582($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 583 584# modification time of this file 585print"Mod-time$git->{modified}\n"; 586 587# print some information to the client 588print"MT +updated\n"; 589print"MT text U\n"; 590if(defined($git->{dir} )and$git->{dir}ne"./") 591{ 592print"MT fname$checkout_path/$git->{dir}$git->{name}\n"; 593}else{ 594print"MT fname$checkout_path/$git->{name}\n"; 595} 596print"MT newline\n"; 597print"MT -updated\n"; 598 599# instruct client we're sending a file to put in this path 600print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 601 602print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 603 604# this is an "entries" line 605print"/$git->{name}/1.$git->{revision}///\n"; 606# permissions 607print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 608 609# transmit file 610 transmitfile($git->{filehash}); 611} 612 613print"ok\n"; 614 615 statecleanup(); 616} 617 618# update \n 619# Response expected: yes. Actually do a cvs update command. This uses any 620# previous Argument, Directory, Entry, or Modified requests, if they have 621# been sent. The last Directory sent specifies the working directory at the 622# time of the operation. The -I option is not used--files which the client 623# can decide whether to ignore are not mentioned and the client sends the 624# Questionable request for others. 625sub req_update 626{ 627my($cmd,$data) =@_; 628 629$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 630 631 argsplit("update"); 632 633# 634# It may just be a client exploring the available heads/modukles 635# in that case, list them as top level directories and leave it 636# at that. Eclipse uses this technique to offer you a list of 637# projects (heads in this case) to checkout. 638# 639if($state->{module}eq'') { 640print"E cvs update: Updating .\n"; 641opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 642while(my$head=readdir(HEADS)) { 643if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 644print"E cvs update: New directory `$head'\n"; 645} 646} 647closedir HEADS; 648print"ok\n"; 649return1; 650} 651 652 653# Grab a handle to the SQLite db and do any necessary updates 654my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 655 656$updater->update(); 657 658# if no files were specified, we need to work out what files we should be providing status on ... 659 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0); 660 661#$log->debug("update state : " . Dumper($state)); 662 663# foreach file specified on the commandline ... 664foreachmy$filename( @{$state->{args}} ) 665{ 666$filename= filecleanup($filename); 667 668# if we have a -C we should pretend we never saw modified stuff 669if(exists($state->{opt}{C} ) ) 670{ 671delete$state->{entries}{$filename}{modified_hash}; 672delete$state->{entries}{$filename}{modified_filename}; 673$state->{entries}{$filename}{unchanged} =1; 674} 675 676my$meta; 677if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 678{ 679$meta=$updater->getmeta($filename,$1); 680}else{ 681$meta=$updater->getmeta($filename); 682} 683 684next unless($meta->{revision} ); 685 686my$oldmeta=$meta; 687 688my$wrev= revparse($filename); 689 690# If the working copy is an old revision, lets get that version too for comparison. 691if(defined($wrev)and$wrev!=$meta->{revision} ) 692{ 693$oldmeta=$updater->getmeta($filename,$wrev); 694} 695 696#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 697 698# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified _and_ the user hasn't specified -C 699next if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{unchanged}and not exists($state->{opt}{C} ) ); 700 701if($meta->{filehash}eq"deleted") 702{ 703my($filepart,$dirpart) = filenamesplit($filename); 704 705$log->info("Removing '$filename' from working copy (no longer in the repo)"); 706 707print"E cvs update: `$filename' is no longer in the repository\n"; 708print"Removed$dirpart\n"; 709print"$filepart\n"; 710} 711elsif(not defined($state->{entries}{$filename}{modified_hash} )or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) 712{ 713$log->info("Updating '$filename'"); 714# normal update, just send the new revision (either U=Update, or A=Add, or R=Remove) 715print"MT +updated\n"; 716print"MT text U\n"; 717print"MT fname$filename\n"; 718print"MT newline\n"; 719print"MT -updated\n"; 720 721my($filepart,$dirpart) = filenamesplit($filename); 722$dirpart=~s/^$state->{directory}//; 723 724if(defined($wrev) ) 725{ 726# instruct client we're sending a file to put in this path as a replacement 727print"Update-existing$dirpart\n"; 728$log->debug("Updating existing file 'Update-existing$dirpart'"); 729}else{ 730# instruct client we're sending a file to put in this path as a new file 731print"Created$dirpart\n"; 732$log->debug("Creating new file 'Created$dirpart'"); 733} 734print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 735 736# this is an "entries" line 737$log->debug("/$filepart/1.$meta->{revision}///"); 738print"/$filepart/1.$meta->{revision}///\n"; 739 740# permissions 741$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 742print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 743 744# transmit file 745 transmitfile($meta->{filehash}); 746}else{ 747my($filepart,$dirpart) = filenamesplit($meta->{name}); 748 749my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 750 751chdir$dir; 752my$file_local=$filepart.".mine"; 753system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 754my$file_old=$filepart.".".$oldmeta->{revision}; 755 transmitfile($oldmeta->{filehash},$file_old); 756my$file_new=$filepart.".".$meta->{revision}; 757 transmitfile($meta->{filehash},$file_new); 758 759# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 760$log->info("Merging$file_local,$file_old,$file_new"); 761 762$log->debug("Temporary directory for merge is$dir"); 763 764my$return=system("merge",$file_local,$file_old,$file_new); 765$return>>=8; 766 767if($return==0) 768{ 769$log->info("Merged successfully"); 770print"M M$filename\n"; 771$log->debug("Update-existing$dirpart"); 772print"Update-existing$dirpart\n"; 773$log->debug($state->{CVSROOT} ."/$state->{module}/$filename"); 774print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 775$log->debug("/$filepart/1.$meta->{revision}///"); 776print"/$filepart/1.$meta->{revision}///\n"; 777} 778elsif($return==1) 779{ 780$log->info("Merged with conflicts"); 781print"M C$filename\n"; 782print"Update-existing$dirpart\n"; 783print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 784print"/$filepart/1.$meta->{revision}/+//\n"; 785} 786else 787{ 788$log->warn("Merge failed"); 789next; 790} 791 792# permissions 793$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 794print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 795 796# transmit file, format is single integer on a line by itself (file 797# size) followed by the file contents 798# TODO : we should copy files in blocks 799my$data=`cat$file_local`; 800$log->debug("File size : " . length($data)); 801 print length($data) . "\n"; 802 print$data; 803 804 chdir "/"; 805 } 806 807 } 808 809 print "ok\n"; 810} 811 812sub req_ci 813{ 814 my ($cmd,$data) =@_; 815 816 argsplit("ci"); 817 818 #$log->debug("State : " . Dumper($state)); 819 820$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" )); 821 822 if ( -e$state->{CVSROOT} . "/index" ) 823 { 824 print "error 1 Index already exists in git repo\n"; 825 exit; 826 } 827 828 my$lockfile= "$state->{CVSROOT}/refs/heads/$state->{module}.lock"; 829 unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) ) 830 { 831 print "error 1 Lock file '$lockfile' already exists, please try again\n"; 832 exit; 833 } 834 835 # Grab a handle to the SQLite db and do any necessary updates 836 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 837$updater->update(); 838 839 my$tmpdir= tempdir ( DIR =>$TEMP_DIR); 840 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 ); 841$log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'"); 842 843$ENV{GIT_DIR} =$state->{CVSROOT} . "/"; 844$ENV{GIT_INDEX_FILE} =$file_index; 845 846 chdir$tmpdir; 847 848 # populate the temporary index based 849 system("git-read-tree",$state->{module}); 850 unless ($?== 0) 851 { 852 die "Error running git-read-tree$state->{module}$file_index$!"; 853 } 854$log->info("Created index '$file_index' with for head$state->{module} - exit status$?"); 855 856 857 my@committedfiles= (); 858 859 # foreach file specified on the commandline ... 860 foreach my$filename( @{$state->{args}} ) 861 { 862$filename= filecleanup($filename); 863 864 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} ); 865 866 my$meta=$updater->getmeta($filename); 867 868 my$wrev= revparse($filename); 869 870 my ($filepart,$dirpart) = filenamesplit($filename); 871 872 # do a checkout of the file if it part of this tree 873 if ($wrev) { 874 system('git-checkout-index', '-f', '-u',$filename); 875 unless ($?== 0) { 876 die "Error running git-checkout-index -f -u$filename:$!"; 877 } 878 } 879 880 my$addflag= 0; 881 my$rmflag= 0; 882$rmflag= 1 if ( defined($wrev) and$wrev< 0 ); 883$addflag= 1 unless ( -e$filename); 884 885 # Do up to date checking 886 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) ) 887 { 888 # fail everything if an up to date check fails 889 print "error 1 Up to date check failed for$filename\n"; 890 close LOCKFILE; 891 unlink($lockfile); 892 chdir "/"; 893 exit; 894 } 895 896 push@committedfiles,$filename; 897$log->info("Committing$filename"); 898 899 system("mkdir","-p",$dirpart) unless ( -d$dirpart); 900 901 unless ($rmflag) 902 { 903$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename"); 904 rename$state->{entries}{$filename}{modified_filename},$filename; 905 906 # Calculate modes to remove 907 my$invmode= ""; 908 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); } 909 910$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename"); 911 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename); 912 } 913 914 if ($rmflag) 915 { 916$log->info("Removing file '$filename'"); 917 unlink($filename); 918 system("git-update-index", "--remove",$filename); 919 } 920 elsif ($addflag) 921 { 922$log->info("Adding file '$filename'"); 923 system("git-update-index", "--add",$filename); 924 } else { 925$log->info("Updating file '$filename'"); 926 system("git-update-index",$filename); 927 } 928 } 929 930 unless ( scalar(@committedfiles) > 0 ) 931 { 932 print "E No files to commit\n"; 933 print "ok\n"; 934 close LOCKFILE; 935 unlink($lockfile); 936 chdir "/"; 937 return; 938 } 939 940 my$treehash= `git-write-tree`; 941 my$parenthash= `cat $ENV{GIT_DIR}refs/heads/$state->{module}`; 942 chomp$treehash; 943 chomp$parenthash; 944 945$log->debug("Treehash :$treehash, Parenthash :$parenthash"); 946 947 # write our commit message out if we have one ... 948 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR); 949 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) ); 950 print$msg_fh"\n\nvia git-CVS emulator\n"; 951 close$msg_fh; 952 953 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`; 954$log->info("Commit hash :$commithash"); 955 956unless($commithash=~/[a-zA-Z0-9]{40}/) 957{ 958$log->warn("Commit failed (Invalid commit hash)"); 959print"error 1 Commit failed (unknown reason)\n"; 960close LOCKFILE; 961unlink($lockfile); 962chdir"/"; 963exit; 964} 965 966open FILE,">","$ENV{GIT_DIR}refs/heads/$state->{module}"; 967print FILE $commithash; 968close FILE; 969 970$updater->update(); 971 972# foreach file specified on the commandline ... 973foreachmy$filename(@committedfiles) 974{ 975$filename= filecleanup($filename); 976 977my$meta=$updater->getmeta($filename); 978 979my($filepart,$dirpart) = filenamesplit($filename); 980 981$log->debug("Checked-in$dirpart:$filename"); 982 983if($meta->{filehash}eq"deleted") 984{ 985print"Remove-entry$dirpart\n"; 986print"$filename\n"; 987}else{ 988print"Checked-in$dirpart\n"; 989print"$filename\n"; 990print"/$filepart/1.$meta->{revision}///\n"; 991} 992} 993 994close LOCKFILE; 995unlink($lockfile); 996chdir"/"; 997 998print"ok\n"; 999}10001001sub req_status1002{1003my($cmd,$data) =@_;10041005 argsplit("status");10061007$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1008#$log->debug("status state : " . Dumper($state));10091010# Grab a handle to the SQLite db and do any necessary updates1011my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1012$updater->update();10131014# if no files were specified, we need to work out what files we should be providing status on ...1015 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);10161017# foreach file specified on the commandline ...1018foreachmy$filename( @{$state->{args}} )1019{1020$filename= filecleanup($filename);10211022my$meta=$updater->getmeta($filename);1023my$oldmeta=$meta;10241025my$wrev= revparse($filename);10261027# If the working copy is an old revision, lets get that version too for comparison.1028if(defined($wrev)and$wrev!=$meta->{revision} )1029{1030$oldmeta=$updater->getmeta($filename,$wrev);1031}10321033# TODO : All possible statuses aren't yet implemented1034my$status;1035# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1036$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1037and1038( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1039or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1040);10411042# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1043$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1044and1045($state->{entries}{$filename}{unchanged}1046or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1047);10481049# Need checkout if it exists in the repo but doesn't have a working copy1050$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );10511052# Locally modified if working copy and repo copy have the same revision but there are local changes1053$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );10541055# Needs Merge if working copy revision is less than repo copy and there are local changes1056$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );10571058$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1059$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1060$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1061$status||="File had conflicts on merge"if(0);10621063$status||="Unknown";10641065print"M ===================================================================\n";1066print"M File:$filename\tStatus:$status\n";1067if(defined($state->{entries}{$filename}{revision}) )1068{1069print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1070}else{1071print"M Working revision:\tNo entry for$filename\n";1072}1073if(defined($meta->{revision}) )1074{1075print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{repository}/$filename,v\n";1076print"M Sticky Tag:\t\t(none)\n";1077print"M Sticky Date:\t\t(none)\n";1078print"M Sticky Options:\t\t(none)\n";1079}else{1080print"M Repository revision:\tNo revision control file\n";1081}1082print"M\n";1083}10841085print"ok\n";1086}10871088sub req_diff1089{1090my($cmd,$data) =@_;10911092 argsplit("diff");10931094$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1095#$log->debug("status state : " . Dumper($state));10961097my($revision1,$revision2);1098if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1099{1100$revision1=$state->{opt}{r}[0];1101$revision2=$state->{opt}{r}[1];1102}else{1103$revision1=$state->{opt}{r};1104}11051106$revision1=~s/^1\.//if(defined($revision1) );1107$revision2=~s/^1\.//if(defined($revision2) );11081109$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );11101111# Grab a handle to the SQLite db and do any necessary updates1112my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1113$updater->update();11141115# if no files were specified, we need to work out what files we should be providing status on ...1116 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);11171118# foreach file specified on the commandline ...1119foreachmy$filename( @{$state->{args}} )1120{1121$filename= filecleanup($filename);11221123my($fh,$file1,$file2,$meta1,$meta2,$filediff);11241125my$wrev= revparse($filename);11261127# We need _something_ to diff against1128next unless(defined($wrev) );11291130# if we have a -r switch, use it1131if(defined($revision1) )1132{1133(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1134$meta1=$updater->getmeta($filename,$revision1);1135unless(defined($meta1)and$meta1->{filehash}ne"deleted")1136{1137print"E File$filenameat revision 1.$revision1doesn't exist\n";1138next;1139}1140 transmitfile($meta1->{filehash},$file1);1141}1142# otherwise we just use the working copy revision1143else1144{1145(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1146$meta1=$updater->getmeta($filename,$wrev);1147 transmitfile($meta1->{filehash},$file1);1148}11491150# if we have a second -r switch, use it too1151if(defined($revision2) )1152{1153(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1154$meta2=$updater->getmeta($filename,$revision2);11551156unless(defined($meta2)and$meta2->{filehash}ne"deleted")1157{1158print"E File$filenameat revision 1.$revision2doesn't exist\n";1159next;1160}11611162 transmitfile($meta2->{filehash},$file2);1163}1164# otherwise we just use the working copy1165else1166{1167$file2=$state->{entries}{$filename}{modified_filename};1168}11691170# if we have been given -r, and we don't have a $file2 yet, lets get one1171if(defined($revision1)and not defined($file2) )1172{1173(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1174$meta2=$updater->getmeta($filename,$wrev);1175 transmitfile($meta2->{filehash},$file2);1176}11771178# We need to have retrieved something useful1179next unless(defined($meta1) );11801181# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1182next if(not defined($meta2)and$wrev==$meta1->{revision}1183and1184( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1185or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1186);11871188# Apparently we only show diffs for locally modified files1189next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );11901191print"M Index:$filename\n";1192print"M ===================================================================\n";1193print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1194print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1195print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1196print"M diff ";1197foreachmy$opt(keys%{$state->{opt}} )1198{1199if(ref$state->{opt}{$opt}eq"ARRAY")1200{1201foreachmy$value( @{$state->{opt}{$opt}} )1202{1203print"-$opt$value";1204}1205}else{1206print"-$opt";1207print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1208}1209}1210print"$filename\n";12111212$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));12131214($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);12151216if(exists$state->{opt}{u} )1217{1218system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1219}else{1220system("diff$file1$file2>$filediff");1221}12221223while( <$fh> )1224{1225print"M$_";1226}1227close$fh;1228}12291230print"ok\n";1231}12321233sub req_log1234{1235my($cmd,$data) =@_;12361237 argsplit("log");12381239$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1240#$log->debug("log state : " . Dumper($state));12411242my($minrev,$maxrev);1243if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1244{1245my$control=$2;1246$minrev=$1;1247$maxrev=$3;1248$minrev=~s/^1\.//if(defined($minrev) );1249$maxrev=~s/^1\.//if(defined($maxrev) );1250$minrev++if(defined($minrev)and$controleq"::");1251}12521253# Grab a handle to the SQLite db and do any necessary updates1254my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1255$updater->update();12561257# if no files were specified, we need to work out what files we should be providing status on ...1258 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);12591260# foreach file specified on the commandline ...1261foreachmy$filename( @{$state->{args}} )1262{1263$filename= filecleanup($filename);12641265my$headmeta=$updater->getmeta($filename);12661267my$revisions=$updater->getlog($filename);1268my$totalrevisions=scalar(@$revisions);12691270if(defined($minrev) )1271{1272$log->debug("Removing revisions less than$minrev");1273while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1274{1275pop@$revisions;1276}1277}1278if(defined($maxrev) )1279{1280$log->debug("Removing revisions greater than$maxrev");1281while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1282{1283shift@$revisions;1284}1285}12861287next unless(scalar(@$revisions) );12881289print"M\n";1290print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1291print"M Working file:$filename\n";1292print"M head: 1.$headmeta->{revision}\n";1293print"M branch:\n";1294print"M locks: strict\n";1295print"M access list:\n";1296print"M symbolic names:\n";1297print"M keyword substitution: kv\n";1298print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1299print"M description:\n";13001301foreachmy$revision(@$revisions)1302{1303print"M ----------------------------\n";1304print"M revision 1.$revision->{revision}\n";1305# reformat the date for log output1306$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}) );1307$revision->{author} =~s/\s+.*//;1308$revision->{author} =~s/^(.{8}).*/$1/;1309print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1310my$commitmessage=$updater->commitmessage($revision->{commithash});1311$commitmessage=~s/^/M /mg;1312print$commitmessage."\n";1313}1314print"M =============================================================================\n";1315}13161317print"ok\n";1318}13191320sub req_annotate1321{1322my($cmd,$data) =@_;13231324 argsplit("annotate");13251326$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1327#$log->debug("status state : " . Dumper($state));13281329# Grab a handle to the SQLite db and do any necessary updates1330my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1331$updater->update();13321333# if no files were specified, we need to work out what files we should be providing annotate on ...1334 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);13351336# we'll need a temporary checkout dir1337my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1338my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1339$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");13401341$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1342$ENV{GIT_INDEX_FILE} =$file_index;13431344chdir$tmpdir;13451346# foreach file specified on the commandline ...1347foreachmy$filename( @{$state->{args}} )1348{1349$filename= filecleanup($filename);13501351my$meta=$updater->getmeta($filename);13521353next unless($meta->{revision} );13541355# get all the commits that this file was in1356# in dense format -- aka skip dead revisions1357my$revisions=$updater->gethistorydense($filename);1358my$lastseenin=$revisions->[0][2];13591360# populate the temporary index based on the latest commit were we saw1361# the file -- but do it cheaply without checking out any files1362# TODO: if we got a revision from the client, use that instead1363# to look up the commithash in sqlite (still good to default to1364# the current head as we do now)1365system("git-read-tree",$lastseenin);1366unless($?==0)1367{1368die"Error running git-read-tree$lastseenin$file_index$!";1369}1370$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");13711372# do a checkout of the file1373system('git-checkout-index','-f','-u',$filename);1374unless($?==0) {1375die"Error running git-checkout-index -f -u$filename:$!";1376}13771378$log->info("Annotate$filename");13791380# Prepare a file with the commits from the linearized1381# history that annotate should know about. This prevents1382# git-jsannotate telling us about commits we are hiding1383# from the client.13841385open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1386for(my$i=0;$i<@$revisions;$i++)1387{1388print ANNOTATEHINTS $revisions->[$i][2];1389if($i+1<@$revisions) {# have we got a parent?1390print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1391}1392print ANNOTATEHINTS "\n";1393}13941395print ANNOTATEHINTS "\n";1396close ANNOTATEHINTS;13971398my$annotatecmd='git-annotate';1399open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1400or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1401my$metadata= {};1402print"E Annotations for$filename\n";1403print"E ***************\n";1404while( <ANNOTATE> )1405{1406if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1407{1408my$commithash=$1;1409my$data=$2;1410unless(defined($metadata->{$commithash} ) )1411{1412$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1413$metadata->{$commithash}{author} =~s/\s+.*//;1414$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1415$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1416}1417printf("M 1.%-5d (%-8s%10s):%s\n",1418$metadata->{$commithash}{revision},1419$metadata->{$commithash}{author},1420$metadata->{$commithash}{modified},1421$data1422);1423}else{1424$log->warn("Error in annotate output! LINE:$_");1425print"E Annotate error\n";1426next;1427}1428}1429close ANNOTATE;1430}14311432# done; get out of the tempdir1433chdir"/";14341435print"ok\n";14361437}14381439# This method takes the state->{arguments} array and produces two new arrays.1440# The first is $state->{args} which is everything before the '--' argument, and1441# the second is $state->{files} which is everything after it.1442sub argsplit1443{1444return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");14451446my$type=shift;14471448$state->{args} = [];1449$state->{files} = [];1450$state->{opt} = {};14511452if(defined($type) )1453{1454my$opt= {};1455$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");1456$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1457$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");1458$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1459$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1460$opt= { k =>1, m =>1}if($typeeq"add");1461$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1462$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");146314641465while(scalar( @{$state->{arguments}} ) >0)1466{1467my$arg=shift@{$state->{arguments}};14681469next if($argeq"--");1470next unless($arg=~/\S/);14711472# if the argument looks like a switch1473if($arg=~/^-(\w)(.*)/)1474{1475# if it's a switch that takes an argument1476if($opt->{$1} )1477{1478# If this switch has already been provided1479if($opt->{$1} >1and exists($state->{opt}{$1} ) )1480{1481$state->{opt}{$1} = [$state->{opt}{$1} ];1482if(length($2) >0)1483{1484push@{$state->{opt}{$1}},$2;1485}else{1486push@{$state->{opt}{$1}},shift@{$state->{arguments}};1487}1488}else{1489# if there's extra data in the arg, use that as the argument for the switch1490if(length($2) >0)1491{1492$state->{opt}{$1} =$2;1493}else{1494$state->{opt}{$1} =shift@{$state->{arguments}};1495}1496}1497}else{1498$state->{opt}{$1} =undef;1499}1500}1501else1502{1503push@{$state->{args}},$arg;1504}1505}1506}1507else1508{1509my$mode=0;15101511foreachmy$value( @{$state->{arguments}} )1512{1513if($valueeq"--")1514{1515$mode++;1516next;1517}1518push@{$state->{args}},$valueif($mode==0);1519push@{$state->{files}},$valueif($mode==1);1520}1521}1522}15231524# This method uses $state->{directory} to populate $state->{args} with a list of filenames1525sub argsfromdir1526{1527my$updater=shift;15281529$state->{args} = [];15301531foreachmy$file( @{$updater->gethead} )1532{1533next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1534next unless($file->{name} =~s/^$state->{directory}//);1535push@{$state->{args}},$file->{name};1536}1537}15381539# This method cleans up the $state variable after a command that uses arguments has run1540sub statecleanup1541{1542$state->{files} = [];1543$state->{args} = [];1544$state->{arguments} = [];1545$state->{entries} = {};1546}15471548sub revparse1549{1550my$filename=shift;15511552returnundefunless(defined($state->{entries}{$filename}{revision} ) );15531554return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1555return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);15561557returnundef;1558}15591560# This method takes a file hash and does a CVS "file transfer" which transmits the1561# size of the file, and then the file contents.1562# If a second argument $targetfile is given, the file is instead written out to1563# a file by the name of $targetfile1564sub transmitfile1565{1566my$filehash=shift;1567my$targetfile=shift;15681569if(defined($filehash)and$filehasheq"deleted")1570{1571$log->warn("filehash is 'deleted'");1572return;1573}15741575die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);15761577my$type=`git-cat-file -t$filehash`;1578 chomp$type;15791580 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );15811582 my$size= `git-cat-file -s $filehash`;1583chomp$size;15841585$log->debug("transmitfile($filehash) size=$size, type=$type");15861587if(open my$fh,'-|',"git-cat-file","blob",$filehash)1588{1589if(defined($targetfile) )1590{1591open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1592print NEWFILE $_while( <$fh> );1593close NEWFILE;1594}else{1595print"$size\n";1596printwhile( <$fh> );1597}1598close$fhor die("Couldn't close filehandle for transmitfile()");1599}else{1600die("Couldn't execute git-cat-file");1601}1602}16031604# This method takes a file name, and returns ( $dirpart, $filepart ) which1605# refers to the directory porition and the file portion of the filename1606# respectively1607sub filenamesplit1608{1609my$filename=shift;16101611my($filepart,$dirpart) = ($filename,".");1612($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1613$dirpart.="/";16141615return($filepart,$dirpart);1616}16171618sub filecleanup1619{1620my$filename=shift;16211622returnundefunless(defined($filename));1623if($filename=~/^\// )1624{1625print"E absolute filenames '$filename' not supported by server\n";1626returnundef;1627}16281629$filename=~s/^\.\///g;1630$filename=$state->{directory} .$filename;16311632return$filename;1633}16341635package GITCVS::log;16361637####1638#### Copyright The Open University UK - 2006.1639####1640#### Authors: Martyn Smith <martyn@catalyst.net.nz>1641#### Martin Langhoff <martin@catalyst.net.nz>1642####1643####16441645use strict;1646use warnings;16471648=head1 NAME16491650GITCVS::log16511652=head1 DESCRIPTION16531654This module provides very crude logging with a similar interface to1655Log::Log4perl16561657=head1 METHODS16581659=cut16601661=head2 new16621663Creates a new log object, optionally you can specify a filename here to1664indicate the file to log to. If no log file is specified, you can specifiy one1665later with method setfile, or indicate you no longer want logging with method1666nofile.16671668Until one of these methods is called, all log calls will buffer messages ready1669to write out.16701671=cut1672sub new1673{1674my$class=shift;1675my$filename=shift;16761677my$self= {};16781679bless$self,$class;16801681if(defined($filename) )1682{1683open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1684}16851686return$self;1687}16881689=head2 setfile16901691This methods takes a filename, and attempts to open that file as the log file.1692If successful, all buffered data is written out to the file, and any further1693logging is written directly to the file.16941695=cut1696sub setfile1697{1698my$self=shift;1699my$filename=shift;17001701if(defined($filename) )1702{1703open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1704}17051706return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");17071708while(my$line=shift@{$self->{buffer}} )1709{1710print{$self->{fh}}$line;1711}1712}17131714=head2 nofile17151716This method indicates no logging is going to be used. It flushes any entries in1717the internal buffer, and sets a flag to ensure no further data is put there.17181719=cut1720sub nofile1721{1722my$self=shift;17231724$self->{nolog} =1;17251726return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");17271728$self->{buffer} = [];1729}17301731=head2 _logopen17321733Internal method. Returns true if the log file is open, false otherwise.17341735=cut1736sub _logopen1737{1738my$self=shift;17391740return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");1741return0;1742}17431744=head2 debug info warn fatal17451746These four methods are wrappers to _log. They provide the actual interface for1747logging data.17481749=cut1750sub debug {my$self=shift;$self->_log("debug",@_); }1751sub info {my$self=shift;$self->_log("info",@_); }1752subwarn{my$self=shift;$self->_log("warn",@_); }1753sub fatal {my$self=shift;$self->_log("fatal",@_); }17541755=head2 _log17561757This is an internal method called by the logging functions. It generates a1758timestamp and pushes the logged line either to file, or internal buffer.17591760=cut1761sub _log1762{1763my$self=shift;1764my$level=shift;17651766return if($self->{nolog} );17671768my@time=localtime;1769my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",1770$time[5] +1900,1771$time[4] +1,1772$time[3],1773$time[2],1774$time[1],1775$time[0],1776uc$level,1777);17781779if($self->_logopen)1780{1781print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";1782}else{1783push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";1784}1785}17861787=head2 DESTROY17881789This method simply closes the file handle if one is open17901791=cut1792sub DESTROY1793{1794my$self=shift;17951796if($self->_logopen)1797{1798close$self->{fh};1799}1800}18011802package GITCVS::updater;18031804####1805#### Copyright The Open University UK - 2006.1806####1807#### Authors: Martyn Smith <martyn@catalyst.net.nz>1808#### Martin Langhoff <martin@catalyst.net.nz>1809####1810####18111812use strict;1813use warnings;1814use DBI;18151816=head1 METHODS18171818=cut18191820=head2 new18211822=cut1823sub new1824{1825my$class=shift;1826my$config=shift;1827my$module=shift;1828my$log=shift;18291830die"Need to specify a git repository"unless(defined($config)and-d $config);1831die"Need to specify a module"unless(defined($module) );18321833$class=ref($class) ||$class;18341835my$self= {};18361837bless$self,$class;18381839$self->{dbdir} =$config."/";1840die"Database dir '$self->{dbdir}' isn't a directory"unless(defined($self->{dbdir})and-d $self->{dbdir} );18411842$self->{module} =$module;1843$self->{file} =$self->{dbdir} ."/gitcvs.$module.sqlite";18441845$self->{git_path} =$config."/";18461847$self->{log} =$log;18481849die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );18501851$self->{dbh} = DBI->connect("dbi:SQLite:dbname=".$self->{file},"","");18521853$self->{tables} = {};1854foreachmy$table($self->{dbh}->tables)1855{1856$table=~s/^"//;1857$table=~s/"$//;1858$self->{tables}{$table} =1;1859}18601861# Construct the revision table if required1862unless($self->{tables}{revision} )1863{1864$self->{dbh}->do("1865 CREATE TABLE revision (1866 name TEXT NOT NULL,1867 revision INTEGER NOT NULL,1868 filehash TEXT NOT NULL,1869 commithash TEXT NOT NULL,1870 author TEXT NOT NULL,1871 modified TEXT NOT NULL,1872 mode TEXT NOT NULL1873 )1874 ");1875}18761877# Construct the revision table if required1878unless($self->{tables}{head} )1879{1880$self->{dbh}->do("1881 CREATE TABLE head (1882 name TEXT NOT NULL,1883 revision INTEGER NOT NULL,1884 filehash TEXT NOT NULL,1885 commithash TEXT NOT NULL,1886 author TEXT NOT NULL,1887 modified TEXT NOT NULL,1888 mode TEXT NOT NULL1889 )1890 ");1891}18921893# Construct the properties table if required1894unless($self->{tables}{properties} )1895{1896$self->{dbh}->do("1897 CREATE TABLE properties (1898 key TEXT NOT NULL PRIMARY KEY,1899 value TEXT1900 )1901 ");1902}19031904# Construct the commitmsgs table if required1905unless($self->{tables}{commitmsgs} )1906{1907$self->{dbh}->do("1908 CREATE TABLE commitmsgs (1909 key TEXT NOT NULL PRIMARY KEY,1910 value TEXT1911 )1912 ");1913}19141915return$self;1916}19171918=head2 update19191920=cut1921sub update1922{1923my$self=shift;19241925# first lets get the commit list1926$ENV{GIT_DIR} =$self->{git_path};19271928# prepare database queries1929my$db_insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);1930my$db_insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);1931my$db_delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);1932my$db_insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);19331934my$commitinfo=`git-cat-file commit$self->{module} 2>&1`;1935unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)1936{1937die("Invalid module '$self->{module}'");1938}193919401941my$git_log;1942my$lastcommit=$self->_get_prop("last_commit");19431944# Start exclusive lock here...1945$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";19461947# TODO: log processing is memory bound1948# if we can parse into a 2nd file that is in reverse order1949# we can probably do something really efficient1950my@git_log_params= ('--parents','--topo-order');19511952if(defined$lastcommit) {1953push@git_log_params,"$lastcommit..$self->{module}";1954}else{1955push@git_log_params,$self->{module};1956}1957open(GITLOG,'-|','git-log',@git_log_params)or die"Cannot call git-log:$!";19581959my@commits;19601961my%commit= ();19621963while( <GITLOG> )1964{1965chomp;1966if(m/^commit\s+(.*)$/) {1967# on ^commit lines put the just seen commit in the stack1968# and prime things for the next one1969if(keys%commit) {1970my%copy=%commit;1971unshift@commits, \%copy;1972%commit= ();1973}1974my@parents=split(m/\s+/,$1);1975$commit{hash} =shift@parents;1976$commit{parents} = \@parents;1977}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {1978# on rfc822-like lines seen before we see any message,1979# lowercase the entry and put it in the hash as key-value1980$commit{lc($1)} =$2;1981}else{1982# message lines - skip initial empty line1983# and trim whitespace1984if(!exists($commit{message}) &&m/^\s*$/) {1985# define it to mark the end of headers1986$commit{message} ='';1987next;1988}1989s/^\s+//;s/\s+$//;# trim ws1990$commit{message} .=$_."\n";1991}1992}1993close GITLOG;19941995unshift@commits, \%commitif(keys%commit);19961997# Now all the commits are in the @commits bucket1998# ordered by time DESC. for each commit that needs processing,1999# determine whether it's following the last head we've seen or if2000# it's on its own branch, grab a file list, and add whatever's changed2001# NOTE: $lastcommit refers to the last commit from previous run2002# $lastpicked is the last commit we picked in this run2003my$lastpicked;2004my$head= {};2005if(defined$lastcommit) {2006$lastpicked=$lastcommit;2007}20082009my$committotal=scalar(@commits);2010my$commitcount=0;20112012# Load the head table into $head (for cached lookups during the update process)2013foreachmy$file( @{$self->gethead()} )2014{2015$head->{$file->{name}} =$file;2016}20172018foreachmy$commit(@commits)2019{2020$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2021if(defined$lastpicked)2022{2023if(!in_array($lastpicked, @{$commit->{parents}}))2024{2025# skip, we'll see this delta2026# as part of a merge later2027# warn "skipping off-track $commit->{hash}\n";2028next;2029}elsif(@{$commit->{parents}} >1) {2030# it is a merge commit, for each parent that is2031# not $lastpicked, see if we can get a log2032# from the merge-base to that parent to put it2033# in the message as a merge summary.2034my@parents= @{$commit->{parents}};2035foreachmy$parent(@parents) {2036# git-merge-base can potentially (but rarely) throw2037# several candidate merge bases. let's assume2038# that the first one is the best one.2039if($parenteq$lastpicked) {2040next;2041}2042open my$p,'git-merge-base '.$lastpicked.' '2043.$parent.'|';2044my@output= (<$p>);2045close$p;2046my$base=join('',@output);2047chomp$base;2048if($base) {2049my@merged;2050# print "want to log between $base $parent \n";2051open(GITLOG,'-|','git-log',"$base..$parent")2052or die"Cannot call git-log:$!";2053my$mergedhash;2054while(<GITLOG>) {2055chomp;2056if(!defined$mergedhash) {2057if(m/^commit\s+(.+)$/) {2058$mergedhash=$1;2059}else{2060next;2061}2062}else{2063# grab the first line that looks non-rfc8222064# aka has content after leading space2065if(m/^\s+(\S.*)$/) {2066my$title=$1;2067$title=substr($title,0,100);# truncate2068unshift@merged,"$mergedhash$title";2069undef$mergedhash;2070}2071}2072}2073close GITLOG;2074if(@merged) {2075$commit->{mergemsg} =$commit->{message};2076$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2077foreachmy$summary(@merged) {2078$commit->{mergemsg} .="\t$summary\n";2079}2080$commit->{mergemsg} .="\n\n";2081# print "Message for $commit->{hash} \n$commit->{mergemsg}";2082}2083}2084}2085}2086}20872088# convert the date to CVS-happy format2089$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);20902091if(defined($lastpicked) )2092{2093my$filepipe=open(FILELIST,'-|','git-diff-tree','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2094while( <FILELIST> )2095{2096unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o)2097{2098die("Couldn't process git-diff-tree line :$_");2099}21002101# $log->debug("File mode=$1, hash=$2, change=$3, name=$4");21022103my$git_perms="";2104$git_perms.="r"if($1&4);2105$git_perms.="w"if($1&2);2106$git_perms.="x"if($1&1);2107$git_perms="rw"if($git_permseq"");21082109if($3eq"D")2110{2111#$log->debug("DELETE $4");2112$head->{$4} = {2113 name =>$4,2114 revision =>$head->{$4}{revision} +1,2115 filehash =>"deleted",2116 commithash =>$commit->{hash},2117 modified =>$commit->{date},2118 author =>$commit->{author},2119 mode =>$git_perms,2120};2121$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2122}2123elsif($3eq"M")2124{2125#$log->debug("MODIFIED $4");2126$head->{$4} = {2127 name =>$4,2128 revision =>$head->{$4}{revision} +1,2129 filehash =>$2,2130 commithash =>$commit->{hash},2131 modified =>$commit->{date},2132 author =>$commit->{author},2133 mode =>$git_perms,2134};2135$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2136}2137elsif($3eq"A")2138{2139#$log->debug("ADDED $4");2140$head->{$4} = {2141 name =>$4,2142 revision =>1,2143 filehash =>$2,2144 commithash =>$commit->{hash},2145 modified =>$commit->{date},2146 author =>$commit->{author},2147 mode =>$git_perms,2148};2149$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2150}2151else2152{2153$log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");2154die;2155}2156}2157close FILELIST;2158}else{2159# this is used to detect files removed from the repo2160my$seen_files= {};21612162my$filepipe=open(FILELIST,'-|','git-ls-tree','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2163while( <FILELIST> )2164{2165unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o)2166{2167die("Couldn't process git-ls-tree line :$_");2168}21692170my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);21712172$seen_files->{$git_filename} =1;21732174my($oldhash,$oldrevision,$oldmode) = (2175$head->{$git_filename}{filehash},2176$head->{$git_filename}{revision},2177$head->{$git_filename}{mode}2178);21792180if($git_perms=~/^\d\d\d(\d)\d\d/o)2181{2182$git_perms="";2183$git_perms.="r"if($1&4);2184$git_perms.="w"if($1&2);2185$git_perms.="x"if($1&1);2186}else{2187$git_perms="rw";2188}21892190# unless the file exists with the same hash, we need to update it ...2191unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2192{2193my$newrevision= ($oldrevisionor0) +1;21942195$head->{$git_filename} = {2196 name =>$git_filename,2197 revision =>$newrevision,2198 filehash =>$git_hash,2199 commithash =>$commit->{hash},2200 modified =>$commit->{date},2201 author =>$commit->{author},2202 mode =>$git_perms,2203};220422052206$db_insert_rev->execute($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2207}2208}2209close FILELIST;22102211# Detect deleted files2212foreachmy$file(keys%$head)2213{2214unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2215{2216$head->{$file}{revision}++;2217$head->{$file}{filehash} ="deleted";2218$head->{$file}{commithash} =$commit->{hash};2219$head->{$file}{modified} =$commit->{date};2220$head->{$file}{author} =$commit->{author};22212222$db_insert_rev->execute($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2223}2224}2225# END : "Detect deleted files"2226}222722282229if(exists$commit->{mergemsg})2230{2231$db_insert_mergelog->execute($commit->{hash},$commit->{mergemsg});2232}22332234$lastpicked=$commit->{hash};22352236$self->_set_prop("last_commit",$commit->{hash});2237}22382239$db_delete_head->execute();2240foreachmy$file(keys%$head)2241{2242$db_insert_head->execute(2243$file,2244$head->{$file}{revision},2245$head->{$file}{filehash},2246$head->{$file}{commithash},2247$head->{$file}{modified},2248$head->{$file}{author},2249$head->{$file}{mode},2250);2251}2252# invalidate the gethead cache2253$self->{gethead_cache} =undef;225422552256# Ending exclusive lock here2257$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2258}22592260sub _headrev2261{2262my$self=shift;2263my$filename=shift;22642265my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2266$db_query->execute($filename);2267my($hash,$revision,$mode) =$db_query->fetchrow_array;22682269return($hash,$revision,$mode);2270}22712272sub _get_prop2273{2274my$self=shift;2275my$key=shift;22762277my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2278$db_query->execute($key);2279my($value) =$db_query->fetchrow_array;22802281return$value;2282}22832284sub _set_prop2285{2286my$self=shift;2287my$key=shift;2288my$value=shift;22892290my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2291$db_query->execute($value,$key);22922293unless($db_query->rows)2294{2295$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2296$db_query->execute($key,$value);2297}22982299return$value;2300}23012302=head2 gethead23032304=cut23052306sub gethead2307{2308my$self=shift;23092310return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );23112312my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head",{},1);2313$db_query->execute();23142315my$tree= [];2316while(my$file=$db_query->fetchrow_hashref)2317{2318push@$tree,$file;2319}23202321$self->{gethead_cache} =$tree;23222323return$tree;2324}23252326=head2 getlog23272328=cut23292330sub getlog2331{2332my$self=shift;2333my$filename=shift;23342335my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2336$db_query->execute($filename);23372338my$tree= [];2339while(my$file=$db_query->fetchrow_hashref)2340{2341push@$tree,$file;2342}23432344return$tree;2345}23462347=head2 getmeta23482349This function takes a filename (with path) argument and returns a hashref of2350metadata for that file.23512352=cut23532354sub getmeta2355{2356my$self=shift;2357my$filename=shift;2358my$revision=shift;23592360my$db_query;2361if(defined($revision)and$revision=~/^\d+$/)2362{2363$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2364$db_query->execute($filename,$revision);2365}2366elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2367{2368$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2369$db_query->execute($filename,$revision);2370}else{2371$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2372$db_query->execute($filename);2373}23742375return$db_query->fetchrow_hashref;2376}23772378=head2 commitmessage23792380this function takes a commithash and returns the commit message for that commit23812382=cut2383sub commitmessage2384{2385my$self=shift;2386my$commithash=shift;23872388die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);23892390my$db_query;2391$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2392$db_query->execute($commithash);23932394my($message) =$db_query->fetchrow_array;23952396if(defined($message) )2397{2398$message.=" "if($message=~/\n$/);2399return$message;2400}24012402my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2403shift@lineswhile($lines[0] =~/\S/);2404$message=join("",@lines);2405$message.=" "if($message=~/\n$/);2406return$message;2407}24082409=head2 gethistory24102411This function takes a filename (with path) argument and returns an arrayofarrays2412containing revision,filehash,commithash ordered by revision descending24132414=cut2415sub gethistory2416{2417my$self=shift;2418my$filename=shift;24192420my$db_query;2421$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2422$db_query->execute($filename);24232424return$db_query->fetchall_arrayref;2425}24262427=head2 gethistorydense24282429This function takes a filename (with path) argument and returns an arrayofarrays2430containing revision,filehash,commithash ordered by revision descending.24312432This version of gethistory skips deleted entries -- so it is useful for annotate.2433The 'dense' part is a reference to a '--dense' option available for git-rev-list2434and other git tools that depend on it.24352436=cut2437sub gethistorydense2438{2439my$self=shift;2440my$filename=shift;24412442my$db_query;2443$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2444$db_query->execute($filename);24452446return$db_query->fetchall_arrayref;2447}24482449=head2 in_array()24502451from Array::PAT - mimics the in_array() function2452found in PHP. Yuck but works for small arrays.24532454=cut2455sub in_array2456{2457my($check,@array) =@_;2458my$retval=0;2459foreachmy$test(@array){2460if($checkeq$test){2461$retval=1;2462}2463}2464return$retval;2465}24662467=head2 safe_pipe_capture24682469an alterative to `command` that allows input to be passed as an array2470to work around shell problems with weird characters in arguments24712472=cut2473sub safe_pipe_capture {24742475my@output;24762477if(my$pid=open my$child,'-|') {2478@output= (<$child>);2479close$childor die join(' ',@_).":$!$?";2480}else{2481exec(@_)or die"$!$?";# exec() can fail the executable can't be found2482}2483returnwantarray?@output:join('',@output);2484}2485248624871;