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"; 582print"Clear-sticky$checkout_path/\n";# yes, twice 583print$state->{CVSROOT} ."/$module/\n"; 584print"Template$checkout_path/\n"; 585print$state->{CVSROOT} ."/$module/\n"; 586print"0\n"; 587 588# instruct the client that we're checking out to $checkout_path 589print"E cvs checkout: Updating$checkout_path\n"; 590 591my%seendirs= (); 592my$lastdir=''; 593 594# recursive 595sub prepdir { 596my($dir,$repodir,$remotedir,$seendirs) =@_; 597my$parent= dirname($dir); 598$dir=~ s|/+$||; 599$repodir=~ s|/+$||; 600$remotedir=~ s|/+$||; 601$parent=~ s|/+$||; 602$log->debug("announcedir$dir,$repodir,$remotedir"); 603 604if($parenteq'.'||$parenteq'./') { 605$parent=''; 606} 607# recurse to announce unseen parents first 608if(length($parent) && !exists($seendirs->{$parent})) { 609 prepdir($parent,$repodir,$remotedir,$seendirs); 610} 611# Announce that we are going to modify at the parent level 612if($parent) { 613print"E cvs checkout: Updating$remotedir/$parent\n"; 614}else{ 615print"E cvs checkout: Updating$remotedir\n"; 616} 617print"Clear-sticky$remotedir/$parent/\n"; 618print"$repodir/$parent/\n"; 619 620print"Clear-static-directory$remotedir/$dir/\n"; 621print"$repodir/$dir/\n"; 622print"Clear-sticky$remotedir/$parent/\n";# yes, twice 623print"$repodir/$parent/\n"; 624print"Template$remotedir/$dir/\n"; 625print"$repodir/$dir/\n"; 626print"0\n"; 627 628$seendirs->{$dir} =1; 629} 630 631foreachmy$git( @{$updater->gethead} ) 632{ 633# Don't want to check out deleted files 634next if($git->{filehash}eq"deleted"); 635 636($git->{name},$git->{dir} ) = filenamesplit($git->{name}); 637 638if(length($git->{dir}) &&$git->{dir}ne'./' 639&&$git->{dir}ne$lastdir) { 640unless(exists($seendirs{$git->{dir}})) { 641 prepdir($git->{dir},$state->{CVSROOT} ."/$module/", 642$checkout_path, \%seendirs); 643$lastdir=$git->{dir}; 644$seendirs{$git->{dir}} =1; 645} 646print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; 647} 648 649# modification time of this file 650print"Mod-time$git->{modified}\n"; 651 652# print some information to the client 653if(defined($git->{dir} )and$git->{dir}ne"./") 654{ 655print"M U$checkout_path/$git->{dir}$git->{name}\n"; 656}else{ 657print"M U$checkout_path/$git->{name}\n"; 658} 659 660# instruct client we're sending a file to put in this path 661print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n"; 662 663print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n"; 664 665# this is an "entries" line 666print"/$git->{name}/1.$git->{revision}///\n"; 667# permissions 668print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; 669 670# transmit file 671 transmitfile($git->{filehash}); 672} 673 674print"ok\n"; 675 676 statecleanup(); 677} 678 679# update \n 680# Response expected: yes. Actually do a cvs update command. This uses any 681# previous Argument, Directory, Entry, or Modified requests, if they have 682# been sent. The last Directory sent specifies the working directory at the 683# time of the operation. The -I option is not used--files which the client 684# can decide whether to ignore are not mentioned and the client sends the 685# Questionable request for others. 686sub req_update 687{ 688my($cmd,$data) =@_; 689 690$log->debug("req_update : ". (defined($data) ?$data:"[NULL]")); 691 692 argsplit("update"); 693 694# 695# It may just be a client exploring the available heads/modukles 696# in that case, list them as top level directories and leave it 697# at that. Eclipse uses this technique to offer you a list of 698# projects (heads in this case) to checkout. 699# 700if($state->{module}eq'') { 701print"E cvs update: Updating .\n"; 702opendir HEADS,$state->{CVSROOT} .'/refs/heads'; 703while(my$head=readdir(HEADS)) { 704if(-f $state->{CVSROOT} .'/refs/heads/'.$head) { 705print"E cvs update: New directory `$head'\n"; 706} 707} 708closedir HEADS; 709print"ok\n"; 710return1; 711} 712 713 714# Grab a handle to the SQLite db and do any necessary updates 715my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 716 717$updater->update(); 718 719# if no files were specified, we need to work out what files we should be providing status on ... 720 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0); 721 722#$log->debug("update state : " . Dumper($state)); 723 724# foreach file specified on the commandline ... 725foreachmy$filename( @{$state->{args}} ) 726{ 727$filename= filecleanup($filename); 728 729# if we have a -C we should pretend we never saw modified stuff 730if(exists($state->{opt}{C} ) ) 731{ 732delete$state->{entries}{$filename}{modified_hash}; 733delete$state->{entries}{$filename}{modified_filename}; 734$state->{entries}{$filename}{unchanged} =1; 735} 736 737my$meta; 738if(defined($state->{opt}{r})and$state->{opt}{r} =~/^1\.(\d+)/) 739{ 740$meta=$updater->getmeta($filename,$1); 741}else{ 742$meta=$updater->getmeta($filename); 743} 744 745next unless($meta->{revision} ); 746 747my$oldmeta=$meta; 748 749my$wrev= revparse($filename); 750 751# If the working copy is an old revision, lets get that version too for comparison. 752if(defined($wrev)and$wrev!=$meta->{revision} ) 753{ 754$oldmeta=$updater->getmeta($filename,$wrev); 755} 756 757#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); 758 759# Files are up to date if the working copy and repo copy have the same revision, 760# and the working copy is unmodified _and_ the user hasn't specified -C 761next if(defined($wrev) 762and defined($meta->{revision}) 763and$wrev==$meta->{revision} 764and$state->{entries}{$filename}{unchanged} 765and not exists($state->{opt}{C} ) ); 766 767# If the working copy and repo copy have the same revision, 768# but the working copy is modified, tell the client it's modified 769if(defined($wrev) 770and defined($meta->{revision}) 771and$wrev==$meta->{revision} 772and not exists($state->{opt}{C} ) ) 773{ 774$log->info("Tell the client the file is modified"); 775print"MT text U\n"; 776print"MT fname$filename\n"; 777print"MT newline\n"; 778next; 779} 780 781if($meta->{filehash}eq"deleted") 782{ 783my($filepart,$dirpart) = filenamesplit($filename); 784 785$log->info("Removing '$filename' from working copy (no longer in the repo)"); 786 787print"E cvs update: `$filename' is no longer in the repository\n"; 788print"Removed$dirpart\n"; 789print"$filepart\n"; 790} 791elsif(not defined($state->{entries}{$filename}{modified_hash} ) 792or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) 793{ 794$log->info("Updating '$filename'"); 795# normal update, just send the new revision (either U=Update, or A=Add, or R=Remove) 796print"MT +updated\n"; 797print"MT text U\n"; 798print"MT fname$filename\n"; 799print"MT newline\n"; 800print"MT -updated\n"; 801 802my($filepart,$dirpart) = filenamesplit($filename); 803$dirpart=~s/^$state->{directory}//; 804 805if(defined($wrev) ) 806{ 807# instruct client we're sending a file to put in this path as a replacement 808print"Update-existing$dirpart\n"; 809$log->debug("Updating existing file 'Update-existing$dirpart'"); 810}else{ 811# instruct client we're sending a file to put in this path as a new file 812print"Created$dirpart\n"; 813$log->debug("Creating new file 'Created$dirpart'"); 814} 815print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 816 817# this is an "entries" line 818$log->debug("/$filepart/1.$meta->{revision}///"); 819print"/$filepart/1.$meta->{revision}///\n"; 820 821# permissions 822$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 823print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 824 825# transmit file 826 transmitfile($meta->{filehash}); 827}else{ 828$log->info("Updating '$filename'"); 829my($filepart,$dirpart) = filenamesplit($meta->{name}); 830 831my$dir= tempdir( DIR =>$TEMP_DIR, CLEANUP =>1) ."/"; 832 833chdir$dir; 834my$file_local=$filepart.".mine"; 835system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local); 836my$file_old=$filepart.".".$oldmeta->{revision}; 837 transmitfile($oldmeta->{filehash},$file_old); 838my$file_new=$filepart.".".$meta->{revision}; 839 transmitfile($meta->{filehash},$file_new); 840 841# we need to merge with the local changes ( M=successful merge, C=conflict merge ) 842$log->info("Merging$file_local,$file_old,$file_new"); 843 844$log->debug("Temporary directory for merge is$dir"); 845 846my$return=system("merge",$file_local,$file_old,$file_new); 847$return>>=8; 848 849if($return==0) 850{ 851$log->info("Merged successfully"); 852print"M M$filename\n"; 853$log->debug("Update-existing$dirpart"); 854print"Update-existing$dirpart\n"; 855$log->debug($state->{CVSROOT} ."/$state->{module}/$filename"); 856print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 857$log->debug("/$filepart/1.$meta->{revision}///"); 858print"/$filepart/1.$meta->{revision}///\n"; 859} 860elsif($return==1) 861{ 862$log->info("Merged with conflicts"); 863print"M C$filename\n"; 864print"Update-existing$dirpart\n"; 865print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 866print"/$filepart/1.$meta->{revision}/+//\n"; 867} 868else 869{ 870$log->warn("Merge failed"); 871next; 872} 873 874# permissions 875$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 876print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 877 878# transmit file, format is single integer on a line by itself (file 879# size) followed by the file contents 880# TODO : we should copy files in blocks 881my$data=`cat$file_local`; 882$log->debug("File size : " . length($data)); 883 print length($data) . "\n"; 884 print$data; 885 886 chdir "/"; 887 } 888 889 } 890 891 print "ok\n"; 892} 893 894sub req_ci 895{ 896 my ($cmd,$data) =@_; 897 898 argsplit("ci"); 899 900 #$log->debug("State : " . Dumper($state)); 901 902$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" )); 903 904 if ( -e$state->{CVSROOT} . "/index" ) 905 { 906 print "error 1 Index already exists in git repo\n"; 907 exit; 908 } 909 910 my$lockfile= "$state->{CVSROOT}/refs/heads/$state->{module}.lock"; 911 unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) ) 912 { 913 print "error 1 Lock file '$lockfile' already exists, please try again\n"; 914 exit; 915 } 916 917 # Grab a handle to the SQLite db and do any necessary updates 918 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 919$updater->update(); 920 921 my$tmpdir= tempdir ( DIR =>$TEMP_DIR); 922 my ( undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN => 0 ); 923$log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'"); 924 925$ENV{GIT_DIR} =$state->{CVSROOT} . "/"; 926$ENV{GIT_INDEX_FILE} =$file_index; 927 928 chdir$tmpdir; 929 930 # populate the temporary index based 931 system("git-read-tree",$state->{module}); 932 unless ($?== 0) 933 { 934 die "Error running git-read-tree$state->{module}$file_index$!"; 935 } 936$log->info("Created index '$file_index' with for head$state->{module} - exit status$?"); 937 938 939 my@committedfiles= (); 940 941 # foreach file specified on the commandline ... 942 foreach my$filename( @{$state->{args}} ) 943 { 944$filename= filecleanup($filename); 945 946 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} ); 947 948 my$meta=$updater->getmeta($filename); 949 950 my$wrev= revparse($filename); 951 952 my ($filepart,$dirpart) = filenamesplit($filename); 953 954 # do a checkout of the file if it part of this tree 955 if ($wrev) { 956 system('git-checkout-index', '-f', '-u',$filename); 957 unless ($?== 0) { 958 die "Error running git-checkout-index -f -u$filename:$!"; 959 } 960 } 961 962 my$addflag= 0; 963 my$rmflag= 0; 964$rmflag= 1 if ( defined($wrev) and$wrev< 0 ); 965$addflag= 1 unless ( -e$filename); 966 967 # Do up to date checking 968 unless ($addflagor$wrev==$meta->{revision} or ($rmflagand -$wrev==$meta->{revision} ) ) 969 { 970 # fail everything if an up to date check fails 971 print "error 1 Up to date check failed for$filename\n"; 972 close LOCKFILE; 973 unlink($lockfile); 974 chdir "/"; 975 exit; 976 } 977 978 push@committedfiles,$filename; 979$log->info("Committing$filename"); 980 981 system("mkdir","-p",$dirpart) unless ( -d$dirpart); 982 983 unless ($rmflag) 984 { 985$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename"); 986 rename$state->{entries}{$filename}{modified_filename},$filename; 987 988 # Calculate modes to remove 989 my$invmode= ""; 990 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); } 991 992$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename"); 993 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename); 994 } 995 996 if ($rmflag) 997 { 998$log->info("Removing file '$filename'"); 999 unlink($filename);1000 system("git-update-index", "--remove",$filename);1001 }1002 elsif ($addflag)1003 {1004$log->info("Adding file '$filename'");1005 system("git-update-index", "--add",$filename);1006 } else {1007$log->info("Updating file '$filename'");1008 system("git-update-index",$filename);1009 }1010 }10111012 unless ( scalar(@committedfiles) > 0 )1013 {1014 print "E No files to commit\n";1015 print "ok\n";1016 close LOCKFILE;1017 unlink($lockfile);1018 chdir "/";1019 return;1020 }10211022 my$treehash= `git-write-tree`;1023 my$parenthash= `cat $ENV{GIT_DIR}refs/heads/$state->{module}`;1024 chomp$treehash;1025 chomp$parenthash;10261027$log->debug("Treehash :$treehash, Parenthash :$parenthash");10281029 # write our commit message out if we have one ...1030 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1031 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1032 print$msg_fh"\n\nvia git-CVS emulator\n";1033 close$msg_fh;10341035 my$commithash= `git-commit-tree $treehash-p $parenthash<$msg_filename`;1036$log->info("Commit hash :$commithash");10371038unless($commithash=~/[a-zA-Z0-9]{40}/)1039{1040$log->warn("Commit failed (Invalid commit hash)");1041print"error 1 Commit failed (unknown reason)\n";1042close LOCKFILE;1043unlink($lockfile);1044chdir"/";1045exit;1046}10471048open FILE,">","$ENV{GIT_DIR}refs/heads/$state->{module}";1049print FILE $commithash;1050close FILE;10511052$updater->update();10531054# foreach file specified on the commandline ...1055foreachmy$filename(@committedfiles)1056{1057$filename= filecleanup($filename);10581059my$meta=$updater->getmeta($filename);10601061my($filepart,$dirpart) = filenamesplit($filename);10621063$log->debug("Checked-in$dirpart:$filename");10641065if($meta->{filehash}eq"deleted")1066{1067print"Remove-entry$dirpart\n";1068print"$filename\n";1069}else{1070print"Checked-in$dirpart\n";1071print"$filename\n";1072print"/$filepart/1.$meta->{revision}///\n";1073}1074}10751076close LOCKFILE;1077unlink($lockfile);1078chdir"/";10791080print"ok\n";1081}10821083sub req_status1084{1085my($cmd,$data) =@_;10861087 argsplit("status");10881089$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1090#$log->debug("status state : " . Dumper($state));10911092# Grab a handle to the SQLite db and do any necessary updates1093my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1094$updater->update();10951096# if no files were specified, we need to work out what files we should be providing status on ...1097 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);10981099# foreach file specified on the commandline ...1100foreachmy$filename( @{$state->{args}} )1101{1102$filename= filecleanup($filename);11031104my$meta=$updater->getmeta($filename);1105my$oldmeta=$meta;11061107my$wrev= revparse($filename);11081109# If the working copy is an old revision, lets get that version too for comparison.1110if(defined($wrev)and$wrev!=$meta->{revision} )1111{1112$oldmeta=$updater->getmeta($filename,$wrev);1113}11141115# TODO : All possible statuses aren't yet implemented1116my$status;1117# Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified1118$status="Up-to-date"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}1119and1120( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1121or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta->{filehash} ) )1122);11231124# Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified1125$status||="Needs Checkout"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrev1126and1127($state->{entries}{$filename}{unchanged}1128or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash} ) )1129);11301131# Need checkout if it exists in the repo but doesn't have a working copy1132$status||="Needs Checkout"if(not defined($wrev)and defined($meta->{revision} ) );11331134# Locally modified if working copy and repo copy have the same revision but there are local changes1135$status||="Locally Modified"if(defined($wrev)and defined($meta->{revision})and$wrev==$meta->{revision}and$state->{entries}{$filename}{modified_filename} );11361137# Needs Merge if working copy revision is less than repo copy and there are local changes1138$status||="Needs Merge"if(defined($wrev)and defined($meta->{revision} )and$meta->{revision} >$wrevand$state->{entries}{$filename}{modified_filename} );11391140$status||="Locally Added"if(defined($state->{entries}{$filename}{revision} )and not defined($meta->{revision} ) );1141$status||="Locally Removed"if(defined($wrev)and defined($meta->{revision} )and-$wrev==$meta->{revision} );1142$status||="Unresolved Conflict"if(defined($state->{entries}{$filename}{conflict} )and$state->{entries}{$filename}{conflict} =~/^\+=/);1143$status||="File had conflicts on merge"if(0);11441145$status||="Unknown";11461147print"M ===================================================================\n";1148print"M File:$filename\tStatus:$status\n";1149if(defined($state->{entries}{$filename}{revision}) )1150{1151print"M Working revision:\t".$state->{entries}{$filename}{revision} ."\n";1152}else{1153print"M Working revision:\tNo entry for$filename\n";1154}1155if(defined($meta->{revision}) )1156{1157print"M Repository revision:\t1.".$meta->{revision} ."\t$state->{repository}/$filename,v\n";1158print"M Sticky Tag:\t\t(none)\n";1159print"M Sticky Date:\t\t(none)\n";1160print"M Sticky Options:\t\t(none)\n";1161}else{1162print"M Repository revision:\tNo revision control file\n";1163}1164print"M\n";1165}11661167print"ok\n";1168}11691170sub req_diff1171{1172my($cmd,$data) =@_;11731174 argsplit("diff");11751176$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1177#$log->debug("status state : " . Dumper($state));11781179my($revision1,$revision2);1180if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1181{1182$revision1=$state->{opt}{r}[0];1183$revision2=$state->{opt}{r}[1];1184}else{1185$revision1=$state->{opt}{r};1186}11871188$revision1=~s/^1\.//if(defined($revision1) );1189$revision2=~s/^1\.//if(defined($revision2) );11901191$log->debug("Diffing revisions ". (defined($revision1) ?$revision1:"[NULL]") ." and ". (defined($revision2) ?$revision2:"[NULL]") );11921193# Grab a handle to the SQLite db and do any necessary updates1194my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1195$updater->update();11961197# if no files were specified, we need to work out what files we should be providing status on ...1198 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);11991200# foreach file specified on the commandline ...1201foreachmy$filename( @{$state->{args}} )1202{1203$filename= filecleanup($filename);12041205my($fh,$file1,$file2,$meta1,$meta2,$filediff);12061207my$wrev= revparse($filename);12081209# We need _something_ to diff against1210next unless(defined($wrev) );12111212# if we have a -r switch, use it1213if(defined($revision1) )1214{1215(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1216$meta1=$updater->getmeta($filename,$revision1);1217unless(defined($meta1)and$meta1->{filehash}ne"deleted")1218{1219print"E File$filenameat revision 1.$revision1doesn't exist\n";1220next;1221}1222 transmitfile($meta1->{filehash},$file1);1223}1224# otherwise we just use the working copy revision1225else1226{1227(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1228$meta1=$updater->getmeta($filename,$wrev);1229 transmitfile($meta1->{filehash},$file1);1230}12311232# if we have a second -r switch, use it too1233if(defined($revision2) )1234{1235(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1236$meta2=$updater->getmeta($filename,$revision2);12371238unless(defined($meta2)and$meta2->{filehash}ne"deleted")1239{1240print"E File$filenameat revision 1.$revision2doesn't exist\n";1241next;1242}12431244 transmitfile($meta2->{filehash},$file2);1245}1246# otherwise we just use the working copy1247else1248{1249$file2=$state->{entries}{$filename}{modified_filename};1250}12511252# if we have been given -r, and we don't have a $file2 yet, lets get one1253if(defined($revision1)and not defined($file2) )1254{1255(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1256$meta2=$updater->getmeta($filename,$wrev);1257 transmitfile($meta2->{filehash},$file2);1258}12591260# We need to have retrieved something useful1261next unless(defined($meta1) );12621263# Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified1264next if(not defined($meta2)and$wrev==$meta1->{revision}1265and1266( ($state->{entries}{$filename}{unchanged}and(not defined($state->{entries}{$filename}{conflict} )or$state->{entries}{$filename}{conflict} !~/^\+=/) )1267or(defined($state->{entries}{$filename}{modified_hash})and$state->{entries}{$filename}{modified_hash}eq$meta1->{filehash} ) )1268);12691270# Apparently we only show diffs for locally modified files1271next unless(defined($meta2)or defined($state->{entries}{$filename}{modified_filename} ) );12721273print"M Index:$filename\n";1274print"M ===================================================================\n";1275print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1276print"M retrieving revision 1.$meta1->{revision}\n"if(defined($meta1) );1277print"M retrieving revision 1.$meta2->{revision}\n"if(defined($meta2) );1278print"M diff ";1279foreachmy$opt(keys%{$state->{opt}} )1280{1281if(ref$state->{opt}{$opt}eq"ARRAY")1282{1283foreachmy$value( @{$state->{opt}{$opt}} )1284{1285print"-$opt$value";1286}1287}else{1288print"-$opt";1289print"$state->{opt}{$opt} "if(defined($state->{opt}{$opt} ) );1290}1291}1292print"$filename\n";12931294$log->info("Diffing$filename-r$meta1->{revision} -r ". ($meta2->{revision}or"workingcopy"));12951296($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);12971298if(exists$state->{opt}{u} )1299{1300system("diff -u -L '$filenamerevision 1.$meta1->{revision}' -L '$filename". (defined($meta2->{revision}) ?"revision 1.$meta2->{revision}":"working copy") ."'$file1$file2>$filediff");1301}else{1302system("diff$file1$file2>$filediff");1303}13041305while( <$fh> )1306{1307print"M$_";1308}1309close$fh;1310}13111312print"ok\n";1313}13141315sub req_log1316{1317my($cmd,$data) =@_;13181319 argsplit("log");13201321$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1322#$log->debug("log state : " . Dumper($state));13231324my($minrev,$maxrev);1325if(defined($state->{opt}{r} )and$state->{opt}{r} =~/([\d.]+)?(::?)([\d.]+)?/)1326{1327my$control=$2;1328$minrev=$1;1329$maxrev=$3;1330$minrev=~s/^1\.//if(defined($minrev) );1331$maxrev=~s/^1\.//if(defined($maxrev) );1332$minrev++if(defined($minrev)and$controleq"::");1333}13341335# Grab a handle to the SQLite db and do any necessary updates1336my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1337$updater->update();13381339# if no files were specified, we need to work out what files we should be providing status on ...1340 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);13411342# foreach file specified on the commandline ...1343foreachmy$filename( @{$state->{args}} )1344{1345$filename= filecleanup($filename);13461347my$headmeta=$updater->getmeta($filename);13481349my$revisions=$updater->getlog($filename);1350my$totalrevisions=scalar(@$revisions);13511352if(defined($minrev) )1353{1354$log->debug("Removing revisions less than$minrev");1355while(scalar(@$revisions) >0and$revisions->[-1]{revision} <$minrev)1356{1357pop@$revisions;1358}1359}1360if(defined($maxrev) )1361{1362$log->debug("Removing revisions greater than$maxrev");1363while(scalar(@$revisions) >0and$revisions->[0]{revision} >$maxrev)1364{1365shift@$revisions;1366}1367}13681369next unless(scalar(@$revisions) );13701371print"M\n";1372print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1373print"M Working file:$filename\n";1374print"M head: 1.$headmeta->{revision}\n";1375print"M branch:\n";1376print"M locks: strict\n";1377print"M access list:\n";1378print"M symbolic names:\n";1379print"M keyword substitution: kv\n";1380print"M total revisions:$totalrevisions;\tselected revisions: ".scalar(@$revisions) ."\n";1381print"M description:\n";13821383foreachmy$revision(@$revisions)1384{1385print"M ----------------------------\n";1386print"M revision 1.$revision->{revision}\n";1387# reformat the date for log output1388$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}) );1389$revision->{author} =~s/\s+.*//;1390$revision->{author} =~s/^(.{8}).*/$1/;1391print"M date:$revision->{modified}; author:$revision->{author}; state: ". ($revision->{filehash}eq"deleted"?"dead":"Exp") ."; lines: +2 -3\n";1392my$commitmessage=$updater->commitmessage($revision->{commithash});1393$commitmessage=~s/^/M /mg;1394print$commitmessage."\n";1395}1396print"M =============================================================================\n";1397}13981399print"ok\n";1400}14011402sub req_annotate1403{1404my($cmd,$data) =@_;14051406 argsplit("annotate");14071408$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));1409#$log->debug("status state : " . Dumper($state));14101411# Grab a handle to the SQLite db and do any necessary updates1412my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1413$updater->update();14141415# if no files were specified, we need to work out what files we should be providing annotate on ...1416 argsfromdir($updater)if(scalar( @{$state->{args}} ) ==0);14171418# we'll need a temporary checkout dir1419my$tmpdir= tempdir ( DIR =>$TEMP_DIR);1420my(undef,$file_index) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);1421$log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'");14221423$ENV{GIT_DIR} =$state->{CVSROOT} ."/";1424$ENV{GIT_INDEX_FILE} =$file_index;14251426chdir$tmpdir;14271428# foreach file specified on the commandline ...1429foreachmy$filename( @{$state->{args}} )1430{1431$filename= filecleanup($filename);14321433my$meta=$updater->getmeta($filename);14341435next unless($meta->{revision} );14361437# get all the commits that this file was in1438# in dense format -- aka skip dead revisions1439my$revisions=$updater->gethistorydense($filename);1440my$lastseenin=$revisions->[0][2];14411442# populate the temporary index based on the latest commit were we saw1443# the file -- but do it cheaply without checking out any files1444# TODO: if we got a revision from the client, use that instead1445# to look up the commithash in sqlite (still good to default to1446# the current head as we do now)1447system("git-read-tree",$lastseenin);1448unless($?==0)1449{1450die"Error running git-read-tree$lastseenin$file_index$!";1451}1452$log->info("Created index '$file_index' with commit$lastseenin- exit status$?");14531454# do a checkout of the file1455system('git-checkout-index','-f','-u',$filename);1456unless($?==0) {1457die"Error running git-checkout-index -f -u$filename:$!";1458}14591460$log->info("Annotate$filename");14611462# Prepare a file with the commits from the linearized1463# history that annotate should know about. This prevents1464# git-jsannotate telling us about commits we are hiding1465# from the client.14661467open(ANNOTATEHINTS,">$tmpdir/.annotate_hints")or die"Error opening >$tmpdir/.annotate_hints$!";1468for(my$i=0;$i<@$revisions;$i++)1469{1470print ANNOTATEHINTS $revisions->[$i][2];1471if($i+1<@$revisions) {# have we got a parent?1472print ANNOTATEHINTS ' '.$revisions->[$i+1][2];1473}1474print ANNOTATEHINTS "\n";1475}14761477print ANNOTATEHINTS "\n";1478close ANNOTATEHINTS;14791480my$annotatecmd='git-annotate';1481open(ANNOTATE,"-|",$annotatecmd,'-l','-S',"$tmpdir/.annotate_hints",$filename)1482or die"Error invoking$annotatecmd-l -S$tmpdir/.annotate_hints$filename:$!";1483my$metadata= {};1484print"E Annotations for$filename\n";1485print"E ***************\n";1486while( <ANNOTATE> )1487{1488if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)1489{1490my$commithash=$1;1491my$data=$2;1492unless(defined($metadata->{$commithash} ) )1493{1494$metadata->{$commithash} =$updater->getmeta($filename,$commithash);1495$metadata->{$commithash}{author} =~s/\s+.*//;1496$metadata->{$commithash}{author} =~s/^(.{8}).*/$1/;1497$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);1498}1499printf("M 1.%-5d (%-8s%10s):%s\n",1500$metadata->{$commithash}{revision},1501$metadata->{$commithash}{author},1502$metadata->{$commithash}{modified},1503$data1504);1505}else{1506$log->warn("Error in annotate output! LINE:$_");1507print"E Annotate error\n";1508next;1509}1510}1511close ANNOTATE;1512}15131514# done; get out of the tempdir1515chdir"/";15161517print"ok\n";15181519}15201521# This method takes the state->{arguments} array and produces two new arrays.1522# The first is $state->{args} which is everything before the '--' argument, and1523# the second is $state->{files} which is everything after it.1524sub argsplit1525{1526return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");15271528my$type=shift;15291530$state->{args} = [];1531$state->{files} = [];1532$state->{opt} = {};15331534if(defined($type) )1535{1536my$opt= {};1537$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");1538$opt= { v =>0, l =>0, R =>0}if($typeeq"status");1539$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");1540$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");1541$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");1542$opt= { k =>1, m =>1}if($typeeq"add");1543$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");1544$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");154515461547while(scalar( @{$state->{arguments}} ) >0)1548{1549my$arg=shift@{$state->{arguments}};15501551next if($argeq"--");1552next unless($arg=~/\S/);15531554# if the argument looks like a switch1555if($arg=~/^-(\w)(.*)/)1556{1557# if it's a switch that takes an argument1558if($opt->{$1} )1559{1560# If this switch has already been provided1561if($opt->{$1} >1and exists($state->{opt}{$1} ) )1562{1563$state->{opt}{$1} = [$state->{opt}{$1} ];1564if(length($2) >0)1565{1566push@{$state->{opt}{$1}},$2;1567}else{1568push@{$state->{opt}{$1}},shift@{$state->{arguments}};1569}1570}else{1571# if there's extra data in the arg, use that as the argument for the switch1572if(length($2) >0)1573{1574$state->{opt}{$1} =$2;1575}else{1576$state->{opt}{$1} =shift@{$state->{arguments}};1577}1578}1579}else{1580$state->{opt}{$1} =undef;1581}1582}1583else1584{1585push@{$state->{args}},$arg;1586}1587}1588}1589else1590{1591my$mode=0;15921593foreachmy$value( @{$state->{arguments}} )1594{1595if($valueeq"--")1596{1597$mode++;1598next;1599}1600push@{$state->{args}},$valueif($mode==0);1601push@{$state->{files}},$valueif($mode==1);1602}1603}1604}16051606# This method uses $state->{directory} to populate $state->{args} with a list of filenames1607sub argsfromdir1608{1609my$updater=shift;16101611$state->{args} = [];16121613foreachmy$file( @{$updater->gethead} )1614{1615next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );1616next unless($file->{name} =~s/^$state->{directory}//);1617push@{$state->{args}},$file->{name};1618}1619}16201621# This method cleans up the $state variable after a command that uses arguments has run1622sub statecleanup1623{1624$state->{files} = [];1625$state->{args} = [];1626$state->{arguments} = [];1627$state->{entries} = {};1628}16291630sub revparse1631{1632my$filename=shift;16331634returnundefunless(defined($state->{entries}{$filename}{revision} ) );16351636return$1if($state->{entries}{$filename}{revision} =~/^1\.(\d+)/);1637return-$1if($state->{entries}{$filename}{revision} =~/^-1\.(\d+)/);16381639returnundef;1640}16411642# This method takes a file hash and does a CVS "file transfer" which transmits the1643# size of the file, and then the file contents.1644# If a second argument $targetfile is given, the file is instead written out to1645# a file by the name of $targetfile1646sub transmitfile1647{1648my$filehash=shift;1649my$targetfile=shift;16501651if(defined($filehash)and$filehasheq"deleted")1652{1653$log->warn("filehash is 'deleted'");1654return;1655}16561657die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);16581659my$type=`git-cat-file -t$filehash`;1660 chomp$type;16611662 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );16631664 my$size= `git-cat-file -s $filehash`;1665chomp$size;16661667$log->debug("transmitfile($filehash) size=$size, type=$type");16681669if(open my$fh,'-|',"git-cat-file","blob",$filehash)1670{1671if(defined($targetfile) )1672{1673open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");1674print NEWFILE $_while( <$fh> );1675close NEWFILE;1676}else{1677print"$size\n";1678printwhile( <$fh> );1679}1680close$fhor die("Couldn't close filehandle for transmitfile()");1681}else{1682die("Couldn't execute git-cat-file");1683}1684}16851686# This method takes a file name, and returns ( $dirpart, $filepart ) which1687# refers to the directory porition and the file portion of the filename1688# respectively1689sub filenamesplit1690{1691my$filename=shift;16921693my($filepart,$dirpart) = ($filename,".");1694($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );1695$dirpart.="/";16961697return($filepart,$dirpart);1698}16991700sub filecleanup1701{1702my$filename=shift;17031704returnundefunless(defined($filename));1705if($filename=~/^\// )1706{1707print"E absolute filenames '$filename' not supported by server\n";1708returnundef;1709}17101711$filename=~s/^\.\///g;1712$filename=$state->{directory} .$filename;17131714return$filename;1715}17161717package GITCVS::log;17181719####1720#### Copyright The Open University UK - 2006.1721####1722#### Authors: Martyn Smith <martyn@catalyst.net.nz>1723#### Martin Langhoff <martin@catalyst.net.nz>1724####1725####17261727use strict;1728use warnings;17291730=head1 NAME17311732GITCVS::log17331734=head1 DESCRIPTION17351736This module provides very crude logging with a similar interface to1737Log::Log4perl17381739=head1 METHODS17401741=cut17421743=head2 new17441745Creates a new log object, optionally you can specify a filename here to1746indicate the file to log to. If no log file is specified, you can specifiy one1747later with method setfile, or indicate you no longer want logging with method1748nofile.17491750Until one of these methods is called, all log calls will buffer messages ready1751to write out.17521753=cut1754sub new1755{1756my$class=shift;1757my$filename=shift;17581759my$self= {};17601761bless$self,$class;17621763if(defined($filename) )1764{1765open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1766}17671768return$self;1769}17701771=head2 setfile17721773This methods takes a filename, and attempts to open that file as the log file.1774If successful, all buffered data is written out to the file, and any further1775logging is written directly to the file.17761777=cut1778sub setfile1779{1780my$self=shift;1781my$filename=shift;17821783if(defined($filename) )1784{1785open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");1786}17871788return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");17891790while(my$line=shift@{$self->{buffer}} )1791{1792print{$self->{fh}}$line;1793}1794}17951796=head2 nofile17971798This method indicates no logging is going to be used. It flushes any entries in1799the internal buffer, and sets a flag to ensure no further data is put there.18001801=cut1802sub nofile1803{1804my$self=shift;18051806$self->{nolog} =1;18071808return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");18091810$self->{buffer} = [];1811}18121813=head2 _logopen18141815Internal method. Returns true if the log file is open, false otherwise.18161817=cut1818sub _logopen1819{1820my$self=shift;18211822return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");1823return0;1824}18251826=head2 debug info warn fatal18271828These four methods are wrappers to _log. They provide the actual interface for1829logging data.18301831=cut1832sub debug {my$self=shift;$self->_log("debug",@_); }1833sub info {my$self=shift;$self->_log("info",@_); }1834subwarn{my$self=shift;$self->_log("warn",@_); }1835sub fatal {my$self=shift;$self->_log("fatal",@_); }18361837=head2 _log18381839This is an internal method called by the logging functions. It generates a1840timestamp and pushes the logged line either to file, or internal buffer.18411842=cut1843sub _log1844{1845my$self=shift;1846my$level=shift;18471848return if($self->{nolog} );18491850my@time=localtime;1851my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",1852$time[5] +1900,1853$time[4] +1,1854$time[3],1855$time[2],1856$time[1],1857$time[0],1858uc$level,1859);18601861if($self->_logopen)1862{1863print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";1864}else{1865push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";1866}1867}18681869=head2 DESTROY18701871This method simply closes the file handle if one is open18721873=cut1874sub DESTROY1875{1876my$self=shift;18771878if($self->_logopen)1879{1880close$self->{fh};1881}1882}18831884package GITCVS::updater;18851886####1887#### Copyright The Open University UK - 2006.1888####1889#### Authors: Martyn Smith <martyn@catalyst.net.nz>1890#### Martin Langhoff <martin@catalyst.net.nz>1891####1892####18931894use strict;1895use warnings;1896use DBI;18971898=head1 METHODS18991900=cut19011902=head2 new19031904=cut1905sub new1906{1907my$class=shift;1908my$config=shift;1909my$module=shift;1910my$log=shift;19111912die"Need to specify a git repository"unless(defined($config)and-d $config);1913die"Need to specify a module"unless(defined($module) );19141915$class=ref($class) ||$class;19161917my$self= {};19181919bless$self,$class;19201921$self->{dbdir} =$config."/";1922die"Database dir '$self->{dbdir}' isn't a directory"unless(defined($self->{dbdir})and-d $self->{dbdir} );19231924$self->{module} =$module;1925$self->{file} =$self->{dbdir} ."/gitcvs.$module.sqlite";19261927$self->{git_path} =$config."/";19281929$self->{log} =$log;19301931die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );19321933$self->{dbh} = DBI->connect("dbi:SQLite:dbname=".$self->{file},"","");19341935$self->{tables} = {};1936foreachmy$table($self->{dbh}->tables)1937{1938$table=~s/^"//;1939$table=~s/"$//;1940$self->{tables}{$table} =1;1941}19421943# Construct the revision table if required1944unless($self->{tables}{revision} )1945{1946$self->{dbh}->do("1947 CREATE TABLE revision (1948 name TEXT NOT NULL,1949 revision INTEGER NOT NULL,1950 filehash TEXT NOT NULL,1951 commithash TEXT NOT NULL,1952 author TEXT NOT NULL,1953 modified TEXT NOT NULL,1954 mode TEXT NOT NULL1955 )1956 ");1957}19581959# Construct the revision table if required1960unless($self->{tables}{head} )1961{1962$self->{dbh}->do("1963 CREATE TABLE head (1964 name TEXT NOT NULL,1965 revision INTEGER NOT NULL,1966 filehash TEXT NOT NULL,1967 commithash TEXT NOT NULL,1968 author TEXT NOT NULL,1969 modified TEXT NOT NULL,1970 mode TEXT NOT NULL1971 )1972 ");1973}19741975# Construct the properties table if required1976unless($self->{tables}{properties} )1977{1978$self->{dbh}->do("1979 CREATE TABLE properties (1980 key TEXT NOT NULL PRIMARY KEY,1981 value TEXT1982 )1983 ");1984}19851986# Construct the commitmsgs table if required1987unless($self->{tables}{commitmsgs} )1988{1989$self->{dbh}->do("1990 CREATE TABLE commitmsgs (1991 key TEXT NOT NULL PRIMARY KEY,1992 value TEXT1993 )1994 ");1995}19961997return$self;1998}19992000=head2 update20012002=cut2003sub update2004{2005my$self=shift;20062007# first lets get the commit list2008$ENV{GIT_DIR} =$self->{git_path};20092010# prepare database queries2011my$db_insert_rev=$self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);2012my$db_insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1);2013my$db_delete_head=$self->{dbh}->prepare_cached("DELETE FROM head",{},1);2014my$db_insert_head=$self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);20152016my$commitinfo=`git-cat-file commit$self->{module} 2>&1`;2017unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)2018{2019die("Invalid module '$self->{module}'");2020}202120222023my$git_log;2024my$lastcommit=$self->_get_prop("last_commit");20252026# Start exclusive lock here...2027$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";20282029# TODO: log processing is memory bound2030# if we can parse into a 2nd file that is in reverse order2031# we can probably do something really efficient2032my@git_log_params= ('--parents','--topo-order');20332034if(defined$lastcommit) {2035push@git_log_params,"$lastcommit..$self->{module}";2036}else{2037push@git_log_params,$self->{module};2038}2039open(GITLOG,'-|','git-log',@git_log_params)or die"Cannot call git-log:$!";20402041my@commits;20422043my%commit= ();20442045while( <GITLOG> )2046{2047chomp;2048if(m/^commit\s+(.*)$/) {2049# on ^commit lines put the just seen commit in the stack2050# and prime things for the next one2051if(keys%commit) {2052my%copy=%commit;2053unshift@commits, \%copy;2054%commit= ();2055}2056my@parents=split(m/\s+/,$1);2057$commit{hash} =shift@parents;2058$commit{parents} = \@parents;2059}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {2060# on rfc822-like lines seen before we see any message,2061# lowercase the entry and put it in the hash as key-value2062$commit{lc($1)} =$2;2063}else{2064# message lines - skip initial empty line2065# and trim whitespace2066if(!exists($commit{message}) &&m/^\s*$/) {2067# define it to mark the end of headers2068$commit{message} ='';2069next;2070}2071s/^\s+//;s/\s+$//;# trim ws2072$commit{message} .=$_."\n";2073}2074}2075close GITLOG;20762077unshift@commits, \%commitif(keys%commit);20782079# Now all the commits are in the @commits bucket2080# ordered by time DESC. for each commit that needs processing,2081# determine whether it's following the last head we've seen or if2082# it's on its own branch, grab a file list, and add whatever's changed2083# NOTE: $lastcommit refers to the last commit from previous run2084# $lastpicked is the last commit we picked in this run2085my$lastpicked;2086my$head= {};2087if(defined$lastcommit) {2088$lastpicked=$lastcommit;2089}20902091my$committotal=scalar(@commits);2092my$commitcount=0;20932094# Load the head table into $head (for cached lookups during the update process)2095foreachmy$file( @{$self->gethead()} )2096{2097$head->{$file->{name}} =$file;2098}20992100foreachmy$commit(@commits)2101{2102$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");2103if(defined$lastpicked)2104{2105if(!in_array($lastpicked, @{$commit->{parents}}))2106{2107# skip, we'll see this delta2108# as part of a merge later2109# warn "skipping off-track $commit->{hash}\n";2110next;2111}elsif(@{$commit->{parents}} >1) {2112# it is a merge commit, for each parent that is2113# not $lastpicked, see if we can get a log2114# from the merge-base to that parent to put it2115# in the message as a merge summary.2116my@parents= @{$commit->{parents}};2117foreachmy$parent(@parents) {2118# git-merge-base can potentially (but rarely) throw2119# several candidate merge bases. let's assume2120# that the first one is the best one.2121if($parenteq$lastpicked) {2122next;2123}2124open my$p,'git-merge-base '.$lastpicked.' '2125.$parent.'|';2126my@output= (<$p>);2127close$p;2128my$base=join('',@output);2129chomp$base;2130if($base) {2131my@merged;2132# print "want to log between $base $parent \n";2133open(GITLOG,'-|','git-log',"$base..$parent")2134or die"Cannot call git-log:$!";2135my$mergedhash;2136while(<GITLOG>) {2137chomp;2138if(!defined$mergedhash) {2139if(m/^commit\s+(.+)$/) {2140$mergedhash=$1;2141}else{2142next;2143}2144}else{2145# grab the first line that looks non-rfc8222146# aka has content after leading space2147if(m/^\s+(\S.*)$/) {2148my$title=$1;2149$title=substr($title,0,100);# truncate2150unshift@merged,"$mergedhash$title";2151undef$mergedhash;2152}2153}2154}2155close GITLOG;2156if(@merged) {2157$commit->{mergemsg} =$commit->{message};2158$commit->{mergemsg} .="\nSummary of merged commits:\n\n";2159foreachmy$summary(@merged) {2160$commit->{mergemsg} .="\t$summary\n";2161}2162$commit->{mergemsg} .="\n\n";2163# print "Message for $commit->{hash} \n$commit->{mergemsg}";2164}2165}2166}2167}2168}21692170# convert the date to CVS-happy format2171$commit->{date} ="$2$1$4$3$5"if($commit->{date} =~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/);21722173if(defined($lastpicked) )2174{2175my$filepipe=open(FILELIST,'-|','git-diff-tree','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");2176while( <FILELIST> )2177{2178unless(/^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)\s+(.*)$/o)2179{2180die("Couldn't process git-diff-tree line :$_");2181}21822183# $log->debug("File mode=$1, hash=$2, change=$3, name=$4");21842185my$git_perms="";2186$git_perms.="r"if($1&4);2187$git_perms.="w"if($1&2);2188$git_perms.="x"if($1&1);2189$git_perms="rw"if($git_permseq"");21902191if($3eq"D")2192{2193#$log->debug("DELETE $4");2194$head->{$4} = {2195 name =>$4,2196 revision =>$head->{$4}{revision} +1,2197 filehash =>"deleted",2198 commithash =>$commit->{hash},2199 modified =>$commit->{date},2200 author =>$commit->{author},2201 mode =>$git_perms,2202};2203$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2204}2205elsif($3eq"M")2206{2207#$log->debug("MODIFIED $4");2208$head->{$4} = {2209 name =>$4,2210 revision =>$head->{$4}{revision} +1,2211 filehash =>$2,2212 commithash =>$commit->{hash},2213 modified =>$commit->{date},2214 author =>$commit->{author},2215 mode =>$git_perms,2216};2217$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2218}2219elsif($3eq"A")2220{2221#$log->debug("ADDED $4");2222$head->{$4} = {2223 name =>$4,2224 revision =>1,2225 filehash =>$2,2226 commithash =>$commit->{hash},2227 modified =>$commit->{date},2228 author =>$commit->{author},2229 mode =>$git_perms,2230};2231$db_insert_rev->execute($4,$head->{$4}{revision},$2,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2232}2233else2234{2235$log->warn("UNKNOWN FILE CHANGE mode=$1, hash=$2, change=$3, name=$4");2236die;2237}2238}2239close FILELIST;2240}else{2241# this is used to detect files removed from the repo2242my$seen_files= {};22432244my$filepipe=open(FILELIST,'-|','git-ls-tree','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");2245while( <FILELIST> )2246{2247unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\s+(.*)$/o)2248{2249die("Couldn't process git-ls-tree line :$_");2250}22512252my($git_perms,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);22532254$seen_files->{$git_filename} =1;22552256my($oldhash,$oldrevision,$oldmode) = (2257$head->{$git_filename}{filehash},2258$head->{$git_filename}{revision},2259$head->{$git_filename}{mode}2260);22612262if($git_perms=~/^\d\d\d(\d)\d\d/o)2263{2264$git_perms="";2265$git_perms.="r"if($1&4);2266$git_perms.="w"if($1&2);2267$git_perms.="x"if($1&1);2268}else{2269$git_perms="rw";2270}22712272# unless the file exists with the same hash, we need to update it ...2273unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$git_perms)2274{2275my$newrevision= ($oldrevisionor0) +1;22762277$head->{$git_filename} = {2278 name =>$git_filename,2279 revision =>$newrevision,2280 filehash =>$git_hash,2281 commithash =>$commit->{hash},2282 modified =>$commit->{date},2283 author =>$commit->{author},2284 mode =>$git_perms,2285};228622872288$db_insert_rev->execute($git_filename,$newrevision,$git_hash,$commit->{hash},$commit->{date},$commit->{author},$git_perms);2289}2290}2291close FILELIST;22922293# Detect deleted files2294foreachmy$file(keys%$head)2295{2296unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")2297{2298$head->{$file}{revision}++;2299$head->{$file}{filehash} ="deleted";2300$head->{$file}{commithash} =$commit->{hash};2301$head->{$file}{modified} =$commit->{date};2302$head->{$file}{author} =$commit->{author};23032304$db_insert_rev->execute($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$commit->{date},$commit->{author},$head->{$file}{mode});2305}2306}2307# END : "Detect deleted files"2308}230923102311if(exists$commit->{mergemsg})2312{2313$db_insert_mergelog->execute($commit->{hash},$commit->{mergemsg});2314}23152316$lastpicked=$commit->{hash};23172318$self->_set_prop("last_commit",$commit->{hash});2319}23202321$db_delete_head->execute();2322foreachmy$file(keys%$head)2323{2324$db_insert_head->execute(2325$file,2326$head->{$file}{revision},2327$head->{$file}{filehash},2328$head->{$file}{commithash},2329$head->{$file}{modified},2330$head->{$file}{author},2331$head->{$file}{mode},2332);2333}2334# invalidate the gethead cache2335$self->{gethead_cache} =undef;233623372338# Ending exclusive lock here2339$self->{dbh}->commit()or die"Failed to commit changes to SQLite";2340}23412342sub _headrev2343{2344my$self=shift;2345my$filename=shift;23462347my$db_query=$self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1);2348$db_query->execute($filename);2349my($hash,$revision,$mode) =$db_query->fetchrow_array;23502351return($hash,$revision,$mode);2352}23532354sub _get_prop2355{2356my$self=shift;2357my$key=shift;23582359my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1);2360$db_query->execute($key);2361my($value) =$db_query->fetchrow_array;23622363return$value;2364}23652366sub _set_prop2367{2368my$self=shift;2369my$key=shift;2370my$value=shift;23712372my$db_query=$self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1);2373$db_query->execute($value,$key);23742375unless($db_query->rows)2376{2377$db_query=$self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1);2378$db_query->execute($key,$value);2379}23802381return$value;2382}23832384=head2 gethead23852386=cut23872388sub gethead2389{2390my$self=shift;23912392return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );23932394my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1);2395$db_query->execute();23962397my$tree= [];2398while(my$file=$db_query->fetchrow_hashref)2399{2400push@$tree,$file;2401}24022403$self->{gethead_cache} =$tree;24042405return$tree;2406}24072408=head2 getlog24092410=cut24112412sub getlog2413{2414my$self=shift;2415my$filename=shift;24162417my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2418$db_query->execute($filename);24192420my$tree= [];2421while(my$file=$db_query->fetchrow_hashref)2422{2423push@$tree,$file;2424}24252426return$tree;2427}24282429=head2 getmeta24302431This function takes a filename (with path) argument and returns a hashref of2432metadata for that file.24332434=cut24352436sub getmeta2437{2438my$self=shift;2439my$filename=shift;2440my$revision=shift;24412442my$db_query;2443if(defined($revision)and$revision=~/^\d+$/)2444{2445$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1);2446$db_query->execute($filename,$revision);2447}2448elsif(defined($revision)and$revision=~/^[a-zA-Z0-9]{40}$/)2449{2450$db_query=$self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1);2451$db_query->execute($filename,$revision);2452}else{2453$db_query=$self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1);2454$db_query->execute($filename);2455}24562457return$db_query->fetchrow_hashref;2458}24592460=head2 commitmessage24612462this function takes a commithash and returns the commit message for that commit24632464=cut2465sub commitmessage2466{2467my$self=shift;2468my$commithash=shift;24692470die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);24712472my$db_query;2473$db_query=$self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1);2474$db_query->execute($commithash);24752476my($message) =$db_query->fetchrow_array;24772478if(defined($message) )2479{2480$message.=" "if($message=~/\n$/);2481return$message;2482}24832484my@lines= safe_pipe_capture("git-cat-file","commit",$commithash);2485shift@lineswhile($lines[0] =~/\S/);2486$message=join("",@lines);2487$message.=" "if($message=~/\n$/);2488return$message;2489}24902491=head2 gethistory24922493This function takes a filename (with path) argument and returns an arrayofarrays2494containing revision,filehash,commithash ordered by revision descending24952496=cut2497sub gethistory2498{2499my$self=shift;2500my$filename=shift;25012502my$db_query;2503$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1);2504$db_query->execute($filename);25052506return$db_query->fetchall_arrayref;2507}25082509=head2 gethistorydense25102511This function takes a filename (with path) argument and returns an arrayofarrays2512containing revision,filehash,commithash ordered by revision descending.25132514This version of gethistory skips deleted entries -- so it is useful for annotate.2515The 'dense' part is a reference to a '--dense' option available for git-rev-list2516and other git tools that depend on it.25172518=cut2519sub gethistorydense2520{2521my$self=shift;2522my$filename=shift;25232524my$db_query;2525$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);2526$db_query->execute($filename);25272528return$db_query->fetchall_arrayref;2529}25302531=head2 in_array()25322533from Array::PAT - mimics the in_array() function2534found in PHP. Yuck but works for small arrays.25352536=cut2537sub in_array2538{2539my($check,@array) =@_;2540my$retval=0;2541foreachmy$test(@array){2542if($checkeq$test){2543$retval=1;2544}2545}2546return$retval;2547}25482549=head2 safe_pipe_capture25502551an alterative to `command` that allows input to be passed as an array2552to work around shell problems with weird characters in arguments25532554=cut2555sub safe_pipe_capture {25562557my@output;25582559if(my$pid=open my$child,'-|') {2560@output= (<$child>);2561close$childor die join(' ',@_).":$!$?";2562}else{2563exec(@_)or die"$!$?";# exec() can fail the executable can't be found2564}2565returnwantarray?@output:join('',@output);2566}2567256825691;