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$checkout_path=~ s|/$||;# get rid of trailing slashes 575 576# Eclipse seems to need the Clear-sticky command 577# to prepare the 'Entries' file for the new directory. 578print"Clear-sticky$checkout_path/\n"; 579print$state->{CVSROOT} ."/$module/\n"; 580print"Clear-static-directory$checkout_path/\n"; 581print$state->{CVSROOT} ."/$module/\n"; 582 583# instruct the client that we're checking out to $checkout_path 584print"E cvs checkout: Updating$checkout_path\n"; 585 586my%seendirs= (); 587my$lastdir=''; 588 589foreachmy$git( @{$updater->gethead} ) 590{ 591# Don't want to check out deleted files 592next if($git->{filehash}eq"deleted"); 593 594($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 595 596# modification time of this file 597print"Mod-time$git->{modified}\n"; 598 599# print some information to the client 600if(defined($git->{dir} )and$git->{dir}ne"./") 601{ 602print"M U$checkout_path/$git->{dir}$git->{name}\n"; 603}else{ 604print"M U$checkout_path/$git->{name}\n"; 605} 606 607if(length($git->{dir}) &&$git->{dir}ne'./' 608&&$git->{dir}ne$lastdir&& !exists($seendirs{$git->{dir}})) { 609 610# Eclipse seems to need the Clear-sticky command 611# to prepare the 'Entries' file for the new directory. 612print"Clear-sticky$checkout_path/$git->{dir}\n"; 613print$state->{CVSROOT} ."/$module/$git->{dir}\n"; 614print"Clear-static-directory$checkout_path/$git->{dir}\n"; 615print$state->{CVSROOT} ."/$module/$git->{dir}\n"; 616print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 617$lastdir=$git->{dir}; 618$seendirs{$git->{dir}} =1; 619} 620 621# instruct client we're sending a file to put in this path 622print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 623 624print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 625 626# this is an "entries" line 627print"/$git->{name}/1.$git->{revision}///\n"; 628# permissions 629print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 630 631# transmit file 632 transmitfile($git->{filehash}); 633} 634 635print"ok\n"; 636 637 statecleanup(); 638} 639 640# update \n 641# Response expected: yes. Actually do a cvs update command. This uses any 642# previous Argument, Directory, Entry, or Modified requests, if they have 643# been sent. The last Directory sent specifies the working directory at the 644# time of the operation. The -I option is not used--files which the client 645# can decide whether to ignore are not mentioned and the client sends the 646# Questionable request for others. 647sub req_update 648{ 649my($cmd,$data) =@_; 650 651$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 652 653 argsplit("update"); 654 655# 656# It may just be a client exploring the available heads/modukles 657# in that case, list them as top level directories and leave it 658# at that. Eclipse uses this technique to offer you a list of 659# projects (heads in this case) to checkout. 660# 661if($state->{module}eq'') { 662print"E cvs update: Updating .\n"; 663opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 664while(my$head=readdir(HEADS)) { 665if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 666print"E cvs update: New directory `$head'\n"; 667} 668} 669closedir HEADS; 670print"ok\n"; 671return1; 672} 673 674 675# Grab a handle to the SQLite db and do any necessary updates 676my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 677 678$updater->update(); 679 680# if no files were specified, we need to work out what files we should be providing status on ... 681 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0); 682 683#$log->debug("update state : " . Dumper($state)); 684 685# foreach file specified on the commandline ... 686foreachmy$filename( @{$state->{args}} ) 687{ 688$filename= filecleanup($filename); 689 690# if we have a -C we should pretend we never saw modified stuff 691if(exists($state->{opt}{C} ) ) 692{ 693delete$state->{entries}{$filename}{modified_hash}; 694delete$state->{entries}{$filename}{modified_filename}; 695$state->{entries}{$filename}{unchanged} =1; 696} 697 698my$meta; 699if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 700{ 701$meta=$updater->getmeta($filename,$1); 702}else{ 703$meta=$updater->getmeta($filename); 704} 705 706next unless($meta->{revision} ); 707 708my$oldmeta=$meta; 709 710my$wrev= revparse($filename); 711 712# If the working copy is an old revision, lets get that version too for comparison. 713if(defined($wrev)and$wrev!=$meta->{revision} ) 714{ 715$oldmeta=$updater->getmeta($filename,$wrev); 716} 717 718#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 719 720# Files are up to date if the working copy and repo copy have the same revision, 721# and the working copy is unmodified _and_ the user hasn't specified -C 722next if(defined($wrev) 723and defined($meta->{revision}) 724and$wrev==$meta->{revision} 725and$state->{entries}{$filename}{unchanged} 726and not exists($state->{opt}{C} ) ); 727 728# If the working copy and repo copy have the same revision, 729# but the working copy is modified, tell the client it's modified 730if(defined($wrev) 731and defined($meta->{revision}) 732and$wrev==$meta->{revision} 733and not exists($state->{opt}{C} ) ) 734{ 735$log->info("Tell the client the file is modified"); 736print"MT text U\n"; 737print"MT fname$filename\n"; 738print"MT newline\n"; 739next; 740} 741 742if($meta->{filehash}eq"deleted") 743{ 744my($filepart,$dirpart) = filenamesplit($filename); 745 746$log->info("Removing '$filename' from working copy (no longer in the repo)"); 747 748print"E cvs update: `$filename' is no longer in the repository\n"; 749print"Removed$dirpart\n"; 750print"$filepart\n"; 751} 752elsif(not defined($state->{entries}{$filename}{modified_hash} ) 753or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) 754{ 755$log->info("Updating '$filename'"); 756# normal update, just send the new revision (either U=Update, or A=Add, or R=Remove) 757print"MT +updated\n"; 758print"MT text U\n"; 759print"MT fname$filename\n"; 760print"MT newline\n"; 761print"MT -updated\n"; 762 763my($filepart,$dirpart) = filenamesplit($filename); 764$dirpart=~s/^$state->{directory}//; 765 766if(defined($wrev) ) 767{ 768# instruct client we're sending a file to put in this path as a replacement 769print"Update-existing$dirpart\n"; 770$log->debug("Updating existing file 'Update-existing$dirpart'"); 771}else{ 772# instruct client we're sending a file to put in this path as a new file 773print"Created$dirpart\n"; 774$log->debug("Creating new file 'Created$dirpart'"); 775} 776print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 777 778# this is an "entries" line 779$log->debug("/$filepart/1.$meta->{revision}///"); 780print"/$filepart/1.$meta->{revision}///\n"; 781 782# permissions 783$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 784print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 785 786# transmit file 787 transmitfile($meta->{filehash}); 788}else{ 789$log->info("Updating '$filename'"); 790my($filepart,$dirpart) = filenamesplit($meta->{name}); 791 792my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 793 794chdir$dir; 795my$file_local=$filepart.".mine"; 796system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 797my$file_old=$filepart.".".$oldmeta->{revision}; 798 transmitfile($oldmeta->{filehash},$file_old); 799my$file_new=$filepart.".".$meta->{revision}; 800 transmitfile($meta->{filehash},$file_new); 801 802# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 803$log->info("Merging$file_local,$file_old,$file_new"); 804 805$log->debug("Temporary directory for merge is$dir"); 806 807my$return=system("merge",$file_local,$file_old,$file_new); 808$return>>=8; 809 810if($return==0) 811{ 812$log->info("Merged successfully"); 813print"M M$filename\n"; 814$log->debug("Update-existing$dirpart"); 815print"Update-existing$dirpart\n"; 816$log->debug($state->{CVSROOT} ."/$state->{module}/$filename"); 817print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 818$log->debug("/$filepart/1.$meta->{revision}///"); 819print"/$filepart/1.$meta->{revision}///\n"; 820} 821elsif($return==1) 822{ 823$log->info("Merged with conflicts"); 824print"M C$filename\n"; 825print"Update-existing$dirpart\n"; 826print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 827print"/$filepart/1.$meta->{revision}/+//\n"; 828} 829else 830{ 831$log->warn("Merge failed"); 832next; 833} 834 835# permissions 836$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 837print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 838 839# transmit file, format is single integer on a line by itself (file 840# size) followed by the file contents 841# TODO : we should copy files in blocks 842my$data=`cat$file_local`; 843$log->debug("File size : " . length($data)); 844 print length($data) . "\n"; 845 print$data; 846 847 chdir "/"; 848 } 849 850 } 851 852 print "ok\n"; 853} 854 855sub req_ci 856{ 857 my ($cmd,$data) =@_; 858 859 argsplit("ci"); 860 861 #$log->debug("State : " . Dumper($state)); 862 863$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" )); 864 865 if ( -e$state->{CVSROOT} . "/index" ) 866 { 867 print "error 1 Index already exists in git repo\n"; 868 exit; 869 } 870 871 my$lockfile= "$state->{CVSROOT}/refs/heads/$state->{module}.lock"; 872 unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) ) 873 { 874 print "error 1 Lock file '$lockfile' already exists, please try again\n"; 875 exit; 876 } 877 878 # Grab a handle to the SQLite db and do any necessary updates 879 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 880$updater->update(); 881 882 my$tmpdir= tempdir ( DIR =>$TEMP_DIR); 883 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 ); 884$log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'"); 885 886$ENV{GIT_DIR} =$state->{CVSROOT} . "/"; 887$ENV{GIT_INDEX_FILE} =$file_index; 888 889 chdir$tmpdir; 890 891 # populate the temporary index based 892 system("git-read-tree",$state->{module}); 893 unless ($?== 0) 894 { 895 die "Error running git-read-tree$state->{module}$file_index$!"; 896 } 897$log->info("Created index '$file_index' with for head$state->{module} - exit status$?"); 898 899 900 my@committedfiles= (); 901 902 # foreach file specified on the commandline ... 903 foreach my$filename( @{$state->{args}} ) 904 { 905$filename= filecleanup($filename); 906 907 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} ); 908 909 my$meta=$updater->getmeta($filename); 910 911 my$wrev= revparse($filename); 912 913 my ($filepart,$dirpart) = filenamesplit($filename); 914 915 # do a checkout of the file if it part of this tree 916 if ($wrev) { 917 system('git-checkout-index', '-f', '-u',$filename); 918 unless ($?== 0) { 919 die "Error running git-checkout-index -f -u$filename:$!"; 920 } 921 } 922 923 my$addflag= 0; 924 my$rmflag= 0; 925$rmflag= 1 if ( defined($wrev) and$wrev< 0 ); 926$addflag= 1 unless ( -e$filename); 927 928 # Do up to date checking 929 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) ) 930 { 931 # fail everything if an up to date check fails 932 print "error 1 Up to date check failed for$filename\n"; 933 close LOCKFILE; 934 unlink($lockfile); 935 chdir "/"; 936 exit; 937 } 938 939 push@committedfiles,$filename; 940$log->info("Committing$filename"); 941 942 system("mkdir","-p",$dirpart) unless ( -d$dirpart); 943 944 unless ($rmflag) 945 { 946$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename"); 947 rename$state->{entries}{$filename}{modified_filename},$filename; 948 949 # Calculate modes to remove 950 my$invmode= ""; 951 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); } 952 953$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename"); 954 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename); 955 } 956 957 if ($rmflag) 958 { 959$log->info("Removing file '$filename'"); 960 unlink($filename); 961 system("git-update-index", "--remove",$filename); 962 } 963 elsif ($addflag) 964 { 965$log->info("Adding file '$filename'"); 966 system("git-update-index", "--add",$filename); 967 } else { 968$log->info("Updating file '$filename'"); 969 system("git-update-index",$filename); 970 } 971 } 972 973 unless ( scalar(@committedfiles) > 0 ) 974 { 975 print "E No files to commit\n"; 976 print "ok\n"; 977 close LOCKFILE; 978 unlink($lockfile); 979 chdir "/"; 980 return; 981 } 982 983 my$treehash= `git-write-tree`; 984 my$parenthash= `cat $ENV{GIT_DIR}refs/heads/$state->{module}`; 985 chomp$treehash; 986 chomp$parenthash; 987 988$log->debug("Treehash :$treehash, Parenthash :$parenthash"); 989 990 # write our commit message out if we have one ... 991 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR); 992 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) ); 993 print$msg_fh"\n\nvia git-CVS emulator\n"; 994 close$msg_fh; 995 996 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`; 997$log->info("Commit hash :$commithash"); 998 999unless($commithash=~/[a-zA-Z0-9]{40}/)1000{1001$log->warn("Commit failed (Invalid commit hash)");1002print"error 1 Commit failed (unknown reason)\n";1003close LOCKFILE;1004unlink($lockfile);1005chdir"/";1006exit;1007}10081009open FILE,">","$ENV{GIT_DIR}refs/heads/$state->{module}";1010print FILE $commithash;1011close FILE;10121013$updater->update();10141015# foreach file specified on the commandline ...1016foreachmy$filename(@committedfiles)1017{1018$filename= filecleanup($filename);10191020my$meta=$updater->getmeta($filename);10211022my($filepart,$dirpart) = filenamesplit($filename);10231024$log->debug("Checked-in$dirpart:$filename");10251026if($meta->{filehash}eq"deleted")1027{1028print"Remove-entry$dirpart\n";1029print"$filename\n";1030}else{1031print"Checked-in$dirpart\n";1032print"$filename\n";1033print"/$filepart/1.$meta->{revision}///\n";1034}1035}10361037close LOCKFILE;1038unlink($lockfile);1039chdir"/";10401041print"ok\n";1042}10431044sub req_status1045{1046my($cmd,$data) =@_;10471048 argsplit("status");10491050$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1051#$log->debug("status state : " . Dumper($state));10521053# Grab a handle to the SQLite db and do any necessary updates1054my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1055$updater->update();10561057# if no files were specified, we need to work out what files we should be providing status on ...1058 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);10591060# foreach file specified on the commandline ...1061foreachmy$filename( @{$state->{args}} )1062{1063$filename= filecleanup($filename);10641065my$meta=$updater->getmeta($filename);1066my$oldmeta=$meta;10671068my$wrev= revparse($filename);10691070# If the working copy is an old revision, lets get that version too for comparison.1071if(defined($wrev)and$wrev!=$meta->{revision} )1072{1073$oldmeta=$updater->getmeta($filename,$wrev);1074}10751076# TODO : All possible statuses aren't yet implemented1077my$status;1078# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1079$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1080and1081( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1082or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1083);10841085# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1086$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1087and1088($state->{entries}{$filename}{unchanged}1089or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1090);10911092# Need checkout if it exists in the repo but doesn't have a working copy1093$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );10941095# Locally modified if working copy and repo copy have the same revision but there are local changes1096$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );10971098# Needs Merge if working copy revision is less than repo copy and there are local changes1099$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );11001101$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1102$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1103$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1104$status||="File had conflicts on merge"if(0);11051106$status||="Unknown";11071108print"M ===================================================================\n";1109print"M File:$filename\tStatus:$status\n";1110if(defined($state->{entries}{$filename}{revision}) )1111{1112print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1113}else{1114print"M Working revision:\tNo entry for$filename\n";1115}1116if(defined($meta->{revision}) )1117{1118print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{repository}/$filename,v\n";1119print"M Sticky Tag:\t\t(none)\n";1120print"M Sticky Date:\t\t(none)\n";1121print"M Sticky Options:\t\t(none)\n";1122}else{1123print"M Repository revision:\tNo revision control file\n";1124}1125print"M\n";1126}11271128print"ok\n";1129}11301131sub req_diff1132{1133my($cmd,$data) =@_;11341135 argsplit("diff");11361137$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1138#$log->debug("status state : " . Dumper($state));11391140my($revision1,$revision2);1141if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1142{1143$revision1=$state->{opt}{r}[0];1144$revision2=$state->{opt}{r}[1];1145}else{1146$revision1=$state->{opt}{r};1147}11481149$revision1=~s/^1\.//if(defined($revision1) );1150$revision2=~s/^1\.//if(defined($revision2) );11511152$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );11531154# Grab a handle to the SQLite db and do any necessary updates1155my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1156$updater->update();11571158# if no files were specified, we need to work out what files we should be providing status on ...1159 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);11601161# foreach file specified on the commandline ...1162foreachmy$filename( @{$state->{args}} )1163{1164$filename= filecleanup($filename);11651166my($fh,$file1,$file2,$meta1,$meta2,$filediff);11671168my$wrev= revparse($filename);11691170# We need _something_ to diff against1171next unless(defined($wrev) );11721173# if we have a -r switch, use it1174if(defined($revision1) )1175{1176(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1177$meta1=$updater->getmeta($filename,$revision1);1178unless(defined($meta1)and$meta1->{filehash}ne"deleted")1179{1180print"E File$filenameat revision 1.$revision1doesn't exist\n";1181next;1182}1183 transmitfile($meta1->{filehash},$file1);1184}1185# otherwise we just use the working copy revision1186else1187{1188(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1189$meta1=$updater->getmeta($filename,$wrev);1190 transmitfile($meta1->{filehash},$file1);1191}11921193# if we have a second -r switch, use it too1194if(defined($revision2) )1195{1196(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1197$meta2=$updater->getmeta($filename,$revision2);11981199unless(defined($meta2)and$meta2->{filehash}ne"deleted")1200{1201print"E File$filenameat revision 1.$revision2doesn't exist\n";1202next;1203}12041205 transmitfile($meta2->{filehash},$file2);1206}1207# otherwise we just use the working copy1208else1209{1210$file2=$state->{entries}{$filename}{modified_filename};1211}12121213# if we have been given -r, and we don't have a $file2 yet, lets get one1214if(defined($revision1)and not defined($file2) )1215{1216(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1217$meta2=$updater->getmeta($filename,$wrev);1218 transmitfile($meta2->{filehash},$file2);1219}12201221# We need to have retrieved something useful1222next unless(defined($meta1) );12231224# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1225next if(not defined($meta2)and$wrev==$meta1->{revision}1226and1227( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1228or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1229);12301231# Apparently we only show diffs for locally modified files1232next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );12331234print"M Index:$filename\n";1235print"M ===================================================================\n";1236print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1237print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1238print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1239print"M diff ";1240foreachmy$opt(keys%{$state->{opt}} )1241{1242if(ref$state->{opt}{$opt}eq"ARRAY")1243{1244foreachmy$value( @{$state->{opt}{$opt}} )1245{1246print"-$opt$value";1247}1248}else{1249print"-$opt";1250print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1251}1252}1253print"$filename\n";12541255$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));12561257($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);12581259if(exists$state->{opt}{u} )1260{1261system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1262}else{1263system("diff$file1$file2>$filediff");1264}12651266while( <$fh> )1267{1268print"M$_";1269}1270close$fh;1271}12721273print"ok\n";1274}12751276sub req_log1277{1278my($cmd,$data) =@_;12791280 argsplit("log");12811282$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1283#$log->debug("log state : " . Dumper($state));12841285my($minrev,$maxrev);1286if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1287{1288my$control=$2;1289$minrev=$1;1290$maxrev=$3;1291$minrev=~s/^1\.//if(defined($minrev) );1292$maxrev=~s/^1\.//if(defined($maxrev) );1293$minrev++if(defined($minrev)and$controleq"::");1294}12951296# Grab a handle to the SQLite db and do any necessary updates1297my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1298$updater->update();12991300# if no files were specified, we need to work out what files we should be providing status on ...1301 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);13021303# foreach file specified on the commandline ...1304foreachmy$filename( @{$state->{args}} )1305{1306$filename= filecleanup($filename);13071308my$headmeta=$updater->getmeta($filename);13091310my$revisions=$updater->getlog($filename);1311my$totalrevisions=scalar(@$revisions);13121313if(defined($minrev) )1314{1315$log->debug("Removing revisions less than$minrev");1316while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1317{1318pop@$revisions;1319}1320}1321if(defined($maxrev) )1322{1323$log->debug("Removing revisions greater than$maxrev");1324while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1325{1326shift@$revisions;1327}1328}13291330next unless(scalar(@$revisions) );13311332print"M\n";1333print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1334print"M Working file:$filename\n";1335print"M head: 1.$headmeta->{revision}\n";1336print"M branch:\n";1337print"M locks: strict\n";1338print"M access list:\n";1339print"M symbolic names:\n";1340print"M keyword substitution: kv\n";1341print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1342print"M description:\n";13431344foreachmy$revision(@$revisions)1345{1346print"M ----------------------------\n";1347print"M revision 1.$revision->{revision}\n";1348# reformat the date for log output1349$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}) );1350$revision->{author} =~s/\s+.*//;1351$revision->{author} =~s/^(.{8}).*/$1/;1352print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1353my$commitmessage=$updater->commitmessage($revision->{commithash});1354$commitmessage=~s/^/M /mg;1355print$commitmessage."\n";1356}1357print"M =============================================================================\n";1358}13591360print"ok\n";1361}13621363sub req_annotate1364{1365my($cmd,$data) =@_;13661367 argsplit("annotate");13681369$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1370#$log->debug("status state : " . Dumper($state));13711372# Grab a handle to the SQLite db and do any necessary updates1373my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1374$updater->update();13751376# if no files were specified, we need to work out what files we should be providing annotate on ...1377 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);13781379# we'll need a temporary checkout dir1380my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1381my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1382$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");13831384$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1385$ENV{GIT_INDEX_FILE} =$file_index;13861387chdir$tmpdir;13881389# foreach file specified on the commandline ...1390foreachmy$filename( @{$state->{args}} )1391{1392$filename= filecleanup($filename);13931394my$meta=$updater->getmeta($filename);13951396next unless($meta->{revision} );13971398# get all the commits that this file was in1399# in dense format -- aka skip dead revisions1400my$revisions=$updater->gethistorydense($filename);1401my$lastseenin=$revisions->[0][2];14021403# populate the temporary index based on the latest commit were we saw1404# the file -- but do it cheaply without checking out any files1405# TODO: if we got a revision from the client, use that instead1406# to look up the commithash in sqlite (still good to default to1407# the current head as we do now)1408system("git-read-tree",$lastseenin);1409unless($?==0)1410{1411die"Error running git-read-tree$lastseenin$file_index$!";1412}1413$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");14141415# do a checkout of the file1416system('git-checkout-index','-f','-u',$filename);1417unless($?==0) {1418die"Error running git-checkout-index -f -u$filename:$!";1419}14201421$log->info("Annotate$filename");14221423# Prepare a file with the commits from the linearized1424# history that annotate should know about. This prevents1425# git-jsannotate telling us about commits we are hiding1426# from the client.14271428open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1429for(my$i=0;$i<@$revisions;$i++)1430{1431print ANNOTATEHINTS $revisions->[$i][2];1432if($i+1<@$revisions) {# have we got a parent?1433print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1434}1435print ANNOTATEHINTS "\n";1436}14371438print ANNOTATEHINTS "\n";1439close ANNOTATEHINTS;14401441my$annotatecmd='git-annotate';1442open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1443or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1444my$metadata= {};1445print"E Annotations for$filename\n";1446print"E ***************\n";1447while( <ANNOTATE> )1448{1449if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1450{1451my$commithash=$1;1452my$data=$2;1453unless(defined($metadata->{$commithash} ) )1454{1455$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1456$metadata->{$commithash}{author} =~s/\s+.*//;1457$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1458$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1459}1460printf("M 1.%-5d (%-8s%10s):%s\n",1461$metadata->{$commithash}{revision},1462$metadata->{$commithash}{author},1463$metadata->{$commithash}{modified},1464$data1465);1466}else{1467$log->warn("Error in annotate output! LINE:$_");1468print"E Annotate error\n";1469next;1470}1471}1472close ANNOTATE;1473}14741475# done; get out of the tempdir1476chdir"/";14771478print"ok\n";14791480}14811482# This method takes the state->{arguments} array and produces two new arrays.1483# The first is $state->{args} which is everything before the '--' argument, and1484# the second is $state->{files} which is everything after it.1485sub argsplit1486{1487return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");14881489my$type=shift;14901491$state->{args} = [];1492$state->{files} = [];1493$state->{opt} = {};14941495if(defined($type) )1496{1497my$opt= {};1498$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");1499$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1500$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");1501$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1502$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1503$opt= { k =>1, m =>1}if($typeeq"add");1504$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1505$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");150615071508while(scalar( @{$state->{arguments}} ) >0)1509{1510my$arg=shift@{$state->{arguments}};15111512next if($argeq"--");1513next unless($arg=~/\S/);15141515# if the argument looks like a switch1516if($arg=~/^-(\w)(.*)/)1517{1518# if it's a switch that takes an argument1519if($opt->{$1} )1520{1521# If this switch has already been provided1522if($opt->{$1} >1and exists($state->{opt}{$1} ) )1523{1524$state->{opt}{$1} = [$state->{opt}{$1} ];1525if(length($2) >0)1526{1527push@{$state->{opt}{$1}},$2;1528}else{1529push@{$state->{opt}{$1}},shift@{$state->{arguments}};1530}1531}else{1532# if there's extra data in the arg, use that as the argument for the switch1533if(length($2) >0)1534{1535$state->{opt}{$1} =$2;1536}else{1537$state->{opt}{$1} =shift@{$state->{arguments}};1538}1539}1540}else{1541$state->{opt}{$1} =undef;1542}1543}1544else1545{1546push@{$state->{args}},$arg;1547}1548}1549}1550else1551{1552my$mode=0;15531554foreachmy$value( @{$state->{arguments}} )1555{1556if($valueeq"--")1557{1558$mode++;1559next;1560}1561push@{$state->{args}},$valueif($mode==0);1562push@{$state->{files}},$valueif($mode==1);1563}1564}1565}15661567# This method uses $state->{directory} to populate $state->{args} with a list of filenames1568sub argsfromdir1569{1570my$updater=shift;15711572$state->{args} = [];15731574foreachmy$file( @{$updater->gethead} )1575{1576next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1577next unless($file->{name} =~s/^$state->{directory}//);1578push@{$state->{args}},$file->{name};1579}1580}15811582# This method cleans up the $state variable after a command that uses arguments has run1583sub statecleanup1584{1585$state->{files} = [];1586$state->{args} = [];1587$state->{arguments} = [];1588$state->{entries} = {};1589}15901591sub revparse1592{1593my$filename=shift;15941595returnundefunless(defined($state->{entries}{$filename}{revision} ) );15961597return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1598return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);15991600returnundef;1601}16021603# This method takes a file hash and does a CVS "file transfer" which transmits the1604# size of the file, and then the file contents.1605# If a second argument $targetfile is given, the file is instead written out to1606# a file by the name of $targetfile1607sub transmitfile1608{1609my$filehash=shift;1610my$targetfile=shift;16111612if(defined($filehash)and$filehasheq"deleted")1613{1614$log->warn("filehash is 'deleted'");1615return;1616}16171618die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);16191620my$type=`git-cat-file -t$filehash`;1621 chomp$type;16221623 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );16241625 my$size= `git-cat-file -s $filehash`;1626chomp$size;16271628$log->debug("transmitfile($filehash) size=$size, type=$type");16291630if(open my$fh,'-|',"git-cat-file","blob",$filehash)1631{1632if(defined($targetfile) )1633{1634open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1635print NEWFILE $_while( <$fh> );1636close NEWFILE;1637}else{1638print"$size\n";1639printwhile( <$fh> );1640}1641close$fhor die("Couldn't close filehandle for transmitfile()");1642}else{1643die("Couldn't execute git-cat-file");1644}1645}16461647# This method takes a file name, and returns ( $dirpart, $filepart ) which1648# refers to the directory porition and the file portion of the filename1649# respectively1650sub filenamesplit1651{1652my$filename=shift;16531654my($filepart,$dirpart) = ($filename,".");1655($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1656$dirpart.="/";16571658return($filepart,$dirpart);1659}16601661sub filecleanup1662{1663my$filename=shift;16641665returnundefunless(defined($filename));1666if($filename=~/^\// )1667{1668print"E absolute filenames '$filename' not supported by server\n";1669returnundef;1670}16711672$filename=~s/^\.\///g;1673$filename=$state->{directory} .$filename;16741675return$filename;1676}16771678package GITCVS::log;16791680####1681#### Copyright The Open University UK - 2006.1682####1683#### Authors: Martyn Smith <martyn@catalyst.net.nz>1684#### Martin Langhoff <martin@catalyst.net.nz>1685####1686####16871688use strict;1689use warnings;16901691=head1 NAME16921693GITCVS::log16941695=head1 DESCRIPTION16961697This module provides very crude logging with a similar interface to1698Log::Log4perl16991700=head1 METHODS17011702=cut17031704=head2 new17051706Creates a new log object, optionally you can specify a filename here to1707indicate the file to log to. If no log file is specified, you can specifiy one1708later with method setfile, or indicate you no longer want logging with method1709nofile.17101711Until one of these methods is called, all log calls will buffer messages ready1712to write out.17131714=cut1715sub new1716{1717my$class=shift;1718my$filename=shift;17191720my$self= {};17211722bless$self,$class;17231724if(defined($filename) )1725{1726open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1727}17281729return$self;1730}17311732=head2 setfile17331734This methods takes a filename, and attempts to open that file as the log file.1735If successful, all buffered data is written out to the file, and any further1736logging is written directly to the file.17371738=cut1739sub setfile1740{1741my$self=shift;1742my$filename=shift;17431744if(defined($filename) )1745{1746open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1747}17481749return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");17501751while(my$line=shift@{$self->{buffer}} )1752{1753print{$self->{fh}}$line;1754}1755}17561757=head2 nofile17581759This method indicates no logging is going to be used. It flushes any entries in1760the internal buffer, and sets a flag to ensure no further data is put there.17611762=cut1763sub nofile1764{1765my$self=shift;17661767$self->{nolog} =1;17681769return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");17701771$self->{buffer} = [];1772}17731774=head2 _logopen17751776Internal method. Returns true if the log file is open, false otherwise.17771778=cut1779sub _logopen1780{1781my$self=shift;17821783return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");1784return0;1785}17861787=head2 debug info warn fatal17881789These four methods are wrappers to _log. They provide the actual interface for1790logging data.17911792=cut1793sub debug {my$self=shift;$self->_log("debug",@_); }1794sub info {my$self=shift;$self->_log("info",@_); }1795subwarn{my$self=shift;$self->_log("warn",@_); }1796sub fatal {my$self=shift;$self->_log("fatal",@_); }17971798=head2 _log17991800This is an internal method called by the logging functions. It generates a1801timestamp and pushes the logged line either to file, or internal buffer.18021803=cut1804sub _log1805{1806my$self=shift;1807my$level=shift;18081809return if($self->{nolog} );18101811my@time=localtime;1812my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",1813$time[5] +1900,1814$time[4] +1,1815$time[3],1816$time[2],1817$time[1],1818$time[0],1819uc$level,1820);18211822if($self->_logopen)1823{1824print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";1825}else{1826push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";1827}1828}18291830=head2 DESTROY18311832This method simply closes the file handle if one is open18331834=cut1835sub DESTROY1836{1837my$self=shift;18381839if($self->_logopen)1840{1841close$self->{fh};1842}1843}18441845package GITCVS::updater;18461847####1848#### Copyright The Open University UK - 2006.1849####1850#### Authors: Martyn Smith <martyn@catalyst.net.nz>1851#### Martin Langhoff <martin@catalyst.net.nz>1852####1853####18541855use strict;1856use warnings;1857use DBI;18581859=head1 METHODS18601861=cut18621863=head2 new18641865=cut1866sub new1867{1868my$class=shift;1869my$config=shift;1870my$module=shift;1871my$log=shift;18721873die"Need to specify a git repository"unless(defined($config)and-d $config);1874die"Need to specify a module"unless(defined($module) );18751876$class=ref($class) ||$class;18771878my$self= {};18791880bless$self,$class;18811882$self->{dbdir} =$config."/";1883die"Database dir '$self->{dbdir}' isn't a directory"unless(defined($self->{dbdir})and-d $self->{dbdir} );18841885$self->{module} =$module;1886$self->{file} =$self->{dbdir} ."/gitcvs.$module.sqlite";18871888$self->{git_path} =$config."/";18891890$self->{log} =$log;18911892die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );18931894$self->{dbh} = DBI->connect("dbi:SQLite:dbname=".$self->{file},"","");18951896$self->{tables} = {};1897foreachmy$table($self->{dbh}->tables)1898{1899$table=~s/^"//;1900$table=~s/"$//;1901$self->{tables}{$table} =1;1902}19031904# Construct the revision table if required1905unless($self->{tables}{revision} )1906{1907$self->{dbh}->do("1908 CREATE TABLE revision (1909 name TEXT NOT NULL,1910 revision INTEGER NOT NULL,1911 filehash TEXT NOT NULL,1912 commithash TEXT NOT NULL,1913 author TEXT NOT NULL,1914 modified TEXT NOT NULL,1915 mode TEXT NOT NULL1916 )1917 ");1918}19191920# Construct the revision table if required1921unless($self->{tables}{head} )1922{1923$self->{dbh}->do("1924 CREATE TABLE head (1925 name TEXT NOT NULL,1926 revision INTEGER NOT NULL,1927 filehash TEXT NOT NULL,1928 commithash TEXT NOT NULL,1929 author TEXT NOT NULL,1930 modified TEXT NOT NULL,1931 mode TEXT NOT NULL1932 )1933 ");1934}19351936# Construct the properties table if required1937unless($self->{tables}{properties} )1938{1939$self->{dbh}->do("1940 CREATE TABLE properties (1941 key TEXT NOT NULL PRIMARY KEY,1942 value TEXT1943 )1944 ");1945}19461947# Construct the commitmsgs table if required1948unless($self->{tables}{commitmsgs} )1949{1950$self->{dbh}->do("1951 CREATE TABLE commitmsgs (1952 key TEXT NOT NULL PRIMARY KEY,1953 value TEXT1954 )1955 ");1956}19571958return$self;1959}19601961=head2 update19621963=cut1964sub update1965{1966my$self=shift;19671968# first lets get the commit list1969$ENV{GIT_DIR} =$self->{git_path};19701971# prepare database queries1972my$db_insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);1973my$db_insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);1974my$db_delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);1975my$db_insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);19761977my$commitinfo=`git-cat-file commit$self->{module} 2>&1`;1978unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)1979{1980die("Invalid module '$self->{module}'");1981}198219831984my$git_log;1985my$lastcommit=$self->_get_prop("last_commit");19861987# Start exclusive lock here...1988$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";19891990# TODO: log processing is memory bound1991# if we can parse into a 2nd file that is in reverse order1992# we can probably do something really efficient1993my@git_log_params= ('--parents','--topo-order');19941995if(defined$lastcommit) {1996push@git_log_params,"$lastcommit..$self->{module}";1997}else{1998push@git_log_params,$self->{module};1999}2000open(GITLOG,'-|','git-log',@git_log_params)or die"Cannot call git-log:$!";20012002my@commits;20032004my%commit= ();20052006while( <GITLOG> )2007{2008chomp;2009if(m/^commit\s+(.*)$/) {2010# on ^commit lines put the just seen commit in the stack2011# and prime things for the next one2012if(keys%commit) {2013my%copy=%commit;2014unshift@commits, \%copy;2015%commit= ();2016}2017my@parents=split(m/\s+/,$1);2018$commit{hash} =shift@parents;2019$commit{parents} = \@parents;2020}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2021# on rfc822-like lines seen before we see any message,2022# lowercase the entry and put it in the hash as key-value2023$commit{lc($1)} =$2;2024}else{2025# message lines - skip initial empty line2026# and trim whitespace2027if(!exists($commit{message}) &&m/^\s*$/) {2028# define it to mark the end of headers2029$commit{message} ='';2030next;2031}2032s/^\s+//;s/\s+$//;# trim ws2033$commit{message} .=$_."\n";2034}2035}2036close GITLOG;20372038unshift@commits, \%commitif(keys%commit);20392040# Now all the commits are in the @commits bucket2041# ordered by time DESC. for each commit that needs processing,2042# determine whether it's following the last head we've seen or if2043# it's on its own branch, grab a file list, and add whatever's changed2044# NOTE: $lastcommit refers to the last commit from previous run2045# $lastpicked is the last commit we picked in this run2046my$lastpicked;2047my$head= {};2048if(defined$lastcommit) {2049$lastpicked=$lastcommit;2050}20512052my$committotal=scalar(@commits);2053my$commitcount=0;20542055# Load the head table into $head (for cached lookups during the update process)2056foreachmy$file( @{$self->gethead()} )2057{2058$head->{$file->{name}} =$file;2059}20602061foreachmy$commit(@commits)2062{2063$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2064if(defined$lastpicked)2065{2066if(!in_array($lastpicked, @{$commit->{parents}}))2067{2068# skip, we'll see this delta2069# as part of a merge later2070# warn "skipping off-track $commit->{hash}\n";2071next;2072}elsif(@{$commit->{parents}} >1) {2073# it is a merge commit, for each parent that is2074# not $lastpicked, see if we can get a log2075# from the merge-base to that parent to put it2076# in the message as a merge summary.2077my@parents= @{$commit->{parents}};2078foreachmy$parent(@parents) {2079# git-merge-base can potentially (but rarely) throw2080# several candidate merge bases. let's assume2081# that the first one is the best one.2082if($parenteq$lastpicked) {2083next;2084}2085open my$p,'git-merge-base '.$lastpicked.' '2086.$parent.'|';2087my@output= (<$p>);2088close$p;2089my$base=join('',@output);2090chomp$base;2091if($base) {2092my@merged;2093# print "want to log between $base $parent \n";2094open(GITLOG,'-|','git-log',"$base..$parent")2095or die"Cannot call git-log:$!";2096my$mergedhash;2097while(<GITLOG>) {2098chomp;2099if(!defined$mergedhash) {2100if(m/^commit\s+(.+)$/) {2101$mergedhash=$1;2102}else{2103next;2104}2105}else{2106# grab the first line that looks non-rfc8222107# aka has content after leading space2108if(m/^\s+(\S.*)$/) {2109my$title=$1;2110$title=substr($title,0,100);# truncate2111unshift@merged,"$mergedhash$title";2112undef$mergedhash;2113}2114}2115}2116close GITLOG;2117if(@merged) {2118$commit->{mergemsg} =$commit->{message};2119$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2120foreachmy$summary(@merged) {2121$commit->{mergemsg} .="\t$summary\n";2122}2123$commit->{mergemsg} .="\n\n";2124# print "Message for $commit->{hash} \n$commit->{mergemsg}";2125}2126}2127}2128}2129}21302131# convert the date to CVS-happy format2132$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);21332134if(defined($lastpicked) )2135{2136my$filepipe=open(FILELIST,'-|','git-diff-tree','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2137while( <FILELIST> )2138{2139unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o)2140{2141die("Couldn't process git-diff-tree line :$_");2142}21432144# $log->debug("File mode=$1, hash=$2, change=$3, name=$4");21452146my$git_perms="";2147$git_perms.="r"if($1&4);2148$git_perms.="w"if($1&2);2149$git_perms.="x"if($1&1);2150$git_perms="rw"if($git_permseq"");21512152if($3eq"D")2153{2154#$log->debug("DELETE $4");2155$head->{$4} = {2156 name =>$4,2157 revision =>$head->{$4}{revision} +1,2158 filehash =>"deleted",2159 commithash =>$commit->{hash},2160 modified =>$commit->{date},2161 author =>$commit->{author},2162 mode =>$git_perms,2163};2164$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2165}2166elsif($3eq"M")2167{2168#$log->debug("MODIFIED $4");2169$head->{$4} = {2170 name =>$4,2171 revision =>$head->{$4}{revision} +1,2172 filehash =>$2,2173 commithash =>$commit->{hash},2174 modified =>$commit->{date},2175 author =>$commit->{author},2176 mode =>$git_perms,2177};2178$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2179}2180elsif($3eq"A")2181{2182#$log->debug("ADDED $4");2183$head->{$4} = {2184 name =>$4,2185 revision =>1,2186 filehash =>$2,2187 commithash =>$commit->{hash},2188 modified =>$commit->{date},2189 author =>$commit->{author},2190 mode =>$git_perms,2191};2192$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2193}2194else2195{2196$log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");2197die;2198}2199}2200close FILELIST;2201}else{2202# this is used to detect files removed from the repo2203my$seen_files= {};22042205my$filepipe=open(FILELIST,'-|','git-ls-tree','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2206while( <FILELIST> )2207{2208unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o)2209{2210die("Couldn't process git-ls-tree line :$_");2211}22122213my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);22142215$seen_files->{$git_filename} =1;22162217my($oldhash,$oldrevision,$oldmode) = (2218$head->{$git_filename}{filehash},2219$head->{$git_filename}{revision},2220$head->{$git_filename}{mode}2221);22222223if($git_perms=~/^\d\d\d(\d)\d\d/o)2224{2225$git_perms="";2226$git_perms.="r"if($1&4);2227$git_perms.="w"if($1&2);2228$git_perms.="x"if($1&1);2229}else{2230$git_perms="rw";2231}22322233# unless the file exists with the same hash, we need to update it ...2234unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2235{2236my$newrevision= ($oldrevisionor0) +1;22372238$head->{$git_filename} = {2239 name =>$git_filename,2240 revision =>$newrevision,2241 filehash =>$git_hash,2242 commithash =>$commit->{hash},2243 modified =>$commit->{date},2244 author =>$commit->{author},2245 mode =>$git_perms,2246};224722482249$db_insert_rev->execute($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2250}2251}2252close FILELIST;22532254# Detect deleted files2255foreachmy$file(keys%$head)2256{2257unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2258{2259$head->{$file}{revision}++;2260$head->{$file}{filehash} ="deleted";2261$head->{$file}{commithash} =$commit->{hash};2262$head->{$file}{modified} =$commit->{date};2263$head->{$file}{author} =$commit->{author};22642265$db_insert_rev->execute($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2266}2267}2268# END : "Detect deleted files"2269}227022712272if(exists$commit->{mergemsg})2273{2274$db_insert_mergelog->execute($commit->{hash},$commit->{mergemsg});2275}22762277$lastpicked=$commit->{hash};22782279$self->_set_prop("last_commit",$commit->{hash});2280}22812282$db_delete_head->execute();2283foreachmy$file(keys%$head)2284{2285$db_insert_head->execute(2286$file,2287$head->{$file}{revision},2288$head->{$file}{filehash},2289$head->{$file}{commithash},2290$head->{$file}{modified},2291$head->{$file}{author},2292$head->{$file}{mode},2293);2294}2295# invalidate the gethead cache2296$self->{gethead_cache} =undef;229722982299# Ending exclusive lock here2300$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2301}23022303sub _headrev2304{2305my$self=shift;2306my$filename=shift;23072308my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2309$db_query->execute($filename);2310my($hash,$revision,$mode) =$db_query->fetchrow_array;23112312return($hash,$revision,$mode);2313}23142315sub _get_prop2316{2317my$self=shift;2318my$key=shift;23192320my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2321$db_query->execute($key);2322my($value) =$db_query->fetchrow_array;23232324return$value;2325}23262327sub _set_prop2328{2329my$self=shift;2330my$key=shift;2331my$value=shift;23322333my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2334$db_query->execute($value,$key);23352336unless($db_query->rows)2337{2338$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2339$db_query->execute($key,$value);2340}23412342return$value;2343}23442345=head2 gethead23462347=cut23482349sub gethead2350{2351my$self=shift;23522353return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );23542355my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2356$db_query->execute();23572358my$tree= [];2359while(my$file=$db_query->fetchrow_hashref)2360{2361push@$tree,$file;2362}23632364$self->{gethead_cache} =$tree;23652366return$tree;2367}23682369=head2 getlog23702371=cut23722373sub getlog2374{2375my$self=shift;2376my$filename=shift;23772378my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2379$db_query->execute($filename);23802381my$tree= [];2382while(my$file=$db_query->fetchrow_hashref)2383{2384push@$tree,$file;2385}23862387return$tree;2388}23892390=head2 getmeta23912392This function takes a filename (with path) argument and returns a hashref of2393metadata for that file.23942395=cut23962397sub getmeta2398{2399my$self=shift;2400my$filename=shift;2401my$revision=shift;24022403my$db_query;2404if(defined($revision)and$revision=~/^\d+$/)2405{2406$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2407$db_query->execute($filename,$revision);2408}2409elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2410{2411$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2412$db_query->execute($filename,$revision);2413}else{2414$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2415$db_query->execute($filename);2416}24172418return$db_query->fetchrow_hashref;2419}24202421=head2 commitmessage24222423this function takes a commithash and returns the commit message for that commit24242425=cut2426sub commitmessage2427{2428my$self=shift;2429my$commithash=shift;24302431die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);24322433my$db_query;2434$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2435$db_query->execute($commithash);24362437my($message) =$db_query->fetchrow_array;24382439if(defined($message) )2440{2441$message.=" "if($message=~/\n$/);2442return$message;2443}24442445my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2446shift@lineswhile($lines[0] =~/\S/);2447$message=join("",@lines);2448$message.=" "if($message=~/\n$/);2449return$message;2450}24512452=head2 gethistory24532454This function takes a filename (with path) argument and returns an arrayofarrays2455containing revision,filehash,commithash ordered by revision descending24562457=cut2458sub gethistory2459{2460my$self=shift;2461my$filename=shift;24622463my$db_query;2464$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2465$db_query->execute($filename);24662467return$db_query->fetchall_arrayref;2468}24692470=head2 gethistorydense24712472This function takes a filename (with path) argument and returns an arrayofarrays2473containing revision,filehash,commithash ordered by revision descending.24742475This version of gethistory skips deleted entries -- so it is useful for annotate.2476The 'dense' part is a reference to a '--dense' option available for git-rev-list2477and other git tools that depend on it.24782479=cut2480sub gethistorydense2481{2482my$self=shift;2483my$filename=shift;24842485my$db_query;2486$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2487$db_query->execute($filename);24882489return$db_query->fetchall_arrayref;2490}24912492=head2 in_array()24932494from Array::PAT - mimics the in_array() function2495found in PHP. Yuck but works for small arrays.24962497=cut2498sub in_array2499{2500my($check,@array) =@_;2501my$retval=0;2502foreachmy$test(@array){2503if($checkeq$test){2504$retval=1;2505}2506}2507return$retval;2508}25092510=head2 safe_pipe_capture25112512an alterative to `command` that allows input to be passed as an array2513to work around shell problems with weird characters in arguments25142515=cut2516sub safe_pipe_capture {25172518my@output;25192520if(my$pid=open my$child,'-|') {2521@output= (<$child>);2522close$childor die join(' ',@_).":$!$?";2523}else{2524exec(@_)or die"$!$?";# exec() can fail the executable can't be found2525}2526returnwantarray?@output:join('',@output);2527}2528252925301;