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@laptop.org> 12#### 13#### 14#### Released under the GNU Public License, version 2. 15#### 16#### 17 18use5.008; 19use strict; 20use warnings; 21use bytes; 22 23use Fcntl; 24use File::Temp qw/tempdir tempfile/; 25use File::Path qw/rmtree/; 26use File::Basename; 27use Getopt::Long qw(:config require_order no_ignore_case); 28 29my$VERSION='@@GIT_VERSION@@'; 30 31my$log= GITCVS::log->new(); 32my$cfg; 33 34my$DATE_LIST= { 35 Jan =>"01", 36 Feb =>"02", 37 Mar =>"03", 38 Apr =>"04", 39 May =>"05", 40 Jun =>"06", 41 Jul =>"07", 42 Aug =>"08", 43 Sep =>"09", 44 Oct =>"10", 45 Nov =>"11", 46 Dec =>"12", 47}; 48 49# Enable autoflush for STDOUT (otherwise the whole thing falls apart) 50$| =1; 51 52#### Definition and mappings of functions #### 53 54# NOTE: Despite the existence of req_CATCHALL and req_EMPTY unimplemented 55# requests, this list is incomplete. It is missing many rarer/optional 56# requests. Perhaps some clients require a claim of support for 57# these specific requests for main functionality to work? 58my$methods= { 59'Root'=> \&req_Root, 60'Valid-responses'=> \&req_Validresponses, 61'valid-requests'=> \&req_validrequests, 62'Directory'=> \&req_Directory, 63'Sticky'=> \&req_Sticky, 64'Entry'=> \&req_Entry, 65'Modified'=> \&req_Modified, 66'Unchanged'=> \&req_Unchanged, 67'Questionable'=> \&req_Questionable, 68'Argument'=> \&req_Argument, 69'Argumentx'=> \&req_Argument, 70'expand-modules'=> \&req_expandmodules, 71'add'=> \&req_add, 72'remove'=> \&req_remove, 73'co'=> \&req_co, 74'update'=> \&req_update, 75'ci'=> \&req_ci, 76'diff'=> \&req_diff, 77'log'=> \&req_log, 78'rlog'=> \&req_log, 79'tag'=> \&req_CATCHALL, 80'status'=> \&req_status, 81'admin'=> \&req_CATCHALL, 82'history'=> \&req_CATCHALL, 83'watchers'=> \&req_EMPTY, 84'editors'=> \&req_EMPTY, 85'noop'=> \&req_EMPTY, 86'annotate'=> \&req_annotate, 87'Global_option'=> \&req_Globaloption, 88}; 89 90############################################## 91 92 93# $state holds all the bits of information the clients sends us that could 94# potentially be useful when it comes to actually _doing_ something. 95my$state= { prependdir =>''}; 96 97# Work is for managing temporary working directory 98my$work= 99{ 100state=>undef,# undef, 1 (empty), 2 (with stuff) 101 workDir =>undef, 102index=>undef, 103 emptyDir =>undef, 104 tmpDir =>undef 105}; 106 107$log->info("--------------- STARTING -----------------"); 108 109my$usage= 110"Usage: git cvsserver [options] [pserver|server] [<directory> ...]\n". 111" --base-path <path> : Prepend to requested CVSROOT\n". 112" Can be read from GIT_CVSSERVER_BASE_PATH\n". 113" --strict-paths : Don't allow recursing into subdirectories\n". 114" --export-all : Don't check for gitcvs.enabled in config\n". 115" --version, -V : Print version information and exit\n". 116" -h, -H : Print usage information and exit\n". 117"\n". 118"<directory> ... is a list of allowed directories. If no directories\n". 119"are given, all are allowed. This is an additional restriction, gitcvs\n". 120"access still needs to be enabled by the gitcvs.enabled config option.\n". 121"Alternately, one directory may be specified in GIT_CVSSERVER_ROOT.\n"; 122 123my@opts= ('h|H','version|V', 124'base-path=s','strict-paths','export-all'); 125GetOptions($state,@opts) 126or die$usage; 127 128if($state->{version}) { 129print"git-cvsserver version$VERSION\n"; 130exit; 131} 132if($state->{help}) { 133print$usage; 134exit; 135} 136 137my$TEMP_DIR= tempdir( CLEANUP =>1); 138$log->debug("Temporary directory is '$TEMP_DIR'"); 139 140$state->{method} ='ext'; 141if(@ARGV) { 142if($ARGV[0]eq'pserver') { 143$state->{method} ='pserver'; 144shift@ARGV; 145}elsif($ARGV[0]eq'server') { 146shift@ARGV; 147} 148} 149 150# everything else is a directory 151$state->{allowed_roots} = [@ARGV]; 152 153# don't export the whole system unless the users requests it 154if($state->{'export-all'} && !@{$state->{allowed_roots}}) { 155die"--export-all can only be used together with an explicit whitelist\n"; 156} 157 158# Environment handling for running under git-shell 159if(exists$ENV{GIT_CVSSERVER_BASE_PATH}) { 160if($state->{'base-path'}) { 161die"Cannot specify base path both ways.\n"; 162} 163my$base_path=$ENV{GIT_CVSSERVER_BASE_PATH}; 164$state->{'base-path'} =$base_path; 165$log->debug("Picked up base path '$base_path' from environment.\n"); 166} 167if(exists$ENV{GIT_CVSSERVER_ROOT}) { 168if(@{$state->{allowed_roots}}) { 169die"Cannot specify roots both ways:@ARGV\n"; 170} 171my$allowed_root=$ENV{GIT_CVSSERVER_ROOT}; 172$state->{allowed_roots} = [$allowed_root]; 173$log->debug("Picked up allowed root '$allowed_root' from environment.\n"); 174} 175 176# if we are called with a pserver argument, 177# deal with the authentication cat before entering the 178# main loop 179if($state->{method}eq'pserver') { 180my$line= <STDIN>;chomp$line; 181unless($line=~/^BEGIN (AUTH|VERIFICATION) REQUEST$/) { 182die"E Do not understand$line- expecting BEGIN AUTH REQUEST\n"; 183} 184my$request=$1; 185$line= <STDIN>;chomp$line; 186unless(req_Root('root',$line)) {# reuse Root 187print"E Invalid root$line\n"; 188exit1; 189} 190$line= <STDIN>;chomp$line; 191my$user=$line; 192$line= <STDIN>;chomp$line; 193my$password=$line; 194 195if($usereq'anonymous') { 196# "A" will be 1 byte, use length instead in case the 197# encryption method ever changes (yeah, right!) 198if(length($password) >1) { 199print"E Don't supply a password for the `anonymous' user\n"; 200print"I HATE YOU\n"; 201exit1; 202} 203 204# Fall through to LOVE 205}else{ 206# Trying to authenticate a user 207if(not exists$cfg->{gitcvs}->{authdb}) { 208print"E the repo config file needs a [gitcvs] section with an 'authdb' parameter set to the filename of the authentication database\n"; 209print"I HATE YOU\n"; 210exit1; 211} 212 213my$authdb=$cfg->{gitcvs}->{authdb}; 214 215unless(-e $authdb) { 216print"E The authentication database specified in [gitcvs.authdb] does not exist\n"; 217print"I HATE YOU\n"; 218exit1; 219} 220 221my$auth_ok; 222open my$passwd,"<",$authdbor die$!; 223while(<$passwd>) { 224if(m{^\Q$user\E:(.*)}) { 225if(crypt($user, descramble($password))eq$1) { 226$auth_ok=1; 227} 228}; 229} 230close$passwd; 231 232unless($auth_ok) { 233print"I HATE YOU\n"; 234exit1; 235} 236 237# Fall through to LOVE 238} 239 240# For checking whether the user is anonymous on commit 241$state->{user} =$user; 242 243$line= <STDIN>;chomp$line; 244unless($lineeq"END$requestREQUEST") { 245die"E Do not understand$line-- expecting END$requestREQUEST\n"; 246} 247print"I LOVE YOU\n"; 248exit if$requesteq'VERIFICATION';# cvs login 249# and now back to our regular programme... 250} 251 252# Keep going until the client closes the connection 253while(<STDIN>) 254{ 255chomp; 256 257# Check to see if we've seen this method, and call appropriate function. 258if(/^([\w-]+)(?:\s+(.*))?$/and defined($methods->{$1}) ) 259{ 260# use the $methods hash to call the appropriate sub for this command 261#$log->info("Method : $1"); 262&{$methods->{$1}}($1,$2); 263}else{ 264# log fatal because we don't understand this function. If this happens 265# we're fairly screwed because we don't know if the client is expecting 266# a response. If it is, the client will hang, we'll hang, and the whole 267# thing will be custard. 268$log->fatal("Don't understand command$_\n"); 269die("Unknown command$_"); 270} 271} 272 273$log->debug("Processing time : user=". (times)[0] ." system=". (times)[1]); 274$log->info("--------------- FINISH -----------------"); 275 276chdir'/'; 277exit0; 278 279# Magic catchall method. 280# This is the method that will handle all commands we haven't yet 281# implemented. It simply sends a warning to the log file indicating a 282# command that hasn't been implemented has been invoked. 283sub req_CATCHALL 284{ 285my($cmd,$data) =@_; 286$log->warn("Unhandled command : req_$cmd:$data"); 287} 288 289# This method invariably succeeds with an empty response. 290sub req_EMPTY 291{ 292print"ok\n"; 293} 294 295# Root pathname \n 296# Response expected: no. Tell the server which CVSROOT to use. Note that 297# pathname is a local directory and not a fully qualified CVSROOT variable. 298# pathname must already exist; if creating a new root, use the init 299# request, not Root. pathname does not include the hostname of the server, 300# how to access the server, etc.; by the time the CVS protocol is in use, 301# connection, authentication, etc., are already taken care of. The Root 302# request must be sent only once, and it must be sent before any requests 303# other than Valid-responses, valid-requests, UseUnchanged, Set or init. 304sub req_Root 305{ 306my($cmd,$data) =@_; 307$log->debug("req_Root :$data"); 308 309unless($data=~ m#^/#) { 310print"error 1 Root must be an absolute pathname\n"; 311return0; 312} 313 314my$cvsroot=$state->{'base-path'} ||''; 315$cvsroot=~ s#/+$##; 316$cvsroot.=$data; 317 318if($state->{CVSROOT} 319&& ($state->{CVSROOT}ne$cvsroot)) { 320print"error 1 Conflicting roots specified\n"; 321return0; 322} 323 324$state->{CVSROOT} =$cvsroot; 325 326$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 327 328if(@{$state->{allowed_roots}}) { 329my$allowed=0; 330foreachmy$dir(@{$state->{allowed_roots}}) { 331next unless$dir=~ m#^/#; 332$dir=~ s#/+$##; 333if($state->{'strict-paths'}) { 334if($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { 335$allowed=1; 336last; 337} 338}elsif($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { 339$allowed=1; 340last; 341} 342} 343 344unless($allowed) { 345print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 346print"E\n"; 347print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 348return0; 349} 350} 351 352unless(-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { 353print"E$ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; 354print"E\n"; 355print"error 1$ENV{GIT_DIR} is not a valid repository\n"; 356return0; 357} 358 359my@gitvars=`git config -l`; 360if($?) { 361print"E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; 362print"E\n"; 363print"error 1 - problem executing git-config\n"; 364return0; 365} 366foreachmy$line(@gitvars) 367{ 368next unless($line=~/^(gitcvs)\.(?:(ext|pserver)\.)?([\w-]+)=(.*)$/); 369unless($2) { 370$cfg->{$1}{$3} =$4; 371}else{ 372$cfg->{$1}{$2}{$3} =$4; 373} 374} 375 376my$enabled= ($cfg->{gitcvs}{$state->{method}}{enabled} 377||$cfg->{gitcvs}{enabled}); 378unless($state->{'export-all'} || 379($enabled&&$enabled=~/^\s*(1|true|yes)\s*$/i)) { 380print"E GITCVS emulation needs to be enabled on this repo\n"; 381print"E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; 382print"E\n"; 383print"error 1 GITCVS emulation disabled\n"; 384return0; 385} 386 387my$logfile=$cfg->{gitcvs}{$state->{method}}{logfile} ||$cfg->{gitcvs}{logfile}; 388if($logfile) 389{ 390$log->setfile($logfile); 391}else{ 392$log->nofile(); 393} 394 395return1; 396} 397 398# Global_option option \n 399# Response expected: no. Transmit one of the global options `-q', `-Q', 400# `-l', `-t', `-r', or `-n'. option must be one of those strings, no 401# variations (such as combining of options) are allowed. For graceful 402# handling of valid-requests, it is probably better to make new global 403# options separate requests, rather than trying to add them to this 404# request. 405sub req_Globaloption 406{ 407my($cmd,$data) =@_; 408$log->debug("req_Globaloption :$data"); 409$state->{globaloptions}{$data} =1; 410} 411 412# Valid-responses request-list \n 413# Response expected: no. Tell the server what responses the client will 414# accept. request-list is a space separated list of tokens. 415sub req_Validresponses 416{ 417my($cmd,$data) =@_; 418$log->debug("req_Validresponses :$data"); 419 420# TODO : re-enable this, currently it's not particularly useful 421#$state->{validresponses} = [ split /\s+/, $data ]; 422} 423 424# valid-requests \n 425# Response expected: yes. Ask the server to send back a Valid-requests 426# response. 427sub req_validrequests 428{ 429my($cmd,$data) =@_; 430 431$log->debug("req_validrequests"); 432 433$log->debug("SEND : Valid-requests ".join(" ",keys%$methods)); 434$log->debug("SEND : ok"); 435 436print"Valid-requests ".join(" ",keys%$methods) ."\n"; 437print"ok\n"; 438} 439 440# Directory local-directory \n 441# Additional data: repository \n. Response expected: no. Tell the server 442# what directory to use. The repository should be a directory name from a 443# previous server response. Note that this both gives a default for Entry 444# and Modified and also for ci and the other commands; normal usage is to 445# send Directory for each directory in which there will be an Entry or 446# Modified, and then a final Directory for the original directory, then the 447# command. The local-directory is relative to the top level at which the 448# command is occurring (i.e. the last Directory which is sent before the 449# command); to indicate that top level, `.' should be sent for 450# local-directory. 451sub req_Directory 452{ 453my($cmd,$data) =@_; 454 455my$repository= <STDIN>; 456chomp$repository; 457 458 459$state->{localdir} =$data; 460$state->{repository} =$repository; 461$state->{path} =$repository; 462$state->{path} =~s/^\Q$state->{CVSROOT}\E\///; 463$state->{module} =$1if($state->{path} =~s/^(.*?)(\/|$)//); 464$state->{path} .="/"if($state->{path} =~ /\S/ ); 465 466$state->{directory} =$state->{localdir}; 467$state->{directory} =""if($state->{directory}eq"."); 468$state->{directory} .="/"if($state->{directory} =~ /\S/ ); 469 470if( (not defined($state->{prependdir})or$state->{prependdir}eq'')and$state->{localdir}eq"."and$state->{path} =~/\S/) 471{ 472$log->info("Setting prepend to '$state->{path}'"); 473$state->{prependdir} =$state->{path}; 474my%entries; 475foreachmy$entry(keys%{$state->{entries}} ) 476{ 477$entries{$state->{prependdir} .$entry} =$state->{entries}{$entry}; 478} 479$state->{entries}=\%entries; 480 481my%dirMap; 482foreachmy$dir(keys%{$state->{dirMap}} ) 483{ 484$dirMap{$state->{prependdir} .$dir} =$state->{dirMap}{$dir}; 485} 486$state->{dirMap}=\%dirMap; 487} 488 489if(defined($state->{prependdir} ) ) 490{ 491$log->debug("Prepending '$state->{prependdir}' to state|directory"); 492$state->{directory} =$state->{prependdir} .$state->{directory} 493} 494 495if( !defined($state->{dirMap}{$state->{directory}}) ) 496{ 497$state->{dirMap}{$state->{directory}} = 498{ 499'names'=> {} 500#'tagspec' => undef 501}; 502} 503 504$log->debug("req_Directory : localdir=$datarepository=$repositorypath=$state->{path} directory=$state->{directory} module=$state->{module}"); 505} 506 507# Sticky tagspec \n 508# Response expected: no. Tell the server that the directory most 509# recently specified with Directory has a sticky tag or date 510# tagspec. The first character of tagspec is T for a tag, D for 511# a date, or some other character supplied by a Set-sticky 512# response from a previous request to the server. The remainder 513# of tagspec contains the actual tag or date, again as supplied 514# by Set-sticky. 515# The server should remember Static-directory and Sticky requests 516# for a particular directory; the client need not resend them each 517# time it sends a Directory request for a given directory. However, 518# the server is not obliged to remember them beyond the context 519# of a single command. 520sub req_Sticky 521{ 522my($cmd,$tagspec) =@_; 523 524my($stickyInfo); 525if($tagspeceq"") 526{ 527# nothing 528} 529elsif($tagspec=~/^T([^ ]+)\s*$/) 530{ 531$stickyInfo= {'tag'=>$1}; 532} 533elsif($tagspec=~/^D([0-9.]+)\s*$/) 534{ 535$stickyInfo= {'date'=>$1}; 536} 537else 538{ 539die"Unknown tag_or_date format\n"; 540} 541$state->{dirMap}{$state->{directory}}{stickyInfo}=$stickyInfo; 542 543$log->debug("req_Sticky : tagspec=$tagspecrepository=$state->{repository}" 544." path=$state->{path} directory=$state->{directory}" 545." module=$state->{module}"); 546} 547 548# Entry entry-line \n 549# Response expected: no. Tell the server what version of a file is on the 550# local machine. The name in entry-line is a name relative to the directory 551# most recently specified with Directory. If the user is operating on only 552# some files in a directory, Entry requests for only those files need be 553# included. If an Entry request is sent without Modified, Is-modified, or 554# Unchanged, it means the file is lost (does not exist in the working 555# directory). If both Entry and one of Modified, Is-modified, or Unchanged 556# are sent for the same file, Entry must be sent first. For a given file, 557# one can send Modified, Is-modified, or Unchanged, but not more than one 558# of these three. 559sub req_Entry 560{ 561my($cmd,$data) =@_; 562 563#$log->debug("req_Entry : $data"); 564 565my@data=split(/\//,$data, -1); 566 567$state->{entries}{$state->{directory}.$data[1]} = { 568 revision =>$data[2], 569 conflict =>$data[3], 570 options =>$data[4], 571 tag_or_date =>$data[5], 572}; 573 574$state->{dirMap}{$state->{directory}}{names}{$data[1]} ='F'; 575 576$log->info("Received entry line '$data' => '".$state->{directory} .$data[1] ."'"); 577} 578 579# Questionable filename \n 580# Response expected: no. Additional data: no. Tell the server to check 581# whether filename should be ignored, and if not, next time the server 582# sends responses, send (in a M response) `?' followed by the directory and 583# filename. filename must not contain `/'; it needs to be a file in the 584# directory named by the most recent Directory request. 585sub req_Questionable 586{ 587my($cmd,$data) =@_; 588 589$log->debug("req_Questionable :$data"); 590$state->{entries}{$state->{directory}.$data}{questionable} =1; 591} 592 593# add \n 594# Response expected: yes. Add a file or directory. This uses any previous 595# Argument, Directory, Entry, or Modified requests, if they have been sent. 596# The last Directory sent specifies the working directory at the time of 597# the operation. To add a directory, send the directory to be added using 598# Directory and Argument requests. 599sub req_add 600{ 601my($cmd,$data) =@_; 602 603 argsplit("add"); 604 605my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 606$updater->update(); 607 608my$addcount=0; 609 610foreachmy$filename( @{$state->{args}} ) 611{ 612$filename= filecleanup($filename); 613 614my$meta=$updater->getmeta($filename); 615my$wrev= revparse($filename); 616 617if($wrev&&$meta&& ($wrev=~/^-/)) 618{ 619# previously removed file, add back 620$log->info("added file$filenamewas previously removed, send$meta->{revision}"); 621 622print"MT +updated\n"; 623print"MT text U\n"; 624print"MT fname$filename\n"; 625print"MT newline\n"; 626print"MT -updated\n"; 627 628unless($state->{globaloptions}{-n} ) 629{ 630my($filepart,$dirpart) = filenamesplit($filename,1); 631 632print"Created$dirpart\n"; 633print$state->{CVSROOT} ."/$state->{module}/$filename\n"; 634 635# this is an "entries" line 636my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 637$log->debug("/$filepart/$meta->{revision}//$kopts/"); 638print"/$filepart/$meta->{revision}//$kopts/\n"; 639# permissions 640$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); 641print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; 642# transmit file 643 transmitfile($meta->{filehash}); 644} 645 646next; 647} 648 649unless(defined($state->{entries}{$filename}{modified_filename} ) ) 650{ 651print"E cvs add: nothing known about `$filename'\n"; 652next; 653} 654# TODO : check we're not squashing an already existing file 655if(defined($state->{entries}{$filename}{revision} ) ) 656{ 657print"E cvs add: `$filename' has already been entered\n"; 658next; 659} 660 661my($filepart,$dirpart) = filenamesplit($filename,1); 662 663print"E cvs add: scheduling file `$filename' for addition\n"; 664 665print"Checked-in$dirpart\n"; 666print"$filename\n"; 667my$kopts= kopts_from_path($filename,"file", 668$state->{entries}{$filename}{modified_filename}); 669print"/$filepart/0//$kopts/\n"; 670 671my$requestedKopts=$state->{opt}{k}; 672if(defined($requestedKopts)) 673{ 674$requestedKopts="-k$requestedKopts"; 675} 676else 677{ 678$requestedKopts=""; 679} 680if($koptsne$requestedKopts) 681{ 682$log->warn("Ignoring requested -k='$requestedKopts'" 683." for '$filename'; detected -k='$kopts' instead"); 684#TODO: Also have option to send warning to user? 685} 686 687$addcount++; 688} 689 690if($addcount==1) 691{ 692print"E cvs add: use `cvs commit' to add this file permanently\n"; 693} 694elsif($addcount>1) 695{ 696print"E cvs add: use `cvs commit' to add these files permanently\n"; 697} 698 699print"ok\n"; 700} 701 702# remove \n 703# Response expected: yes. Remove a file. This uses any previous Argument, 704# Directory, Entry, or Modified requests, if they have been sent. The last 705# Directory sent specifies the working directory at the time of the 706# operation. Note that this request does not actually do anything to the 707# repository; the only effect of a successful remove request is to supply 708# the client with a new entries line containing `-' to indicate a removed 709# file. In fact, the client probably could perform this operation without 710# contacting the server, although using remove may cause the server to 711# perform a few more checks. The client sends a subsequent ci request to 712# actually record the removal in the repository. 713sub req_remove 714{ 715my($cmd,$data) =@_; 716 717 argsplit("remove"); 718 719# Grab a handle to the SQLite db and do any necessary updates 720my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log); 721$updater->update(); 722 723#$log->debug("add state : " . Dumper($state)); 724 725my$rmcount=0; 726 727foreachmy$filename( @{$state->{args}} ) 728{ 729$filename= filecleanup($filename); 730 731if(defined($state->{entries}{$filename}{unchanged} )or defined($state->{entries}{$filename}{modified_filename} ) ) 732{ 733print"E cvs remove: file `$filename' still in working directory\n"; 734next; 735} 736 737my$meta=$updater->getmeta($filename); 738my$wrev= revparse($filename); 739 740unless(defined($wrev) ) 741{ 742print"E cvs remove: nothing known about `$filename'\n"; 743next; 744} 745 746if(defined($wrev)and($wrev=~/^-/) ) 747{ 748print"E cvs remove: file `$filename' already scheduled for removal\n"; 749next; 750} 751 752unless($wreveq$meta->{revision} ) 753{ 754# TODO : not sure if the format of this message is quite correct. 755print"E cvs remove: Up to date check failed for `$filename'\n"; 756next; 757} 758 759 760my($filepart,$dirpart) = filenamesplit($filename,1); 761 762print"E cvs remove: scheduling `$filename' for removal\n"; 763 764print"Checked-in$dirpart\n"; 765print"$filename\n"; 766my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash}); 767print"/$filepart/-$wrev//$kopts/\n"; 768 769$rmcount++; 770} 771 772if($rmcount==1) 773{ 774print"E cvs remove: use `cvs commit' to remove this file permanently\n"; 775} 776elsif($rmcount>1) 777{ 778print"E cvs remove: use `cvs commit' to remove these files permanently\n"; 779} 780 781print"ok\n"; 782} 783 784# Modified filename \n 785# Response expected: no. Additional data: mode, \n, file transmission. Send 786# the server a copy of one locally modified file. filename is a file within 787# the most recent directory sent with Directory; it must not contain `/'. 788# If the user is operating on only some files in a directory, only those 789# files need to be included. This can also be sent without Entry, if there 790# is no entry for the file. 791sub req_Modified 792{ 793my($cmd,$data) =@_; 794 795my$mode= <STDIN>; 796defined$mode 797or(print"E end of file reading mode for$data\n"),return; 798chomp$mode; 799my$size= <STDIN>; 800defined$size 801or(print"E end of file reading size of$data\n"),return; 802chomp$size; 803 804# Grab config information 805my$blocksize=8192; 806my$bytesleft=$size; 807my$tmp; 808 809# Get a filehandle/name to write it to 810my($fh,$filename) = tempfile( DIR =>$TEMP_DIR); 811 812# Loop over file data writing out to temporary file. 813while($bytesleft) 814{ 815$blocksize=$bytesleftif($bytesleft<$blocksize); 816read STDIN,$tmp,$blocksize; 817print$fh $tmp; 818$bytesleft-=$blocksize; 819} 820 821close$fh 822or(print"E failed to write temporary,$filename:$!\n"),return; 823 824# Ensure we have something sensible for the file mode 825if($mode=~/u=(\w+)/) 826{ 827$mode=$1; 828}else{ 829$mode="rw"; 830} 831 832# Save the file data in $state 833$state->{entries}{$state->{directory}.$data}{modified_filename} =$filename; 834$state->{entries}{$state->{directory}.$data}{modified_mode} =$mode; 835$state->{entries}{$state->{directory}.$data}{modified_hash} =`git hash-object$filename`; 836$state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; 837 838 #$log->debug("req_Modified : file=$datamode=$modesize=$size"); 839} 840 841# Unchanged filename\n 842# Response expected: no. Tell the server that filename has not been 843# modified in the checked out directory. The filename is a file within the 844# most recent directory sent with Directory; it must not contain `/'. 845sub req_Unchanged 846{ 847 my ($cmd,$data) =@_; 848 849$state->{entries}{$state->{directory}.$data}{unchanged} = 1; 850 851 #$log->debug("req_Unchanged :$data"); 852} 853 854# Argument text\n 855# Response expected: no. Save argument for use in a subsequent command. 856# Arguments accumulate until an argument-using command is given, at which 857# point they are forgotten. 858# Argumentx text\n 859# Response expected: no. Append\nfollowed by text to the current argument 860# being saved. 861sub req_Argument 862{ 863 my ($cmd,$data) =@_; 864 865 # Argumentx means: append to last Argument (with a newline in front) 866 867$log->debug("$cmd:$data"); 868 869 if ($cmdeq 'Argumentx') { 870 ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" .$data; 871 } else { 872 push @{$state->{arguments}},$data; 873 } 874} 875 876# expand-modules\n 877# Response expected: yes. Expand the modules which are specified in the 878# arguments. Returns the data in Module-expansion responses. Note that the 879# server can assume that this is checkout or export, not rtag or rdiff; the 880# latter do not access the working directory and thus have no need to 881# expand modules on the client side. Expand may not be the best word for 882# what this request does. It does not necessarily tell you all the files 883# contained in a module, for example. Basically it is a way of telling you 884# which working directories the server needs to know about in order to 885# handle a checkout of the specified modules. For example, suppose that the 886# server has a module defined by 887# aliasmodule -a 1dir 888# That is, one can check out aliasmodule and it will take 1dir in the 889# repository and check it out to 1dir in the working directory. Now suppose 890# the client already has this module checked out and is planning on using 891# the co request to update it. Without using expand-modules, the client 892# would have two bad choices: it could either send information about all 893# working directories under the current directory, which could be 894# unnecessarily slow, or it could be ignorant of the fact that aliasmodule 895# stands for 1dir, and neglect to send information for 1dir, which would 896# lead to incorrect operation. With expand-modules, the client would first 897# ask for the module to be expanded: 898sub req_expandmodules 899{ 900 my ($cmd,$data) =@_; 901 902 argsplit(); 903 904$log->debug("req_expandmodules : " . ( defined($data) ?$data: "[NULL]" ) ); 905 906 unless ( ref$state->{arguments} eq "ARRAY" ) 907 { 908 print "ok\n"; 909 return; 910 } 911 912 foreach my$module( @{$state->{arguments}} ) 913 { 914$log->debug("SEND : Module-expansion$module"); 915 print "Module-expansion$module\n"; 916 } 917 918 print "ok\n"; 919 statecleanup(); 920} 921 922# co\n 923# Response expected: yes. Get files from the repository. This uses any 924# previous Argument, Directory, Entry, or Modified requests, if they have 925# been sent. Arguments to this command are module names; the client cannot 926# know what directories they correspond to except by (1) just sending the 927# co request, and then seeing what directory names the server sends back in 928# its responses, and (2) the expand-modules request. 929sub req_co 930{ 931 my ($cmd,$data) =@_; 932 933 argsplit("co"); 934 935 # Provide list of modules, if -c was used. 936 if (exists$state->{opt}{c}) { 937 my$showref= `git show-ref --heads`; 938 for my$line(split '\n',$showref) { 939 if ($line=~ m% refs/heads/(.*)$%) { 940 print "M$1\t$1\n"; 941 } 942 } 943 print "ok\n"; 944 return 1; 945 } 946 947 my$module=$state->{args}[0]; 948$state->{module} =$module; 949 my$checkout_path=$module; 950 951 # use the user specified directory if we're given it 952$checkout_path=$state->{opt}{d}if(exists($state->{opt}{d} ) ); 953 954$log->debug("req_co : ". (defined($data) ?$data:"[NULL]") ); 955 956$log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); 957 958$ENV{GIT_DIR} =$state->{CVSROOT} ."/"; 959 960# Grab a handle to the SQLite db and do any necessary updates 961my$updater= GITCVS::updater->new($state->{CVSROOT},$module,$log); 962$updater->update(); 963 964$checkout_path=~ s|/$||;# get rid of trailing slashes 965 966# Eclipse seems to need the Clear-sticky command 967# to prepare the 'Entries' file for the new directory. 968print"Clear-sticky$checkout_path/\n"; 969print$state->{CVSROOT} ."/$module/\n"; 970print"Clear-static-directory$checkout_path/\n"; 971print$state->{CVSROOT} ."/$module/\n"; 972print"Clear-sticky$checkout_path/\n";# yes, twice 973print$state->{CVSROOT} ."/$module/\n"; 974print"Template$checkout_path/\n"; 975print$state->{CVSROOT} ."/$module/\n"; 976print"0\n"; 977 978# instruct the client that we're checking out to $checkout_path 979print"E cvs checkout: Updating$checkout_path\n"; 980 981my%seendirs= (); 982my$lastdir=''; 983 984# recursive 985sub prepdir { 986my($dir,$repodir,$remotedir,$seendirs) =@_; 987my$parent= dirname($dir); 988$dir=~ s|/+$||; 989$repodir=~ s|/+$||; 990$remotedir=~ s|/+$||; 991$parent=~ s|/+$||; 992$log->debug("announcedir$dir,$repodir,$remotedir"); 993 994if($parenteq'.'||$parenteq'./') { 995$parent=''; 996} 997# recurse to announce unseen parents first 998if(length($parent) && !exists($seendirs->{$parent})) { 999 prepdir($parent,$repodir,$remotedir,$seendirs);1000}1001# Announce that we are going to modify at the parent level1002if($parent) {1003print"E cvs checkout: Updating$remotedir/$parent\n";1004}else{1005print"E cvs checkout: Updating$remotedir\n";1006}1007print"Clear-sticky$remotedir/$parent/\n";1008print"$repodir/$parent/\n";10091010print"Clear-static-directory$remotedir/$dir/\n";1011print"$repodir/$dir/\n";1012print"Clear-sticky$remotedir/$parent/\n";# yes, twice1013print"$repodir/$parent/\n";1014print"Template$remotedir/$dir/\n";1015print"$repodir/$dir/\n";1016print"0\n";10171018$seendirs->{$dir} =1;1019}10201021foreachmy$git( @{$updater->gethead} )1022{1023# Don't want to check out deleted files1024next if($git->{filehash}eq"deleted");10251026my$fullName=$git->{name};1027($git->{name},$git->{dir} ) = filenamesplit($git->{name});10281029if(length($git->{dir}) &&$git->{dir}ne'./'1030&&$git->{dir}ne$lastdir) {1031unless(exists($seendirs{$git->{dir}})) {1032 prepdir($git->{dir},$state->{CVSROOT} ."/$module/",1033$checkout_path, \%seendirs);1034$lastdir=$git->{dir};1035$seendirs{$git->{dir}} =1;1036}1037print"E cvs checkout: Updating /$checkout_path/$git->{dir}\n";1038}10391040# modification time of this file1041print"Mod-time$git->{modified}\n";10421043# print some information to the client1044if(defined($git->{dir} )and$git->{dir}ne"./")1045{1046print"M U$checkout_path/$git->{dir}$git->{name}\n";1047}else{1048print"M U$checkout_path/$git->{name}\n";1049}10501051# instruct client we're sending a file to put in this path1052print"Created$checkout_path/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."\n";10531054print$state->{CVSROOT} ."/$module/". (defined($git->{dir} )and$git->{dir}ne"./"?$git->{dir} ."/":"") ."$git->{name}\n";10551056# this is an "entries" line1057my$kopts= kopts_from_path($fullName,"sha1",$git->{filehash});1058print"/$git->{name}/$git->{revision}//$kopts/\n";1059# permissions1060print"u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";10611062# transmit file1063 transmitfile($git->{filehash});1064}10651066print"ok\n";10671068 statecleanup();1069}10701071# update \n1072# Response expected: yes. Actually do a cvs update command. This uses any1073# previous Argument, Directory, Entry, or Modified requests, if they have1074# been sent. The last Directory sent specifies the working directory at the1075# time of the operation. The -I option is not used--files which the client1076# can decide whether to ignore are not mentioned and the client sends the1077# Questionable request for others.1078sub req_update1079{1080my($cmd,$data) =@_;10811082$log->debug("req_update : ". (defined($data) ?$data:"[NULL]"));10831084 argsplit("update");10851086#1087# It may just be a client exploring the available heads/modules1088# in that case, list them as top level directories and leave it1089# at that. Eclipse uses this technique to offer you a list of1090# projects (heads in this case) to checkout.1091#1092if($state->{module}eq'') {1093my$showref=`git show-ref --heads`;1094print"E cvs update: Updating .\n";1095formy$line(split'\n',$showref) {1096if($line=~ m% refs/heads/(.*)$%) {1097print"E cvs update: New directory `$1'\n";1098}1099}1100print"ok\n";1101return1;1102}110311041105# Grab a handle to the SQLite db and do any necessary updates1106my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);11071108$updater->update();11091110 argsfromdir($updater);11111112#$log->debug("update state : " . Dumper($state));11131114my$last_dirname="///";11151116# foreach file specified on the command line ...1117foreachmy$filename( @{$state->{args}} )1118{1119$filename= filecleanup($filename);11201121$log->debug("Processing file$filename");11221123unless($state->{globaloptions}{-Q} ||$state->{globaloptions}{-q} )1124{1125my$cur_dirname= dirname($filename);1126if($cur_dirnamene$last_dirname)1127{1128$last_dirname=$cur_dirname;1129if($cur_dirnameeq"")1130{1131$cur_dirname=".";1132}1133print"E cvs update: Updating$cur_dirname\n";1134}1135}11361137# if we have a -C we should pretend we never saw modified stuff1138if(exists($state->{opt}{C} ) )1139{1140delete$state->{entries}{$filename}{modified_hash};1141delete$state->{entries}{$filename}{modified_filename};1142$state->{entries}{$filename}{unchanged} =1;1143}11441145my$meta;1146if(defined($state->{opt}{r})and$state->{opt}{r} =~/^(1\.\d+)$/)1147{1148$meta=$updater->getmeta($filename,$1);1149}else{1150$meta=$updater->getmeta($filename);1151}11521153# If -p was given, "print" the contents of the requested revision.1154if(exists($state->{opt}{p} ) ) {1155if(defined($meta->{revision} ) ) {1156$log->info("Printing '$filename' revision ".$meta->{revision});11571158 transmitfile($meta->{filehash}, {print=>1});1159}11601161next;1162}11631164if( !defined$meta)1165{1166$meta= {1167 name =>$filename,1168 revision =>'0',1169 filehash =>'added'1170};1171}11721173my$oldmeta=$meta;11741175my$wrev= revparse($filename);11761177# If the working copy is an old revision, lets get that version too for comparison.1178if(defined($wrev)and$wrevne$meta->{revision} )1179{1180$oldmeta=$updater->getmeta($filename,$wrev);1181}11821183#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");11841185# Files are up to date if the working copy and repo copy have the same revision,1186# and the working copy is unmodified _and_ the user hasn't specified -C1187next if(defined($wrev)1188and defined($meta->{revision})1189and$wreveq$meta->{revision}1190and$state->{entries}{$filename}{unchanged}1191and not exists($state->{opt}{C} ) );11921193# If the working copy and repo copy have the same revision,1194# but the working copy is modified, tell the client it's modified1195if(defined($wrev)1196and defined($meta->{revision})1197and$wreveq$meta->{revision}1198and defined($state->{entries}{$filename}{modified_hash})1199and not exists($state->{opt}{C} ) )1200{1201$log->info("Tell the client the file is modified");1202print"MT text M\n";1203print"MT fname$filename\n";1204print"MT newline\n";1205next;1206}12071208if($meta->{filehash}eq"deleted")1209{1210# TODO: If it has been modified in the sandbox, error out1211# with the appropriate message, rather than deleting a modified1212# file.12131214my($filepart,$dirpart) = filenamesplit($filename,1);12151216$log->info("Removing '$filename' from working copy (no longer in the repo)");12171218print"E cvs update: `$filename' is no longer in the repository\n";1219# Don't want to actually _DO_ the update if -n specified1220unless($state->{globaloptions}{-n} ) {1221print"Removed$dirpart\n";1222print"$filepart\n";1223}1224}1225elsif(not defined($state->{entries}{$filename}{modified_hash} )1226or$state->{entries}{$filename}{modified_hash}eq$oldmeta->{filehash}1227or$meta->{filehash}eq'added')1228{1229# normal update, just send the new revision (either U=Update,1230# or A=Add, or R=Remove)1231if(defined($wrev) && ($wrev=~/^-/) )1232{1233$log->info("Tell the client the file is scheduled for removal");1234print"MT text R\n";1235print"MT fname$filename\n";1236print"MT newline\n";1237next;1238}1239elsif( (!defined($wrev) ||$wreveq'0') &&1240(!defined($meta->{revision}) ||$meta->{revision}eq'0') )1241{1242$log->info("Tell the client the file is scheduled for addition");1243print"MT text A\n";1244print"MT fname$filename\n";1245print"MT newline\n";1246next;12471248}1249else{1250$log->info("UpdatingX3 '$filename' to ".$meta->{revision});1251print"MT +updated\n";1252print"MT text U\n";1253print"MT fname$filename\n";1254print"MT newline\n";1255print"MT -updated\n";1256}12571258my($filepart,$dirpart) = filenamesplit($filename,1);12591260# Don't want to actually _DO_ the update if -n specified1261unless($state->{globaloptions}{-n} )1262{1263if(defined($wrev) )1264{1265# instruct client we're sending a file to put in this path as a replacement1266print"Update-existing$dirpart\n";1267$log->debug("Updating existing file 'Update-existing$dirpart'");1268}else{1269# instruct client we're sending a file to put in this path as a new file1270print"Clear-static-directory$dirpart\n";1271print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";1272print"Clear-sticky$dirpart\n";1273print$state->{CVSROOT} ."/$state->{module}/$dirpart\n";12741275$log->debug("Creating new file 'Created$dirpart'");1276print"Created$dirpart\n";1277}1278print$state->{CVSROOT} ."/$state->{module}/$filename\n";12791280# this is an "entries" line1281my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1282$log->debug("/$filepart/$meta->{revision}//$kopts/");1283print"/$filepart/$meta->{revision}//$kopts/\n";12841285# permissions1286$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1287print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";12881289# transmit file1290 transmitfile($meta->{filehash});1291}1292}else{1293my($filepart,$dirpart) = filenamesplit($meta->{name},1);12941295my$mergeDir= setupTmpDir();12961297my$file_local=$filepart.".mine";1298my$mergedFile="$mergeDir/$file_local";1299system("ln","-s",$state->{entries}{$filename}{modified_filename},$file_local);1300my$file_old=$filepart.".".$oldmeta->{revision};1301 transmitfile($oldmeta->{filehash}, { targetfile =>$file_old});1302my$file_new=$filepart.".".$meta->{revision};1303 transmitfile($meta->{filehash}, { targetfile =>$file_new});13041305# we need to merge with the local changes ( M=successful merge, C=conflict merge )1306$log->info("Merging$file_local,$file_old,$file_new");1307print"M Merging differences between$oldmeta->{revision} and$meta->{revision} into$filename\n";13081309$log->debug("Temporary directory for merge is$mergeDir");13101311my$return=system("git","merge-file",$file_local,$file_old,$file_new);1312$return>>=8;13131314 cleanupTmpDir();13151316if($return==0)1317{1318$log->info("Merged successfully");1319print"M M$filename\n";1320$log->debug("Merged$dirpart");13211322# Don't want to actually _DO_ the update if -n specified1323unless($state->{globaloptions}{-n} )1324{1325print"Merged$dirpart\n";1326$log->debug($state->{CVSROOT} ."/$state->{module}/$filename");1327print$state->{CVSROOT} ."/$state->{module}/$filename\n";1328my$kopts= kopts_from_path("$dirpart/$filepart",1329"file",$mergedFile);1330$log->debug("/$filepart/$meta->{revision}//$kopts/");1331print"/$filepart/$meta->{revision}//$kopts/\n";1332}1333}1334elsif($return==1)1335{1336$log->info("Merged with conflicts");1337print"E cvs update: conflicts found in$filename\n";1338print"M C$filename\n";13391340# Don't want to actually _DO_ the update if -n specified1341unless($state->{globaloptions}{-n} )1342{1343print"Merged$dirpart\n";1344print$state->{CVSROOT} ."/$state->{module}/$filename\n";1345my$kopts= kopts_from_path("$dirpart/$filepart",1346"file",$mergedFile);1347print"/$filepart/$meta->{revision}/+/$kopts/\n";1348}1349}1350else1351{1352$log->warn("Merge failed");1353next;1354}13551356# Don't want to actually _DO_ the update if -n specified1357unless($state->{globaloptions}{-n} )1358{1359# permissions1360$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");1361print"u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";13621363# transmit file, format is single integer on a line by itself (file1364# size) followed by the file contents1365# TODO : we should copy files in blocks1366my$data=`cat$mergedFile`;1367$log->debug("File size : " . length($data));1368 print length($data) . "\n";1369 print$data;1370 }1371 }13721373 }13741375 print "ok\n";1376}13771378sub req_ci1379{1380 my ($cmd,$data) =@_;13811382 argsplit("ci");13831384 #$log->debug("State : " . Dumper($state));13851386$log->info("req_ci : " . ( defined($data) ?$data: "[NULL]" ));13871388 if ($state->{method} eq 'pserver' and$state->{user} eq 'anonymous' )1389 {1390 print "error 1 anonymous user cannot commit via pserver\n";1391 cleanupWorkTree();1392 exit;1393 }13941395 if ( -e$state->{CVSROOT} . "/index" )1396 {1397$log->warn("file 'index' already exists in the git repository");1398 print "error 1 Index already exists in git repo\n";1399 cleanupWorkTree();1400 exit;1401 }14021403 # Grab a handle to the SQLite db and do any necessary updates1404 my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1405$updater->update();14061407 # Remember where the head was at the beginning.1408 my$parenthash= `git show-ref -s refs/heads/$state->{module}`;1409 chomp$parenthash;1410 if ($parenthash!~ /^[0-9a-f]{40}$/) {1411 print "error 1 pserver cannot find the current HEAD of module";1412 cleanupWorkTree();1413 exit;1414 }14151416 setupWorkTree($parenthash);14171418$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");14191420$log->info("Created index '$work->{index}' for head$state->{module} - exit status$?");14211422 my@committedfiles= ();1423 my%oldmeta;14241425 # foreach file specified on the command line ...1426 foreach my$filename( @{$state->{args}} )1427 {1428 my$committedfile=$filename;1429$filename= filecleanup($filename);14301431 next unless ( exists$state->{entries}{$filename}{modified_filename} or not$state->{entries}{$filename}{unchanged} );14321433 my$meta=$updater->getmeta($filename);1434$oldmeta{$filename} =$meta;14351436 my$wrev= revparse($filename);14371438 my ($filepart,$dirpart) = filenamesplit($filename);14391440 # do a checkout of the file if it is part of this tree1441 if ($wrev) {1442 system('git', 'checkout-index', '-f', '-u',$filename);1443 unless ($?== 0) {1444 die "Error running git-checkout-index -f -u$filename:$!";1445 }1446 }14471448 my$addflag= 0;1449 my$rmflag= 0;1450$rmflag= 1 if ( defined($wrev) and ($wrev=~/^-/) );1451$addflag= 1 unless ( -e$filename);14521453 # Do up to date checking1454 unless ($addflagor$wreveq$meta->{revision} or1455 ($rmflagand$wreveq "-$meta->{revision}" ) )1456 {1457 # fail everything if an up to date check fails1458 print "error 1 Up to date check failed for$filename\n";1459 cleanupWorkTree();1460 exit;1461 }14621463 push@committedfiles,$committedfile;1464$log->info("Committing$filename");14651466 system("mkdir","-p",$dirpart) unless ( -d$dirpart);14671468 unless ($rmflag)1469 {1470$log->debug("rename$state->{entries}{$filename}{modified_filename}$filename");1471 rename$state->{entries}{$filename}{modified_filename},$filename;14721473 # Calculate modes to remove1474 my$invmode= "";1475 foreach ( qw (r w x) ) {$invmode.=$_unless ($state->{entries}{$filename}{modified_mode} =~ /$_/); }14761477$log->debug("chmod u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode. "$filename");1478 system("chmod","u+" .$state->{entries}{$filename}{modified_mode} . "-" .$invmode,$filename);1479 }14801481 if ($rmflag)1482 {1483$log->info("Removing file '$filename'");1484 unlink($filename);1485 system("git", "update-index", "--remove",$filename);1486 }1487 elsif ($addflag)1488 {1489$log->info("Adding file '$filename'");1490 system("git", "update-index", "--add",$filename);1491 } else {1492$log->info("UpdatingX2 file '$filename'");1493 system("git", "update-index",$filename);1494 }1495 }14961497 unless ( scalar(@committedfiles) > 0 )1498 {1499 print "E No files to commit\n";1500 print "ok\n";1501 cleanupWorkTree();1502 return;1503 }15041505 my$treehash= `git write-tree`;1506 chomp$treehash;15071508$log->debug("Treehash :$treehash, Parenthash :$parenthash");15091510 # write our commit message out if we have one ...1511 my ($msg_fh,$msg_filename) = tempfile( DIR =>$TEMP_DIR);1512 print$msg_fh$state->{opt}{m};# if ( exists ($state->{opt}{m} ) );1513 if ( defined ($cfg->{gitcvs}{commitmsgannotation} ) ) {1514 if ($cfg->{gitcvs}{commitmsgannotation} !~ /^\s*$/) {1515 print$msg_fh"\n\n".$cfg->{gitcvs}{commitmsgannotation}."\n"1516 }1517 } else {1518 print$msg_fh"\n\nvia git-CVS emulator\n";1519 }1520 close$msg_fh;15211522 my$commithash= `git commit-tree $treehash-p $parenthash<$msg_filename`;1523chomp($commithash);1524$log->info("Commit hash :$commithash");15251526unless($commithash=~/[a-zA-Z0-9]{40}/)1527{1528$log->warn("Commit failed (Invalid commit hash)");1529print"error 1 Commit failed (unknown reason)\n";1530 cleanupWorkTree();1531exit;1532}15331534### Emulate git-receive-pack by running hooks/update1535my@hook= ($ENV{GIT_DIR}.'hooks/update',"refs/heads/$state->{module}",1536$parenthash,$commithash);1537if( -x $hook[0] ) {1538unless(system(@hook) ==0)1539{1540$log->warn("Commit failed (update hook declined to update ref)");1541print"error 1 Commit failed (update hook declined)\n";1542 cleanupWorkTree();1543exit;1544}1545}15461547### Update the ref1548if(system(qw(git update-ref -m),"cvsserver ci",1549"refs/heads/$state->{module}",$commithash,$parenthash)) {1550$log->warn("update-ref for$state->{module} failed.");1551print"error 1 Cannot commit -- update first\n";1552 cleanupWorkTree();1553exit;1554}15551556### Emulate git-receive-pack by running hooks/post-receive1557my$hook=$ENV{GIT_DIR}.'hooks/post-receive';1558if( -x $hook) {1559open(my$pipe,"|$hook") ||die"can't fork$!";15601561local$SIG{PIPE} =sub{die'pipe broke'};15621563print$pipe"$parenthash$commithashrefs/heads/$state->{module}\n";15641565close$pipe||die"bad pipe:$!$?";1566}15671568$updater->update();15691570### Then hooks/post-update1571$hook=$ENV{GIT_DIR}.'hooks/post-update';1572if(-x $hook) {1573system($hook,"refs/heads/$state->{module}");1574}15751576# foreach file specified on the command line ...1577foreachmy$filename(@committedfiles)1578{1579$filename= filecleanup($filename);15801581my$meta=$updater->getmeta($filename);1582unless(defined$meta->{revision}) {1583$meta->{revision} ="1.1";1584}15851586my($filepart,$dirpart) = filenamesplit($filename,1);15871588$log->debug("Checked-in$dirpart:$filename");15891590print"M$state->{CVSROOT}/$state->{module}/$filename,v <--$dirpart$filepart\n";1591if(defined$meta->{filehash} &&$meta->{filehash}eq"deleted")1592{1593print"M new revision: delete; previous revision:$oldmeta{$filename}{revision}\n";1594print"Remove-entry$dirpart\n";1595print"$filename\n";1596}else{1597if($meta->{revision}eq"1.1") {1598print"M initial revision: 1.1\n";1599}else{1600print"M new revision:$meta->{revision}; previous revision:$oldmeta{$filename}{revision}\n";1601}1602print"Checked-in$dirpart\n";1603print"$filename\n";1604my$kopts= kopts_from_path($filename,"sha1",$meta->{filehash});1605print"/$filepart/$meta->{revision}//$kopts/\n";1606}1607}16081609 cleanupWorkTree();1610print"ok\n";1611}16121613sub req_status1614{1615my($cmd,$data) =@_;16161617 argsplit("status");16181619$log->info("req_status : ". (defined($data) ?$data:"[NULL]"));1620#$log->debug("status state : " . Dumper($state));16211622# Grab a handle to the SQLite db and do any necessary updates1623my$updater;1624$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1625$updater->update();16261627# if no files were specified, we need to work out what files we should1628# be providing status on ...1629 argsfromdir($updater);16301631# foreach file specified on the command line ...1632foreachmy$filename( @{$state->{args}} )1633{1634$filename= filecleanup($filename);16351636if(exists($state->{opt}{l}) &&1637index($filename,'/',length($state->{prependdir})) >=0)1638{1639next;1640}16411642my$meta=$updater->getmeta($filename);1643my$oldmeta=$meta;16441645my$wrev= revparse($filename);16461647# If the working copy is an old revision, lets get that1648# version too for comparison.1649if(defined($wrev)and$wrevne$meta->{revision} )1650{1651$oldmeta=$updater->getmeta($filename,$wrev);1652}16531654# TODO : All possible statuses aren't yet implemented1655my$status;1656# Files are up to date if the working copy and repo copy have1657# the same revision, and the working copy is unmodified1658if(defined($wrev)and defined($meta->{revision})and1659$wreveq$meta->{revision}and1660( ($state->{entries}{$filename}{unchanged}and1661(not defined($state->{entries}{$filename}{conflict} )or1662$state->{entries}{$filename}{conflict} !~/^\+=/) )or1663(defined($state->{entries}{$filename}{modified_hash})and1664$state->{entries}{$filename}{modified_hash}eq1665$meta->{filehash} ) ) )1666{1667$status="Up-to-date"1668}16691670# Need checkout if the working copy has a different (usually1671# older) revision than the repo copy, and the working copy is1672# unmodified1673if(defined($wrev)and defined($meta->{revision} )and1674$meta->{revision}ne$wrevand1675($state->{entries}{$filename}{unchanged}or1676(defined($state->{entries}{$filename}{modified_hash})and1677$state->{entries}{$filename}{modified_hash}eq1678$oldmeta->{filehash} ) ) )1679{1680$status||="Needs Checkout";1681}16821683# Need checkout if it exists in the repo but doesn't have a working1684# copy1685if(not defined($wrev)and defined($meta->{revision} ) )1686{1687$status||="Needs Checkout";1688}16891690# Locally modified if working copy and repo copy have the1691# same revision but there are local changes1692if(defined($wrev)and defined($meta->{revision})and1693$wreveq$meta->{revision}and1694$state->{entries}{$filename}{modified_filename} )1695{1696$status||="Locally Modified";1697}16981699# Needs Merge if working copy revision is different1700# (usually older) than repo copy and there are local changes1701if(defined($wrev)and defined($meta->{revision} )and1702$meta->{revision}ne$wrevand1703$state->{entries}{$filename}{modified_filename} )1704{1705$status||="Needs Merge";1706}17071708if(defined($state->{entries}{$filename}{revision} )and1709not defined($meta->{revision} ) )1710{1711$status||="Locally Added";1712}1713if(defined($wrev)and defined($meta->{revision} )and1714$wreveq"-$meta->{revision}")1715{1716$status||="Locally Removed";1717}1718if(defined($state->{entries}{$filename}{conflict} )and1719$state->{entries}{$filename}{conflict} =~/^\+=/)1720{1721$status||="Unresolved Conflict";1722}1723if(0)1724{1725$status||="File had conflicts on merge";1726}17271728$status||="Unknown";17291730my($filepart) = filenamesplit($filename);17311732print"M =======". ("=" x 60) ."\n";1733print"M File:$filepart\tStatus:$status\n";1734if(defined($state->{entries}{$filename}{revision}) )1735{1736print"M Working revision:\t".1737$state->{entries}{$filename}{revision} ."\n";1738}else{1739print"M Working revision:\tNo entry for$filename\n";1740}1741if(defined($meta->{revision}) )1742{1743print"M Repository revision:\t".1744$meta->{revision} .1745"\t$state->{CVSROOT}/$state->{module}/$filename,v\n";1746my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};1747my($tag)=($tagOrDate=~m/^T(.+)$/);1748if( !defined($tag) )1749{1750$tag="(none)";1751}1752print"M Sticky Tag:\t\t$tag\n";1753my($date)=($tagOrDate=~m/^D(.+)$/);1754if( !defined($date) )1755{1756$date="(none)";1757}1758print"M Sticky Date:\t\t$date\n";1759my($options)=$state->{entries}{$filename}{options};1760if($optionseq"")1761{1762$options="(none)";1763}1764print"M Sticky Options:\t\t$options\n";1765}else{1766print"M Repository revision:\tNo revision control file\n";1767}1768print"M\n";1769}17701771print"ok\n";1772}17731774sub req_diff1775{1776my($cmd,$data) =@_;17771778 argsplit("diff");17791780$log->debug("req_diff : ". (defined($data) ?$data:"[NULL]"));1781#$log->debug("status state : " . Dumper($state));17821783my($revision1,$revision2);1784if(defined($state->{opt}{r} )and ref$state->{opt}{r}eq"ARRAY")1785{1786$revision1=$state->{opt}{r}[0];1787$revision2=$state->{opt}{r}[1];1788}else{1789$revision1=$state->{opt}{r};1790}17911792$log->debug("Diffing revisions ".1793(defined($revision1) ?$revision1:"[NULL]") .1794" and ". (defined($revision2) ?$revision2:"[NULL]") );17951796# Grab a handle to the SQLite db and do any necessary updates1797my$updater;1798$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1799$updater->update();18001801# if no files were specified, we need to work out what files we should1802# be providing status on ...1803 argsfromdir($updater);18041805# foreach file specified on the command line ...1806foreachmy$filename( @{$state->{args}} )1807{1808$filename= filecleanup($filename);18091810my($fh,$file1,$file2,$meta1,$meta2,$filediff);18111812my$wrev= revparse($filename);18131814# We need _something_ to diff against1815next unless(defined($wrev) );18161817# if we have a -r switch, use it1818if(defined($revision1) )1819{1820(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1821$meta1=$updater->getmeta($filename,$revision1);1822unless(defined($meta1)and$meta1->{filehash}ne"deleted")1823{1824print"E File$filenameat revision$revision1doesn't exist\n";1825next;1826}1827 transmitfile($meta1->{filehash}, { targetfile =>$file1});1828}1829# otherwise we just use the working copy revision1830else1831{1832(undef,$file1) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1833$meta1=$updater->getmeta($filename,$wrev);1834 transmitfile($meta1->{filehash}, { targetfile =>$file1});1835}18361837# if we have a second -r switch, use it too1838if(defined($revision2) )1839{1840(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1841$meta2=$updater->getmeta($filename,$revision2);18421843unless(defined($meta2)and$meta2->{filehash}ne"deleted")1844{1845print"E File$filenameat revision$revision2doesn't exist\n";1846next;1847}18481849 transmitfile($meta2->{filehash}, { targetfile =>$file2});1850}1851# otherwise we just use the working copy1852else1853{1854$file2=$state->{entries}{$filename}{modified_filename};1855}18561857# if we have been given -r, and we don't have a $file2 yet, lets1858# get one1859if(defined($revision1)and not defined($file2) )1860{1861(undef,$file2) = tempfile( DIR =>$TEMP_DIR, OPEN =>0);1862$meta2=$updater->getmeta($filename,$wrev);1863 transmitfile($meta2->{filehash}, { targetfile =>$file2});1864}18651866# We need to have retrieved something useful1867next unless(defined($meta1) );18681869# Files to date if the working copy and repo copy have the same1870# revision, and the working copy is unmodified1871if(not defined($meta2)and$wreveq$meta1->{revision}and1872( ($state->{entries}{$filename}{unchanged}and1873(not defined($state->{entries}{$filename}{conflict} )or1874$state->{entries}{$filename}{conflict} !~/^\+=/) )or1875(defined($state->{entries}{$filename}{modified_hash})and1876$state->{entries}{$filename}{modified_hash}eq1877$meta1->{filehash} ) ) )1878{1879next;1880}18811882# Apparently we only show diffs for locally modified files1883unless(defined($meta2)or1884defined($state->{entries}{$filename}{modified_filename} ) )1885{1886next;1887}18881889print"M Index:$filename\n";1890print"M =======". ("=" x 60) ."\n";1891print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1892if(defined($meta1) )1893{1894print"M retrieving revision$meta1->{revision}\n"1895}1896if(defined($meta2) )1897{1898print"M retrieving revision$meta2->{revision}\n"1899}1900print"M diff ";1901foreachmy$opt(keys%{$state->{opt}} )1902{1903if(ref$state->{opt}{$opt}eq"ARRAY")1904{1905foreachmy$value( @{$state->{opt}{$opt}} )1906{1907print"-$opt$value";1908}1909}else{1910print"-$opt";1911if(defined($state->{opt}{$opt} ) )1912{1913print"$state->{opt}{$opt} "1914}1915}1916}1917print"$filename\n";19181919$log->info("Diffing$filename-r$meta1->{revision} -r ".1920($meta2->{revision}or"workingcopy"));19211922($fh,$filediff) = tempfile ( DIR =>$TEMP_DIR);19231924if(exists$state->{opt}{u} )1925{1926system("diff -u -L '$filenamerevision$meta1->{revision}'".1927" -L '$filename".1928(defined($meta2->{revision}) ?1929"revision$meta2->{revision}":1930"working copy") .1931"'$file1$file2>$filediff");1932}else{1933system("diff$file1$file2>$filediff");1934}19351936while( <$fh> )1937{1938print"M$_";1939}1940close$fh;1941}19421943print"ok\n";1944}19451946sub req_log1947{1948my($cmd,$data) =@_;19491950 argsplit("log");19511952$log->debug("req_log : ". (defined($data) ?$data:"[NULL]"));1953#$log->debug("log state : " . Dumper($state));19541955my($revFilter);1956if(defined($state->{opt}{r} ) )1957{1958$revFilter=$state->{opt}{r};1959}19601961# Grab a handle to the SQLite db and do any necessary updates1962my$updater;1963$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);1964$updater->update();19651966# if no files were specified, we need to work out what files we1967# should be providing status on ...1968 argsfromdir($updater);19691970# foreach file specified on the command line ...1971foreachmy$filename( @{$state->{args}} )1972{1973$filename= filecleanup($filename);19741975my$headmeta=$updater->getmeta($filename);19761977my($revisions,$totalrevisions) =$updater->getlog($filename,1978$revFilter);19791980next unless(scalar(@$revisions) );19811982print"M\n";1983print"M RCS file:$state->{CVSROOT}/$state->{module}/$filename,v\n";1984print"M Working file:$filename\n";1985print"M head:$headmeta->{revision}\n";1986print"M branch:\n";1987print"M locks: strict\n";1988print"M access list:\n";1989print"M symbolic names:\n";1990print"M keyword substitution: kv\n";1991print"M total revisions:$totalrevisions;\tselected revisions: ".1992scalar(@$revisions) ."\n";1993print"M description:\n";19941995foreachmy$revision(@$revisions)1996{1997print"M ----------------------------\n";1998print"M revision$revision->{revision}\n";1999# reformat the date for log output2000if($revision->{modified} =~/(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/and2001defined($DATE_LIST->{$2}) )2002{2003$revision->{modified} =sprintf('%04d/%02d/%02d%s',2004$3,$DATE_LIST->{$2},$1,$4);2005}2006$revision->{author} = cvs_author($revision->{author});2007print"M date:$revision->{modified};".2008" author:$revision->{author}; state: ".2009($revision->{filehash}eq"deleted"?"dead":"Exp") .2010"; lines: +2 -3\n";2011my$commitmessage;2012$commitmessage=$updater->commitmessage($revision->{commithash});2013$commitmessage=~s/^/M /mg;2014print$commitmessage."\n";2015}2016print"M =======". ("=" x 70) ."\n";2017}20182019print"ok\n";2020}20212022sub req_annotate2023{2024my($cmd,$data) =@_;20252026 argsplit("annotate");20272028$log->info("req_annotate : ". (defined($data) ?$data:"[NULL]"));2029#$log->debug("status state : " . Dumper($state));20302031# Grab a handle to the SQLite db and do any necessary updates2032my$updater= GITCVS::updater->new($state->{CVSROOT},$state->{module},$log);2033$updater->update();20342035# if no files were specified, we need to work out what files we should be providing annotate on ...2036 argsfromdir($updater);20372038# we'll need a temporary checkout dir2039 setupWorkTree();20402041$log->info("Temp checkoutdir creation successful, basing annotate session work on '$work->{workDir}', index file is '$ENV{GIT_INDEX_FILE}'");20422043# foreach file specified on the command line ...2044foreachmy$filename( @{$state->{args}} )2045{2046$filename= filecleanup($filename);20472048my$meta=$updater->getmeta($filename);20492050next unless($meta->{revision} );20512052# get all the commits that this file was in2053# in dense format -- aka skip dead revisions2054my$revisions=$updater->gethistorydense($filename);2055my$lastseenin=$revisions->[0][2];20562057# populate the temporary index based on the latest commit were we saw2058# the file -- but do it cheaply without checking out any files2059# TODO: if we got a revision from the client, use that instead2060# to look up the commithash in sqlite (still good to default to2061# the current head as we do now)2062system("git","read-tree",$lastseenin);2063unless($?==0)2064{2065print"E error running git-read-tree$lastseenin$ENV{GIT_INDEX_FILE}$!\n";2066return;2067}2068$log->info("Created index '$ENV{GIT_INDEX_FILE}' with commit$lastseenin- exit status$?");20692070# do a checkout of the file2071system('git','checkout-index','-f','-u',$filename);2072unless($?==0) {2073print"E error running git-checkout-index -f -u$filename:$!\n";2074return;2075}20762077$log->info("Annotate$filename");20782079# Prepare a file with the commits from the linearized2080# history that annotate should know about. This prevents2081# git-jsannotate telling us about commits we are hiding2082# from the client.20832084my$a_hints="$work->{workDir}/.annotate_hints";2085if(!open(ANNOTATEHINTS,'>',$a_hints)) {2086print"E failed to open '$a_hints' for writing:$!\n";2087return;2088}2089for(my$i=0;$i<@$revisions;$i++)2090{2091print ANNOTATEHINTS $revisions->[$i][2];2092if($i+1<@$revisions) {# have we got a parent?2093print ANNOTATEHINTS ' '.$revisions->[$i+1][2];2094}2095print ANNOTATEHINTS "\n";2096}20972098print ANNOTATEHINTS "\n";2099close ANNOTATEHINTS2100or(print"E failed to write$a_hints:$!\n"),return;21012102my@cmd= (qw(git annotate -l -S),$a_hints,$filename);2103if(!open(ANNOTATE,"-|",@cmd)) {2104print"E error invoking ".join(' ',@cmd) .":$!\n";2105return;2106}2107my$metadata= {};2108print"E Annotations for$filename\n";2109print"E ***************\n";2110while( <ANNOTATE> )2111{2112if(m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i)2113{2114my$commithash=$1;2115my$data=$2;2116unless(defined($metadata->{$commithash} ) )2117{2118$metadata->{$commithash} =$updater->getmeta($filename,$commithash);2119$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});2120$metadata->{$commithash}{modified} =sprintf("%02d-%s-%02d",$1,$2,$3)if($metadata->{$commithash}{modified} =~/^(\d+)\s(\w+)\s\d\d(\d\d)/);2121}2122printf("M %-7s (%-8s%10s):%s\n",2123$metadata->{$commithash}{revision},2124$metadata->{$commithash}{author},2125$metadata->{$commithash}{modified},2126$data2127);2128}else{2129$log->warn("Error in annotate output! LINE:$_");2130print"E Annotate error\n";2131next;2132}2133}2134close ANNOTATE;2135}21362137# done; get out of the tempdir2138 cleanupWorkTree();21392140print"ok\n";21412142}21432144# This method takes the state->{arguments} array and produces two new arrays.2145# The first is $state->{args} which is everything before the '--' argument, and2146# the second is $state->{files} which is everything after it.2147sub argsplit2148{2149$state->{args} = [];2150$state->{files} = [];2151$state->{opt} = {};21522153return unless(defined($state->{arguments})and ref$state->{arguments}eq"ARRAY");21542155my$type=shift;21562157if(defined($type) )2158{2159my$opt= {};2160$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");2161$opt= { v =>0, l =>0, R =>0}if($typeeq"status");2162$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");2163$opt= { l =>0, R =>0, k =>1, D =>1, D =>1, r =>2}if($typeeq"diff");2164$opt= { c =>0, R =>0, l =>0, f =>0, F =>1, m =>1, r =>1}if($typeeq"ci");2165$opt= { k =>1, m =>1}if($typeeq"add");2166$opt= { f =>0, l =>0, R =>0}if($typeeq"remove");2167$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");216821692170while(scalar( @{$state->{arguments}} ) >0)2171{2172my$arg=shift@{$state->{arguments}};21732174next if($argeq"--");2175next unless($arg=~/\S/);21762177# if the argument looks like a switch2178if($arg=~/^-(\w)(.*)/)2179{2180# if it's a switch that takes an argument2181if($opt->{$1} )2182{2183# If this switch has already been provided2184if($opt->{$1} >1and exists($state->{opt}{$1} ) )2185{2186$state->{opt}{$1} = [$state->{opt}{$1} ];2187if(length($2) >0)2188{2189push@{$state->{opt}{$1}},$2;2190}else{2191push@{$state->{opt}{$1}},shift@{$state->{arguments}};2192}2193}else{2194# if there's extra data in the arg, use that as the argument for the switch2195if(length($2) >0)2196{2197$state->{opt}{$1} =$2;2198}else{2199$state->{opt}{$1} =shift@{$state->{arguments}};2200}2201}2202}else{2203$state->{opt}{$1} =undef;2204}2205}2206else2207{2208push@{$state->{args}},$arg;2209}2210}2211}2212else2213{2214my$mode=0;22152216foreachmy$value( @{$state->{arguments}} )2217{2218if($valueeq"--")2219{2220$mode++;2221next;2222}2223push@{$state->{args}},$valueif($mode==0);2224push@{$state->{files}},$valueif($mode==1);2225}2226}2227}22282229# This method uses $state->{directory} to populate $state->{args} with a list of filenames2230sub argsfromdir2231{2232my$updater=shift;22332234$state->{args} = []if(scalar(@{$state->{args}}) ==1and$state->{args}[0]eq".");22352236return if(scalar( @{$state->{args}} ) >1);22372238my@gethead= @{$updater->gethead};22392240# push added files2241foreachmy$file(keys%{$state->{entries}}) {2242if(exists$state->{entries}{$file}{revision} &&2243$state->{entries}{$file}{revision}eq'0')2244{2245push@gethead, { name =>$file, filehash =>'added'};2246}2247}22482249if(scalar(@{$state->{args}}) ==1)2250{2251my$arg=$state->{args}[0];2252$arg.=$state->{prependdir}if(defined($state->{prependdir} ) );22532254$log->info("Only one arg specified, checking for directory expansion on '$arg'");22552256foreachmy$file(@gethead)2257{2258next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2259next unless($file->{name} =~/^$arg\//or$file->{name}eq$arg);2260push@{$state->{args}},$file->{name};2261}22622263shift@{$state->{args}}if(scalar(@{$state->{args}}) >1);2264}else{2265$log->info("Only one arg specified, populating file list automatically");22662267$state->{args} = [];22682269foreachmy$file(@gethead)2270{2271next if($file->{filehash}eq"deleted"and not defined($state->{entries}{$file->{name}} ) );2272next unless($file->{name} =~s/^$state->{prependdir}//);2273push@{$state->{args}},$file->{name};2274}2275}2276}227722782279## look up directory sticky tag, of either fullPath or a parent:2280sub getDirStickyInfo2281{2282my($fullPath)=@_;22832284$fullPath=~s%/+$%%;2285while($fullPathne""&& !defined($state->{dirMap}{"$fullPath/"}))2286{2287$fullPath=~s%/?[^/]*$%%;2288}22892290if( !defined($state->{dirMap}{"$fullPath/"}) &&2291($fullPatheq""||2292$fullPatheq".") )2293{2294return$state->{dirMap}{""}{stickyInfo};2295}2296else2297{2298return$state->{dirMap}{"$fullPath/"}{stickyInfo};2299}2300}23012302# Resolve precedence of various ways of specifying which version of2303# a file you want. Returns undef (for default head), or a ref to a hash2304# that contains "tag" and/or "date" keys.2305sub resolveStickyInfo2306{2307my($filename,$stickyTag,$stickyDate,$reset) =@_;23082309# Order of precedence of sticky tags:2310# -A [head]2311# -r /tag/2312# [file entry sticky tag]2313# [the tag specified in dir req_Sticky]2314# [the tag specified in a parent dir req_Sticky]2315# [head]23162317my$result;2318if($reset)2319{2320# $result=undef;2321}2322elsif(defined($stickyTag) &&$stickyTagne"")2323# || ( defined($stickyDate) && $stickyDate ne "" ) # TODO2324{2325$result={'tag'=> (defined($stickyTag)?$stickyTag:undef) };23262327# TODO: Convert -D value into the form 2011.04.10.04.46.57,2328# similar to an entry line's sticky date, without the D prefix.2329# It sometimes (always?) arrives as something more like2330# '10 Apr 2011 04:46:57 -0000'...2331# $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };2332}2333elsif(defined($state->{entries}{$filename}) &&2334defined($state->{entries}{$filename}{tag_or_date}) &&2335$state->{entries}{$filename}{tag_or_date}ne"")2336{2337my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};2338if($tagOrDate=~/^T([^ ]+)\s*$/)2339{2340$result= {'tag'=>$1};2341}2342elsif($tagOrDate=~/^D([0-9.]+)\s*$/)2343{2344$result= {'date'=>$1};2345}2346else2347{2348die"Unknown tag_or_date format\n";2349}2350}2351else2352{2353$result=getDirStickyInfo($filename);2354}23552356return$result;2357}23582359# Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into2360# a form appropriate for the sticky tag field of an Entries2361# line (field index 5, 0-based).2362sub getStickyTagOrDate2363{2364my($stickyInfo)=@_;23652366my$result;2367if(defined($stickyInfo) &&defined($stickyInfo->{tag}))2368{2369$result="T$stickyInfo->{tag}";2370}2371# TODO: When/if we actually pick versions by {date} properly,2372# also handle it here:2373# "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").2374else2375{2376$result="";2377}23782379return$result;2380}23812382# This method cleans up the $state variable after a command that uses arguments has run2383sub statecleanup2384{2385$state->{files} = [];2386$state->{args} = [];2387$state->{arguments} = [];2388$state->{entries} = {};2389$state->{dirMap} = {};2390}23912392# Return working directory CVS revision "1.X" out2393# of the the working directory "entries" state, for the given filename.2394# This is prefixed with a dash if the file is scheduled for removal2395# when it is committed.2396sub revparse2397{2398my$filename=shift;23992400return$state->{entries}{$filename}{revision};2401}24022403# This method takes a file hash and does a CVS "file transfer". Its2404# exact behaviour depends on a second, optional hash table argument:2405# - If $options->{targetfile}, dump the contents to that file;2406# - If $options->{print}, use M/MT to transmit the contents one line2407# at a time;2408# - Otherwise, transmit the size of the file, followed by the file2409# contents.2410sub transmitfile2411{2412my$filehash=shift;2413my$options=shift;24142415if(defined($filehash)and$filehasheq"deleted")2416{2417$log->warn("filehash is 'deleted'");2418return;2419}24202421die"Need filehash"unless(defined($filehash)and$filehash=~/^[a-zA-Z0-9]{40}$/);24222423my$type=`git cat-file -t$filehash`;2424 chomp$type;24252426 die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ($type) and$typeeq "blob" );24272428 my$size= `git cat-file -s $filehash`;2429chomp$size;24302431$log->debug("transmitfile($filehash) size=$size, type=$type");24322433if(open my$fh,'-|',"git","cat-file","blob",$filehash)2434{2435if(defined($options->{targetfile} ) )2436{2437my$targetfile=$options->{targetfile};2438open NEWFILE,">",$targetfileor die("Couldn't open '$targetfile' for writing :$!");2439print NEWFILE $_while( <$fh> );2440close NEWFILE or die("Failed to write '$targetfile':$!");2441}elsif(defined($options->{print} ) &&$options->{print} ) {2442while( <$fh> ) {2443if(/\n\z/) {2444print'M ',$_;2445}else{2446print'MT text ',$_,"\n";2447}2448}2449}else{2450print"$size\n";2451printwhile( <$fh> );2452}2453close$fhor die("Couldn't close filehandle for transmitfile():$!");2454}else{2455die("Couldn't execute git-cat-file");2456}2457}24582459# This method takes a file name, and returns ( $dirpart, $filepart ) which2460# refers to the directory portion and the file portion of the filename2461# respectively2462sub filenamesplit2463{2464my$filename=shift;2465my$fixforlocaldir=shift;24662467my($filepart,$dirpart) = ($filename,".");2468($filepart,$dirpart) = ($2,$1)if($filename=~/(.*)\/(.*)/ );2469$dirpart.="/";24702471if($fixforlocaldir)2472{2473$dirpart=~s/^$state->{prependdir}//;2474}24752476return($filepart,$dirpart);2477}24782479# Cleanup various junk in filename (try to canonicalize it), and2480# add prependdir to accomodate running CVS client from a2481# subdirectory (so the output is relative to top directory of the project).2482sub filecleanup2483{2484my$filename=shift;24852486returnundefunless(defined($filename));2487if($filename=~/^\// )2488{2489print"E absolute filenames '$filename' not supported by server\n";2490returnundef;2491}24922493if($filenameeq".")2494{2495$filename="";2496}2497$filename=~s/^\.\///g;2498$filename=~ s%/+%/%g;2499$filename=$state->{prependdir} .$filename;2500$filename=~ s%/$%%;2501return$filename;2502}25032504# Remove prependdir from the path, so that is is relative to the directory2505# the CVS client was started from, rather than the top of the project.2506# Essentially the inverse of filecleanup().2507sub remove_prependdir2508{2509my($path) =@_;2510if(defined($state->{prependdir}) &&$state->{prependdir}ne"")2511{2512my($pre)=$state->{prependdir};2513$pre=~s%/$%%;2514if(!($path=~s%^\Q$pre\E/?%%))2515{2516$log->fatal("internal error missing prependdir");2517die("internal error missing prependdir");2518}2519}2520return$path;2521}25222523sub validateGitDir2524{2525if( !defined($state->{CVSROOT}) )2526{2527print"error 1 CVSROOT not specified\n";2528 cleanupWorkTree();2529exit;2530}2531if($ENV{GIT_DIR}ne($state->{CVSROOT} .'/') )2532{2533print"error 1 Internally inconsistent CVSROOT\n";2534 cleanupWorkTree();2535exit;2536}2537}25382539# Setup working directory in a work tree with the requested version2540# loaded in the index.2541sub setupWorkTree2542{2543my($ver) =@_;25442545 validateGitDir();25462547if( (defined($work->{state}) &&$work->{state} !=1) ||2548defined($work->{tmpDir}) )2549{2550$log->warn("Bad work tree state management");2551print"error 1 Internal setup multiple work trees without cleanup\n";2552 cleanupWorkTree();2553exit;2554}25552556$work->{workDir} = tempdir ( DIR =>$TEMP_DIR);25572558if( !defined($work->{index}) )2559{2560(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2561}25622563chdir$work->{workDir}or2564die"Unable to chdir to$work->{workDir}\n";25652566$log->info("Setting up GIT_WORK_TREE as '.' in '$work->{workDir}', index file is '$work->{index}'");25672568$ENV{GIT_WORK_TREE} =".";2569$ENV{GIT_INDEX_FILE} =$work->{index};2570$work->{state} =2;25712572if($ver)2573{2574system("git","read-tree",$ver);2575unless($?==0)2576{2577$log->warn("Error running git-read-tree");2578die"Error running git-read-tree$verin$work->{workDir}$!\n";2579}2580}2581# else # req_annotate reads tree for each file2582}25832584# Ensure current directory is in some kind of working directory,2585# with a recent version loaded in the index.2586sub ensureWorkTree2587{2588if(defined($work->{tmpDir}) )2589{2590$log->warn("Bad work tree state management [ensureWorkTree()]");2591print"error 1 Internal setup multiple dirs without cleanup\n";2592 cleanupWorkTree();2593exit;2594}2595if($work->{state} )2596{2597return;2598}25992600 validateGitDir();26012602if( !defined($work->{emptyDir}) )2603{2604$work->{emptyDir} = tempdir ( DIR =>$TEMP_DIR, OPEN =>0);2605}2606chdir$work->{emptyDir}or2607die"Unable to chdir to$work->{emptyDir}\n";26082609my$ver=`git show-ref -s refs/heads/$state->{module}`;2610chomp$ver;2611if($ver!~/^[0-9a-f]{40}$/)2612{2613$log->warn("Error from git show-ref -s refs/head$state->{module}");2614print"error 1 cannot find the current HEAD of module";2615 cleanupWorkTree();2616exit;2617}26182619if( !defined($work->{index}) )2620{2621(undef,$work->{index}) = tempfile ( DIR =>$TEMP_DIR, OPEN =>0);2622}26232624$ENV{GIT_WORK_TREE} =".";2625$ENV{GIT_INDEX_FILE} =$work->{index};2626$work->{state} =1;26272628system("git","read-tree",$ver);2629unless($?==0)2630{2631die"Error running git-read-tree$ver$!\n";2632}2633}26342635# Cleanup working directory that is not needed any longer.2636sub cleanupWorkTree2637{2638if( !$work->{state} )2639{2640return;2641}26422643chdir"/"or die"Unable to chdir '/'\n";26442645if(defined($work->{workDir}) )2646{2647 rmtree($work->{workDir} );2648undef$work->{workDir};2649}2650undef$work->{state};2651}26522653# Setup a temporary directory (not a working tree), typically for2654# merging dirty state as in req_update.2655sub setupTmpDir2656{2657$work->{tmpDir} = tempdir ( DIR =>$TEMP_DIR);2658chdir$work->{tmpDir}or die"Unable to chdir$work->{tmpDir}\n";26592660return$work->{tmpDir};2661}26622663# Clean up a previously setupTmpDir. Restore previous work tree if2664# appropriate.2665sub cleanupTmpDir2666{2667if( !defined($work->{tmpDir}) )2668{2669$log->warn("cleanup tmpdir that has not been setup");2670die"Cleanup tmpDir that has not been setup\n";2671}2672if(defined($work->{state}) )2673{2674if($work->{state} ==1)2675{2676chdir$work->{emptyDir}or2677die"Unable to chdir to$work->{emptyDir}\n";2678}2679elsif($work->{state} ==2)2680{2681chdir$work->{workDir}or2682die"Unable to chdir to$work->{emptyDir}\n";2683}2684else2685{2686$log->warn("Inconsistent work dir state");2687die"Inconsistent work dir state\n";2688}2689}2690else2691{2692chdir"/"or die"Unable to chdir '/'\n";2693}2694}26952696# Given a path, this function returns a string containing the kopts2697# that should go into that path's Entries line. For example, a binary2698# file should get -kb.2699sub kopts_from_path2700{2701my($path,$srcType,$name) =@_;27022703if(defined($cfg->{gitcvs}{usecrlfattr} )and2704$cfg->{gitcvs}{usecrlfattr} =~/\s*(1|true|yes)\s*$/i)2705{2706my($val) = check_attr("text",$path);2707if($valeq"unspecified")2708{2709$val= check_attr("crlf",$path);2710}2711if($valeq"unset")2712{2713return"-kb"2714}2715elsif( check_attr("eol",$path)ne"unspecified"||2716$valeq"set"||$valeq"input")2717{2718return"";2719}2720else2721{2722$log->info("Unrecognized check_attr crlf$path:$val");2723}2724}27252726if(defined($cfg->{gitcvs}{allbinary} ) )2727{2728if( ($cfg->{gitcvs}{allbinary} =~/^\s*(1|true|yes)\s*$/i) )2729{2730return"-kb";2731}2732elsif( ($cfg->{gitcvs}{allbinary} =~/^\s*guess\s*$/i) )2733{2734if( is_binary($srcType,$name) )2735{2736$log->debug("... as binary");2737return"-kb";2738}2739else2740{2741$log->debug("... as text");2742}2743}2744}2745# Return "" to give no special treatment to any path2746return"";2747}27482749sub check_attr2750{2751my($attr,$path) =@_;2752 ensureWorkTree();2753if(open my$fh,'-|',"git","check-attr",$attr,"--",$path)2754{2755my$val= <$fh>;2756close$fh;2757$val=~s/.*: ([^:\r\n]*)\s*$/$1/;2758return$val;2759}2760else2761{2762returnundef;2763}2764}27652766# This should have the same heuristics as convert.c:is_binary() and related.2767# Note that the bare CR test is done by callers in convert.c.2768sub is_binary2769{2770my($srcType,$name) =@_;2771$log->debug("is_binary($srcType,$name)");27722773# Minimize amount of interpreted code run in the inner per-character2774# loop for large files, by totalling each character value and2775# then analyzing the totals.2776my@counts;2777my$i;2778for($i=0;$i<256;$i++)2779{2780$counts[$i]=0;2781}27822783my$fh= open_blob_or_die($srcType,$name);2784my$line;2785while(defined($line=<$fh>) )2786{2787# Any '\0' and bare CR are considered binary.2788if($line=~/\0|(\r[^\n])/)2789{2790close($fh);2791return1;2792}27932794# Count up each character in the line:2795my$len=length($line);2796for($i=0;$i<$len;$i++)2797{2798$counts[ord(substr($line,$i,1))]++;2799}2800}2801close$fh;28022803# Don't count CR and LF as either printable/nonprintable2804$counts[ord("\n")]=0;2805$counts[ord("\r")]=0;28062807# Categorize individual character count into printable and nonprintable:2808my$printable=0;2809my$nonprintable=0;2810for($i=0;$i<256;$i++)2811{2812if($i<32&&2813$i!=ord("\b") &&2814$i!=ord("\t") &&2815$i!=033&&# ESC2816$i!=014)# FF2817{2818$nonprintable+=$counts[$i];2819}2820elsif($i==127)# DEL2821{2822$nonprintable+=$counts[$i];2823}2824else2825{2826$printable+=$counts[$i];2827}2828}28292830return($printable>>7) <$nonprintable;2831}28322833# Returns open file handle. Possible invocations:2834# - open_blob_or_die("file",$filename);2835# - open_blob_or_die("sha1",$filehash);2836sub open_blob_or_die2837{2838my($srcType,$name) =@_;2839my($fh);2840if($srcTypeeq"file")2841{2842if( !open$fh,"<",$name)2843{2844$log->warn("Unable to open file$name:$!");2845die"Unable to open file$name:$!\n";2846}2847}2848elsif($srcTypeeq"sha1")2849{2850unless(defined($name)and$name=~/^[a-zA-Z0-9]{40}$/)2851{2852$log->warn("Need filehash");2853die"Need filehash\n";2854}28552856my$type=`git cat-file -t$name`;2857 chomp$type;28582859 unless ( defined ($type) and$typeeq "blob" )2860 {2861$log->warn("Invalid type '$type' for '$name'");2862 die ( "Invalid type '$type' (expected 'blob')" )2863 }28642865 my$size= `git cat-file -s $name`;2866chomp$size;28672868$log->debug("open_blob_or_die($name) size=$size, type=$type");28692870unless(open$fh,'-|',"git","cat-file","blob",$name)2871{2872$log->warn("Unable to open sha1$name");2873die"Unable to open sha1$name\n";2874}2875}2876else2877{2878$log->warn("Unknown type of blob source:$srcType");2879die"Unknown type of blob source:$srcType\n";2880}2881return$fh;2882}28832884# Generate a CVS author name from Git author information, by taking the local2885# part of the email address and replacing characters not in the Portable2886# Filename Character Set (see IEEE Std 1003.1-2001, 3.276) by underscores. CVS2887# Login names are Unix login names, which should be restricted to this2888# character set.2889sub cvs_author2890{2891my$author_line=shift;2892(my$author) =$author_line=~/<([^@>]*)/;28932894$author=~s/[^-a-zA-Z0-9_.]/_/g;2895$author=~s/^-/_/;28962897$author;2898}289929002901sub descramble2902{2903# This table is from src/scramble.c in the CVS source2904my@SHIFTS= (29050,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,290616,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,2907114,120,53,79,96,109,72,108,70,64,76,67,116,74,68,87,2908111,52,75,119,49,34,82,81,95,65,112,86,118,110,122,105,290941,57,83,43,46,102,40,89,38,103,45,50,42,123,91,35,2910125,55,54,66,124,126,59,47,92,71,115,78,88,107,106,56,291136,121,117,104,101,100,69,73,99,63,94,93,39,37,61,48,291258,113,32,90,44,98,60,51,33,97,62,77,84,80,85,223,2913225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190,2914199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193,2915174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212,2916207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246,2917192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176,2918227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127,2919182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195,2920243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,1522921);2922my($str) =@_;29232924# This should never happen, the same password format (A) has been2925# used by CVS since the beginning of time2926{2927my$fmt=substr($str,0,1);2928die"invalid password format `$fmt'"unless$fmteq'A';2929}29302931my@str=unpack"C*",substr($str,1);2932my$ret=join'',map{chr$SHIFTS[$_] }@str;2933return$ret;2934}293529362937package GITCVS::log;29382939####2940#### Copyright The Open University UK - 2006.2941####2942#### Authors: Martyn Smith <martyn@catalyst.net.nz>2943#### Martin Langhoff <martin@laptop.org>2944####2945####29462947use strict;2948use warnings;29492950=head1 NAME29512952GITCVS::log29532954=head1 DESCRIPTION29552956This module provides very crude logging with a similar interface to2957Log::Log4perl29582959=head1 METHODS29602961=cut29622963=head2 new29642965Creates a new log object, optionally you can specify a filename here to2966indicate the file to log to. If no log file is specified, you can specify one2967later with method setfile, or indicate you no longer want logging with method2968nofile.29692970Until one of these methods is called, all log calls will buffer messages ready2971to write out.29722973=cut2974sub new2975{2976my$class=shift;2977my$filename=shift;29782979my$self= {};29802981bless$self,$class;29822983if(defined($filename) )2984{2985open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");2986}29872988return$self;2989}29902991=head2 setfile29922993This methods takes a filename, and attempts to open that file as the log file.2994If successful, all buffered data is written out to the file, and any further2995logging is written directly to the file.29962997=cut2998sub setfile2999{3000my$self=shift;3001my$filename=shift;30023003if(defined($filename) )3004{3005open$self->{fh},">>",$filenameor die("Couldn't open '$filename' for writing :$!");3006}30073008return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");30093010while(my$line=shift@{$self->{buffer}} )3011{3012print{$self->{fh}}$line;3013}3014}30153016=head2 nofile30173018This method indicates no logging is going to be used. It flushes any entries in3019the internal buffer, and sets a flag to ensure no further data is put there.30203021=cut3022sub nofile3023{3024my$self=shift;30253026$self->{nolog} =1;30273028return unless(defined($self->{buffer} )and ref$self->{buffer}eq"ARRAY");30293030$self->{buffer} = [];3031}30323033=head2 _logopen30343035Internal method. Returns true if the log file is open, false otherwise.30363037=cut3038sub _logopen3039{3040my$self=shift;30413042return1if(defined($self->{fh} )and ref$self->{fh}eq"GLOB");3043return0;3044}30453046=head2 debug info warn fatal30473048These four methods are wrappers to _log. They provide the actual interface for3049logging data.30503051=cut3052sub debug {my$self=shift;$self->_log("debug",@_); }3053sub info {my$self=shift;$self->_log("info",@_); }3054subwarn{my$self=shift;$self->_log("warn",@_); }3055sub fatal {my$self=shift;$self->_log("fatal",@_); }30563057=head2 _log30583059This is an internal method called by the logging functions. It generates a3060timestamp and pushes the logged line either to file, or internal buffer.30613062=cut3063sub _log3064{3065my$self=shift;3066my$level=shift;30673068return if($self->{nolog} );30693070my@time=localtime;3071my$timestring=sprintf("%4d-%02d-%02d%02d:%02d:%02d: %-5s",3072$time[5] +1900,3073$time[4] +1,3074$time[3],3075$time[2],3076$time[1],3077$time[0],3078uc$level,3079);30803081if($self->_logopen)3082{3083print{$self->{fh}}$timestring." - ".join(" ",@_) ."\n";3084}else{3085push@{$self->{buffer}},$timestring." - ".join(" ",@_) ."\n";3086}3087}30883089=head2 DESTROY30903091This method simply closes the file handle if one is open30923093=cut3094sub DESTROY3095{3096my$self=shift;30973098if($self->_logopen)3099{3100close$self->{fh};3101}3102}31033104package GITCVS::updater;31053106####3107#### Copyright The Open University UK - 2006.3108####3109#### Authors: Martyn Smith <martyn@catalyst.net.nz>3110#### Martin Langhoff <martin@laptop.org>3111####3112####31133114use strict;3115use warnings;3116use DBI;31173118=head1 METHODS31193120=cut31213122=head2 new31233124=cut3125sub new3126{3127my$class=shift;3128my$config=shift;3129my$module=shift;3130my$log=shift;31313132die"Need to specify a git repository"unless(defined($config)and-d $config);3133die"Need to specify a module"unless(defined($module) );31343135$class=ref($class) ||$class;31363137my$self= {};31383139bless$self,$class;31403141$self->{valid_tables} = {'revision'=>1,3142'revision_ix1'=>1,3143'revision_ix2'=>1,3144'head'=>1,3145'head_ix1'=>1,3146'properties'=>1,3147'commitmsgs'=>1};31483149$self->{module} =$module;3150$self->{git_path} =$config."/";31513152$self->{log} =$log;31533154die"Git repo '$self->{git_path}' doesn't exist"unless( -d $self->{git_path} );31553156# Stores full sha1's for various branch/tag names, abbreviations, etc:3157$self->{commitRefCache} = {};31583159$self->{dbdriver} =$cfg->{gitcvs}{$state->{method}}{dbdriver} ||3160$cfg->{gitcvs}{dbdriver} ||"SQLite";3161$self->{dbname} =$cfg->{gitcvs}{$state->{method}}{dbname} ||3162$cfg->{gitcvs}{dbname} ||"%Ggitcvs.%m.sqlite";3163$self->{dbuser} =$cfg->{gitcvs}{$state->{method}}{dbuser} ||3164$cfg->{gitcvs}{dbuser} ||"";3165$self->{dbpass} =$cfg->{gitcvs}{$state->{method}}{dbpass} ||3166$cfg->{gitcvs}{dbpass} ||"";3167$self->{dbtablenameprefix} =$cfg->{gitcvs}{$state->{method}}{dbtablenameprefix} ||3168$cfg->{gitcvs}{dbtablenameprefix} ||"";3169my%mapping= ( m =>$module,3170 a =>$state->{method},3171 u =>getlogin||getpwuid($<) || $<,3172 G =>$self->{git_path},3173 g => mangle_dirname($self->{git_path}),3174);3175$self->{dbname} =~s/%([mauGg])/$mapping{$1}/eg;3176$self->{dbuser} =~s/%([mauGg])/$mapping{$1}/eg;3177$self->{dbtablenameprefix} =~s/%([mauGg])/$mapping{$1}/eg;3178$self->{dbtablenameprefix} = mangle_tablename($self->{dbtablenameprefix});31793180die"Invalid char ':' in dbdriver"if$self->{dbdriver} =~/:/;3181die"Invalid char ';' in dbname"if$self->{dbname} =~/;/;3182$self->{dbh} = DBI->connect("dbi:$self->{dbdriver}:dbname=$self->{dbname}",3183$self->{dbuser},3184$self->{dbpass});3185die"Error connecting to database\n"unlessdefined$self->{dbh};31863187$self->{tables} = {};3188foreachmy$table(keys%{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} )3189{3190$self->{tables}{$table} =1;3191}31923193# Construct the revision table if required3194# The revision table stores an entry for each file, each time that file3195# changes.3196# numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )3197# This is not sufficient to support "-r {commithash}" for any3198# files except files that were modified by that commit (also,3199# some places in the code ignore/effectively strip out -r in3200# some cases, before it gets passed to getmeta()).3201# The "filehash" field typically has a git blob hash, but can also3202# be set to "dead" to indicate that the given version of the file3203# should not exist in the sandbox.3204unless($self->{tables}{$self->tablename("revision")} )3205{3206my$tablename=$self->tablename("revision");3207my$ix1name=$self->tablename("revision_ix1");3208my$ix2name=$self->tablename("revision_ix2");3209$self->{dbh}->do("3210 CREATE TABLE$tablename(3211 name TEXT NOT NULL,3212 revision INTEGER NOT NULL,3213 filehash TEXT NOT NULL,3214 commithash TEXT NOT NULL,3215 author TEXT NOT NULL,3216 modified TEXT NOT NULL,3217 mode TEXT NOT NULL3218 )3219 ");3220$self->{dbh}->do("3221 CREATE INDEX$ix1name3222 ON$tablename(name,revision)3223 ");3224$self->{dbh}->do("3225 CREATE INDEX$ix2name3226 ON$tablename(name,commithash)3227 ");3228}32293230# Construct the head table if required3231# The head table (along with the "last_commit" entry in the property3232# table) is the persisted working state of the "sub update" subroutine.3233# All of it's data is read entirely first, and completely recreated3234# last, every time "sub update" runs.3235# This is also used by "sub getmeta" when it is asked for the latest3236# version of a file (as opposed to some specific version).3237# Another way of thinking about it is as a single slice out of3238# "revisions", giving just the most recent revision information for3239# each file.3240unless($self->{tables}{$self->tablename("head")} )3241{3242my$tablename=$self->tablename("head");3243my$ix1name=$self->tablename("head_ix1");3244$self->{dbh}->do("3245 CREATE TABLE$tablename(3246 name TEXT NOT NULL,3247 revision INTEGER NOT NULL,3248 filehash TEXT NOT NULL,3249 commithash TEXT NOT NULL,3250 author TEXT NOT NULL,3251 modified TEXT NOT NULL,3252 mode TEXT NOT NULL3253 )3254 ");3255$self->{dbh}->do("3256 CREATE INDEX$ix1name3257 ON$tablename(name)3258 ");3259}32603261# Construct the properties table if required3262# - "last_commit" - Used by "sub update".3263unless($self->{tables}{$self->tablename("properties")} )3264{3265my$tablename=$self->tablename("properties");3266$self->{dbh}->do("3267 CREATE TABLE$tablename(3268 key TEXT NOT NULL PRIMARY KEY,3269 value TEXT3270 )3271 ");3272}32733274# Construct the commitmsgs table if required3275# The commitmsgs table is only used for merge commits, since3276# "sub update" will only keep one branch of parents. Shortlogs3277# for ignored commits (i.e. not on the chosen branch) will be used3278# to construct a replacement "collapsed" merge commit message,3279# which will be stored in this table. See also "sub commitmessage".3280unless($self->{tables}{$self->tablename("commitmsgs")} )3281{3282my$tablename=$self->tablename("commitmsgs");3283$self->{dbh}->do("3284 CREATE TABLE$tablename(3285 key TEXT NOT NULL PRIMARY KEY,3286 value TEXT3287 )3288 ");3289}32903291return$self;3292}32933294=head2 tablename32953296=cut3297sub tablename3298{3299my$self=shift;3300my$name=shift;33013302if(exists$self->{valid_tables}{$name}) {3303return$self->{dbtablenameprefix} .$name;3304}else{3305returnundef;3306}3307}33083309=head2 update33103311Bring the database up to date with the latest changes from3312the git repository.33133314Internal working state is read out of the "head" table and the3315"last_commit" property, then it updates "revisions" based on that, and3316finally it writes the new internal state back to the "head" table3317so it can be used as a starting point the next time update is called.33183319=cut3320sub update3321{3322my$self=shift;33233324# first lets get the commit list3325$ENV{GIT_DIR} =$self->{git_path};33263327my$commitsha1=`git rev-parse$self->{module}`;3328chomp$commitsha1;33293330my$commitinfo=`git cat-file commit$self->{module} 2>&1`;3331unless($commitinfo=~/tree\s+[a-zA-Z0-9]{40}/)3332{3333die("Invalid module '$self->{module}'");3334}333533363337my$git_log;3338my$lastcommit=$self->_get_prop("last_commit");33393340if(defined$lastcommit&&$lastcommiteq$commitsha1) {# up-to-date3341return1;3342}33433344# Start exclusive lock here...3345$self->{dbh}->begin_work()or die"Cannot lock database for BEGIN";33463347# TODO: log processing is memory bound3348# if we can parse into a 2nd file that is in reverse order3349# we can probably do something really efficient3350my@git_log_params= ('--pretty','--parents','--topo-order');33513352if(defined$lastcommit) {3353push@git_log_params,"$lastcommit..$self->{module}";3354}else{3355push@git_log_params,$self->{module};3356}3357# git-rev-list is the backend / plumbing version of git-log3358open(my$gitLogPipe,'-|','git','rev-list',@git_log_params)3359or die"Cannot call git-rev-list:$!";3360my@commits=readCommits($gitLogPipe);3361close$gitLogPipe;33623363# Now all the commits are in the @commits bucket3364# ordered by time DESC. for each commit that needs processing,3365# determine whether it's following the last head we've seen or if3366# it's on its own branch, grab a file list, and add whatever's changed3367# NOTE: $lastcommit refers to the last commit from previous run3368# $lastpicked is the last commit we picked in this run3369my$lastpicked;3370my$head= {};3371if(defined$lastcommit) {3372$lastpicked=$lastcommit;3373}33743375my$committotal=scalar(@commits);3376my$commitcount=0;33773378# Load the head table into $head (for cached lookups during the update process)3379foreachmy$file( @{$self->gethead(1)} )3380{3381$head->{$file->{name}} =$file;3382}33833384foreachmy$commit(@commits)3385{3386$self->{log}->debug("GITCVS::updater - Processing commit$commit->{hash} (". (++$commitcount) ." of$committotal)");3387if(defined$lastpicked)3388{3389if(!in_array($lastpicked, @{$commit->{parents}}))3390{3391# skip, we'll see this delta3392# as part of a merge later3393# warn "skipping off-track $commit->{hash}\n";3394next;3395}elsif(@{$commit->{parents}} >1) {3396# it is a merge commit, for each parent that is3397# not $lastpicked (not given a CVS revision number),3398# see if we can get a log3399# from the merge-base to that parent to put it3400# in the message as a merge summary.3401my@parents= @{$commit->{parents}};3402foreachmy$parent(@parents) {3403if($parenteq$lastpicked) {3404next;3405}3406# git-merge-base can potentially (but rarely) throw3407# several candidate merge bases. let's assume3408# that the first one is the best one.3409my$base=eval{3410 safe_pipe_capture('git','merge-base',3411$lastpicked,$parent);3412};3413# The two branches may not be related at all,3414# in which case merge base simply fails to find3415# any, but that's Ok.3416next if($@);34173418chomp$base;3419if($base) {3420my@merged;3421# print "want to log between $base $parent \n";3422open(GITLOG,'-|','git','log','--pretty=medium',"$base..$parent")3423or die"Cannot call git-log:$!";3424my$mergedhash;3425while(<GITLOG>) {3426chomp;3427if(!defined$mergedhash) {3428if(m/^commit\s+(.+)$/) {3429$mergedhash=$1;3430}else{3431next;3432}3433}else{3434# grab the first line that looks non-rfc8223435# aka has content after leading space3436if(m/^\s+(\S.*)$/) {3437my$title=$1;3438$title=substr($title,0,100);# truncate3439unshift@merged,"$mergedhash$title";3440undef$mergedhash;3441}3442}3443}3444close GITLOG;3445if(@merged) {3446$commit->{mergemsg} =$commit->{message};3447$commit->{mergemsg} .="\nSummary of merged commits:\n\n";3448foreachmy$summary(@merged) {3449$commit->{mergemsg} .="\t$summary\n";3450}3451$commit->{mergemsg} .="\n\n";3452# print "Message for $commit->{hash} \n$commit->{mergemsg}";3453}3454}3455}3456}3457}34583459# convert the date to CVS-happy format3460my$cvsDate= convertToCvsDate($commit->{date});34613462if(defined($lastpicked) )3463{3464my$filepipe=open(FILELIST,'-|','git','diff-tree','-z','-r',$lastpicked,$commit->{hash})or die("Cannot call git-diff-tree :$!");3465local($/) ="\0";3466while( <FILELIST> )3467{3468chomp;3469unless(/^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o)3470{3471die("Couldn't process git-diff-tree line :$_");3472}3473my($mode,$hash,$change) = ($1,$2,$3);3474my$name= <FILELIST>;3475chomp($name);34763477# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");34783479my$dbMode= convertToDbMode($mode);34803481if($changeeq"D")3482{3483#$log->debug("DELETE $name");3484$head->{$name} = {3485 name =>$name,3486 revision =>$head->{$name}{revision} +1,3487 filehash =>"deleted",3488 commithash =>$commit->{hash},3489 modified =>$cvsDate,3490 author =>$commit->{author},3491 mode =>$dbMode,3492};3493$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3494}3495elsif($changeeq"M"||$changeeq"T")3496{3497#$log->debug("MODIFIED $name");3498$head->{$name} = {3499 name =>$name,3500 revision =>$head->{$name}{revision} +1,3501 filehash =>$hash,3502 commithash =>$commit->{hash},3503 modified =>$cvsDate,3504 author =>$commit->{author},3505 mode =>$dbMode,3506};3507$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3508}3509elsif($changeeq"A")3510{3511#$log->debug("ADDED $name");3512$head->{$name} = {3513 name =>$name,3514 revision =>$head->{$name}{revision} ?$head->{$name}{revision}+1:1,3515 filehash =>$hash,3516 commithash =>$commit->{hash},3517 modified =>$cvsDate,3518 author =>$commit->{author},3519 mode =>$dbMode,3520};3521$self->insert_rev($name,$head->{$name}{revision},$hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3522}3523else3524{3525$log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");3526die;3527}3528}3529close FILELIST;3530}else{3531# this is used to detect files removed from the repo3532my$seen_files= {};35333534my$filepipe=open(FILELIST,'-|','git','ls-tree','-z','-r',$commit->{hash})or die("Cannot call git-ls-tree :$!");3535local$/="\0";3536while( <FILELIST> )3537{3538chomp;3539unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3540{3541die("Couldn't process git-ls-tree line :$_");3542}35433544my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);35453546$seen_files->{$git_filename} =1;35473548my($oldhash,$oldrevision,$oldmode) = (3549$head->{$git_filename}{filehash},3550$head->{$git_filename}{revision},3551$head->{$git_filename}{mode}3552);35533554my$dbMode= convertToDbMode($mode);35553556# unless the file exists with the same hash, we need to update it ...3557unless(defined($oldhash)and$oldhasheq$git_hashand defined($oldmode)and$oldmodeeq$dbMode)3558{3559my$newrevision= ($oldrevisionor0) +1;35603561$head->{$git_filename} = {3562 name =>$git_filename,3563 revision =>$newrevision,3564 filehash =>$git_hash,3565 commithash =>$commit->{hash},3566 modified =>$cvsDate,3567 author =>$commit->{author},3568 mode =>$dbMode,3569};357035713572$self->insert_rev($git_filename,$newrevision,$git_hash,$commit->{hash},$cvsDate,$commit->{author},$dbMode);3573}3574}3575close FILELIST;35763577# Detect deleted files3578foreachmy$file(keys%$head)3579{3580unless(exists$seen_files->{$file}or$head->{$file}{filehash}eq"deleted")3581{3582$head->{$file}{revision}++;3583$head->{$file}{filehash} ="deleted";3584$head->{$file}{commithash} =$commit->{hash};3585$head->{$file}{modified} =$cvsDate;3586$head->{$file}{author} =$commit->{author};35873588$self->insert_rev($file,$head->{$file}{revision},$head->{$file}{filehash},$commit->{hash},$cvsDate,$commit->{author},$head->{$file}{mode});3589}3590}3591# END : "Detect deleted files"3592}359335943595if(exists$commit->{mergemsg})3596{3597$self->insert_mergelog($commit->{hash},$commit->{mergemsg});3598}35993600$lastpicked=$commit->{hash};36013602$self->_set_prop("last_commit",$commit->{hash});3603}36043605$self->delete_head();3606foreachmy$file(keys%$head)3607{3608$self->insert_head(3609$file,3610$head->{$file}{revision},3611$head->{$file}{filehash},3612$head->{$file}{commithash},3613$head->{$file}{modified},3614$head->{$file}{author},3615$head->{$file}{mode},3616);3617}3618# invalidate the gethead cache3619$self->clearCommitRefCaches();362036213622# Ending exclusive lock here3623$self->{dbh}->commit()or die"Failed to commit changes to SQLite";3624}36253626sub readCommits3627{3628my$pipeHandle=shift;3629my@commits;36303631my%commit= ();36323633while( <$pipeHandle> )3634{3635chomp;3636if(m/^commit\s+(.*)$/) {3637# on ^commit lines put the just seen commit in the stack3638# and prime things for the next one3639if(keys%commit) {3640my%copy=%commit;3641unshift@commits, \%copy;3642%commit= ();3643}3644my@parents=split(m/\s+/,$1);3645$commit{hash} =shift@parents;3646$commit{parents} = \@parents;3647}elsif(m/^(\w+?):\s+(.*)$/&& !exists($commit{message})) {3648# on rfc822-like lines seen before we see any message,3649# lowercase the entry and put it in the hash as key-value3650$commit{lc($1)} =$2;3651}else{3652# message lines - skip initial empty line3653# and trim whitespace3654if(!exists($commit{message}) &&m/^\s*$/) {3655# define it to mark the end of headers3656$commit{message} ='';3657next;3658}3659s/^\s+//;s/\s+$//;# trim ws3660$commit{message} .=$_."\n";3661}3662}36633664unshift@commits, \%commitif(keys%commit);36653666return@commits;3667}36683669sub convertToCvsDate3670{3671my$date=shift;3672# Convert from: "git rev-list --pretty" formatted date3673# Convert to: "the format specified by RFC822 as modified by RFC1123."3674# Example: 26 May 1997 13:01:40 -04003675if($date=~/^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/)3676{3677$date="$2$1$4$3$5";3678}36793680return$date;3681}36823683sub convertToDbMode3684{3685my$mode=shift;36863687# NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",3688# but the database "mode" column historically (and currently)3689# only stores the "rw" (for user) part of the string.3690# FUTURE: It might make more sense to persist the raw3691# octal mode (or perhaps the final full CVS form) instead of3692# this half-converted form, but it isn't currently worth the3693# backwards compatibility headaches.36943695$mode=~/^\d\d(\d)\d{3}$/;3696my$userBits=$1;36973698my$dbMode="";3699$dbMode.="r"if($userBits&4);3700$dbMode.="w"if($userBits&2);3701$dbMode.="x"if($userBits&1);3702$dbMode="rw"if($dbModeeq"");37033704return$dbMode;3705}37063707sub insert_rev3708{3709my$self=shift;3710my$name=shift;3711my$revision=shift;3712my$filehash=shift;3713my$commithash=shift;3714my$modified=shift;3715my$author=shift;3716my$mode=shift;3717my$tablename=$self->tablename("revision");37183719my$insert_rev=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3720$insert_rev->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3721}37223723sub insert_mergelog3724{3725my$self=shift;3726my$key=shift;3727my$value=shift;3728my$tablename=$self->tablename("commitmsgs");37293730my$insert_mergelog=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3731$insert_mergelog->execute($key,$value);3732}37333734sub delete_head3735{3736my$self=shift;3737my$tablename=$self->tablename("head");37383739my$delete_head=$self->{dbh}->prepare_cached("DELETE FROM$tablename",{},1);3740$delete_head->execute();3741}37423743sub insert_head3744{3745my$self=shift;3746my$name=shift;3747my$revision=shift;3748my$filehash=shift;3749my$commithash=shift;3750my$modified=shift;3751my$author=shift;3752my$mode=shift;3753my$tablename=$self->tablename("head");37543755my$insert_head=$self->{dbh}->prepare_cached("INSERT INTO$tablename(name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1);3756$insert_head->execute($name,$revision,$filehash,$commithash,$modified,$author,$mode);3757}37583759sub _get_prop3760{3761my$self=shift;3762my$key=shift;3763my$tablename=$self->tablename("properties");37643765my$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);3766$db_query->execute($key);3767my($value) =$db_query->fetchrow_array;37683769return$value;3770}37713772sub _set_prop3773{3774my$self=shift;3775my$key=shift;3776my$value=shift;3777my$tablename=$self->tablename("properties");37783779my$db_query=$self->{dbh}->prepare_cached("UPDATE$tablenameSET value=? WHERE key=?",{},1);3780$db_query->execute($value,$key);37813782unless($db_query->rows)3783{3784$db_query=$self->{dbh}->prepare_cached("INSERT INTO$tablename(key, value) VALUES (?,?)",{},1);3785$db_query->execute($key,$value);3786}37873788return$value;3789}37903791=head2 gethead37923793=cut37943795sub gethead3796{3797my$self=shift;3798my$intRev=shift;3799my$tablename=$self->tablename("head");38003801return$self->{gethead_cache}if(defined($self->{gethead_cache} ) );38023803my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM$tablenameORDER BY name ASC",{},1);3804$db_query->execute();38053806my$tree= [];3807while(my$file=$db_query->fetchrow_hashref)3808{3809if(!$intRev)3810{3811$file->{revision} ="1.$file->{revision}"3812}3813push@$tree,$file;3814}38153816$self->{gethead_cache} =$tree;38173818return$tree;3819}38203821=head2 getAnyHead38223823Returns a reference to an array of getmeta structures, one3824per file in the specified tree hash.38253826=cut38273828sub getAnyHead3829{3830my($self,$hash) =@_;38313832if(!defined($hash))3833{3834return$self->gethead();3835}38363837my@files;3838{3839open(my$filePipe,'-|','git','ls-tree','-z','-r',$hash)3840or die("Cannot call git-ls-tree :$!");3841local$/="\0";3842@files=<$filePipe>;3843close$filePipe;3844}38453846my$tree=[];3847my($line);3848foreach$line(@files)3849{3850$line=~s/\0$//;3851unless($line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3852{3853die("Couldn't process git-ls-tree line :$_");3854}38553856my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);3857push@$tree,$self->getMetaFromCommithash($git_filename,$hash);3858}38593860return$tree;3861}38623863=head2 getRevisionDirMap38643865A "revision dir map" contains all the plain-file filenames associated3866with a particular revision (treeish), organized by directory:38673868 $type = $out->{$dir}{$fullName}38693870The type of each is "F" (for ordinary file) or "D" (for directory,3871for which the map $out->{$fullName} will also exist).38723873=cut38743875sub getRevisionDirMap3876{3877my($self,$ver)=@_;38783879if(!defined($self->{revisionDirMapCache}))3880{3881$self->{revisionDirMapCache}={};3882}38833884# Get file list (previously cached results are dependent on HEAD,3885# but are early in each case):3886my$cacheKey;3887my(@fileList);3888if( !defined($ver) ||$vereq"")3889{3890$cacheKey="";3891if(defined($self->{revisionDirMapCache}{$cacheKey}) )3892{3893return$self->{revisionDirMapCache}{$cacheKey};3894}38953896my@head= @{$self->gethead()};3897foreachmy$file(@head)3898{3899next if($file->{filehash}eq"deleted");39003901push@fileList,$file->{name};3902}3903}3904else3905{3906my($hash)=$self->lookupCommitRef($ver);3907if( !defined($hash) )3908{3909returnundef;3910}39113912$cacheKey=$hash;3913if(defined($self->{revisionDirMapCache}{$cacheKey}) )3914{3915return$self->{revisionDirMapCache}{$cacheKey};3916}39173918open(my$filePipe,'-|','git','ls-tree','-z','-r',$hash)3919or die("Cannot call git-ls-tree :$!");3920local$/="\0";3921while( <$filePipe> )3922{3923chomp;3924unless(/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)3925{3926die("Couldn't process git-ls-tree line :$_");3927}39283929my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);39303931push@fileList,$git_filename;3932}3933close$filePipe;3934}39353936# Convert to normalized form:3937my%revMap;3938my$file;3939foreach$file(@fileList)3940{3941my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);3942$dir=''if(!defined($dir));39433944# parent directories:3945# ... create empty dir maps for parent dirs:3946my($td)=$dir;3947while(!defined($revMap{$td}))3948{3949$revMap{$td}={};39503951my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);3952$tp=''if(!defined($tp));3953$td=$tp;3954}3955# ... add children to parent maps (now that they exist):3956$td=$dir;3957while($tdne"")3958{3959my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);3960$tp=''if(!defined($tp));39613962if(defined($revMap{$tp}{$td}))3963{3964if($revMap{$tp}{$td}ne'D')3965{3966die"Weird file/directory inconsistency in$cacheKey";3967}3968last;# loop exit3969}3970$revMap{$tp}{$td}='D';39713972$td=$tp;3973}39743975# file3976$revMap{$dir}{$file}='F';3977}39783979# Save in cache:3980$self->{revisionDirMapCache}{$cacheKey}=\%revMap;3981return$self->{revisionDirMapCache}{$cacheKey};3982}39833984=head2 getlog39853986See also gethistorydense().39873988=cut39893990sub getlog3991{3992my$self=shift;3993my$filename=shift;3994my$revFilter=shift;39953996my$tablename=$self->tablename("revision");39973998# Filters:3999# TODO: date, state, or by specific logins filters?4000# TODO: Handle comma-separated list of revFilter items, each item4001# can be a range [only case currently handled] or individual4002# rev or branch or "branch.".4003# TODO: Adjust $db_query WHERE clause based on revFilter, instead of4004# manually filtering the results of the query?4005my($minrev,$maxrev);4006if(defined($revFilter)and4007$state->{opt}{r} =~/^(1.(\d+))?(::?)(1.(\d.+))?$/)4008{4009my$control=$3;4010$minrev=$2;4011$maxrev=$5;4012$minrev++if(defined($minrev)and$controleq"::");4013}40144015my$db_query=$self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM$tablenameWHERE name=? ORDER BY revision DESC",{},1);4016$db_query->execute($filename);40174018my$totalRevs=0;4019my$tree= [];4020while(my$file=$db_query->fetchrow_hashref)4021{4022$totalRevs++;4023if(defined($minrev)and$file->{revision} <$minrev)4024{4025next;4026}4027if(defined($maxrev)and$file->{revision} >$maxrev)4028{4029next;4030}40314032$file->{revision} ="1.".$file->{revision};4033push@$tree,$file;4034}40354036return($tree,$totalRevs);4037}40384039=head2 getmeta40404041This function takes a filename (with path) argument and returns a hashref of4042metadata for that file.40434044There are several ways $revision can be specified:40454046 - A reference to hash that contains a "tag" that is the4047 actual revision (one of the below). TODO: Also allow it to4048 specify a "date" in the hash.4049 - undef, to refer to the latest version on the main branch.4050 - Full CVS client revision number (mapped to integer in DB, without the4051 "1." prefix),4052 - Complex CVS-compatible "special" revision number for4053 non-linear history (see comment below)4054 - git commit sha1 hash4055 - branch or tag name40564057=cut40584059sub getmeta4060{4061my$self=shift;4062my$filename=shift;4063my$revision=shift;4064my$tablename_rev=$self->tablename("revision");4065my$tablename_head=$self->tablename("head");40664067if(ref($revision)eq"HASH")4068{4069$revision=$revision->{tag};4070}40714072# Overview of CVS revision numbers:4073#4074# General CVS numbering scheme:4075# - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.4076# - Result of "cvs checkin -r" (possible, but not really4077# recommended): "2.1", "2.2", etc4078# - Branch tag: "1.2.0.n", where "1.2" is revision it was branched4079# from, "0" is a magic placeholder that identifies it as a4080# branch tag instead of a version tag, and n is 2 times the4081# branch number off of "1.2", starting with "2".4082# - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"4083# is branch number off of "1.2" (like n above), and "x" is4084# the version number on the branch.4085# - Branches can branch off of branches: "1.3.2.7.4.1" (even number4086# of components).4087# - Odd "n"s are used by "vendor branches" that result4088# from "cvs import". Vendor branches have additional4089# strangeness in the sense that the main rcs "head" of the main4090# branch will (temporarily until first normal commit) point4091# to the version on the vendor branch, rather than the actual4092# main branch. (FUTURE: This may provide an opportunity4093# to use "strange" revision numbers for fast-forward-merged4094# branch tip when CVS client is asking for the main branch.)4095#4096# git-cvsserver CVS-compatible special numbering schemes:4097# - Currently git-cvsserver only tries to be identical to CVS for4098# simple "1.x" numbers on the "main" branch (as identified4099# by the module name that was originally cvs checkout'ed).4100# - The database only stores the "x" part, for historical reasons.4101# But most of the rest of the cvsserver preserves4102# and thinks using the full revision number.4103# - To handle non-linear history, it uses a version of the form4104# "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely4105# identify this as a special revision number, and there are4106# 20 b's that together encode the sha1 git commit from which4107# this version of this file originated. Each b is4108# the numerical value of the corresponding byte plus4109# 100.4110# - "plus 100" avoids "0"s, and also reduces the4111# likelyhood of a collision in the case that someone someday4112# writes an import tool that tries to preserve original4113# CVS revision numbers, and the original CVS data had done4114# lots of branches off of branches and other strangeness to4115# end up with a real version number that just happens to look4116# like this special revision number form. Also, if needed4117# there are several ways to extend/identify alternative encodings4118# within the "2.1.1.2000" part if necessary.4119# - Unlike real CVS revisions, you can't really reconstruct what4120# relation a revision of this form has to other revisions.4121# - FUTURE: TODO: Rework database somehow to make up and remember4122# fully-CVS-compatible branches and branch version numbers.41234124my$meta;4125if(defined($revision) )4126{4127if($revision=~/^1\.(\d+)$/)4128{4129my($intRev) =$1;4130my$db_query;4131$db_query=$self->{dbh}->prepare_cached(4132"SELECT * FROM$tablename_revWHERE name=? AND revision=?",4133{},1);4134$db_query->execute($filename,$intRev);4135$meta=$db_query->fetchrow_hashref;4136}4137elsif($revision=~/^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/)4138{4139my($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);4140$commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;4141if($commitHash=~/^[0-9a-f]{40}$/)4142{4143return$self->getMetaFromCommithash($filename,$commitHash);4144}41454146# error recovery: fall back on head version below4147print"E Failed to find$filenameversion=$revisionor commit=$commitHash\n";4148$log->warning("failed get$revisionwith commithash=$commitHash");4149undef$revision;4150}4151elsif($revision=~/^[0-9a-f]{40}$/)4152{4153# Try DB first. This is mostly only useful for req_annotate(),4154# which only calls this for stuff that should already be in4155# the DB. It is fairly likely to be a waste of time4156# in most other cases [unless the file happened to be4157# modified in $revision specifically], but4158# it is probably in the noise compared to how long4159# getMetaFromCommithash() will take.4160my$db_query;4161$db_query=$self->{dbh}->prepare_cached(4162"SELECT * FROM$tablename_revWHERE name=? AND commithash=?",4163{},1);4164$db_query->execute($filename,$revision);4165$meta=$db_query->fetchrow_hashref;41664167if(!$meta)4168{4169my($revCommit)=$self->lookupCommitRef($revision);4170if($revCommit=~/^[0-9a-f]{40}$/)4171{4172return$self->getMetaFromCommithash($filename,$revCommit);4173}41744175# error recovery: nothing found:4176print"E Failed to find$filenameversion=$revision\n";4177$log->warning("failed get$revision");4178return$meta;4179}4180}4181else4182{4183my($revCommit)=$self->lookupCommitRef($revision);4184if($revCommit=~/^[0-9a-f]{40}$/)4185{4186return$self->getMetaFromCommithash($filename,$revCommit);4187}41884189# error recovery: fall back on head version below4190print"E Failed to find$filenameversion=$revision\n";4191$log->warning("failed get$revision");4192undef$revision;# Allow fallback4193}4194}41954196if(!defined($revision))4197{4198my$db_query;4199$db_query=$self->{dbh}->prepare_cached(4200"SELECT * FROM$tablename_headWHERE name=?",{},1);4201$db_query->execute($filename);4202$meta=$db_query->fetchrow_hashref;4203}42044205if($meta)4206{4207$meta->{revision} ="1.$meta->{revision}";4208}4209return$meta;4210}42114212sub getMetaFromCommithash4213{4214my$self=shift;4215my$filename=shift;4216my$revCommit=shift;42174218# NOTE: This function doesn't scale well (lots of forks), especially4219# if you have many files that have not been modified for many commits4220# (each git-rev-parse redoes a lot of work for each file4221# that theoretically could be done in parallel by smarter4222# graph traversal).4223#4224# TODO: Possible optimization strategies:4225# - Solve the issue of assigning and remembering "real" CVS4226# revision numbers for branches, and ensure the4227# data structure can do this efficiently. Perhaps something4228# similar to "git notes", and carefully structured to take4229# advantage same-sha1-is-same-contents, to roll the same4230# unmodified subdirectory data onto multiple commits?4231# - Write and use a C tool that is like git-blame, but4232# operates on multiple files with file granularity, instead4233# of one file with line granularity. Cache4234# most-recently-modified in $self->{commitRefCache}{$revCommit}.4235# Try to be intelligent about how many files we do with4236# one fork (perhaps one directory at a time, without recursion,4237# and/or include directory as one line item, recurse from here4238# instead of in C tool?).4239# - Perhaps we could ask the DB for (filename,fileHash),4240# and just guess that it is correct (that the file hadn't4241# changed between $revCommit and the found commit, then4242# changed back, confusing anything trying to interpret4243# history). Probably need to add another index to revisions4244# DB table for this.4245# - NOTE: Trying to store all (commit,file) keys in DB [to4246# find "lastModfiedCommit] (instead of4247# just files that changed in each commit as we do now) is4248# probably not practical from a disk space perspective.42494250# Does the file exist in $revCommit?4251# TODO: Include file hash in dirmap cache.4252my($dirMap)=$self->getRevisionDirMap($revCommit);4253my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);4254if(!defined($dir))4255{4256$dir="";4257}4258if( !defined($dirMap->{$dir}) ||4259!defined($dirMap->{$dir}{$filename}) )4260{4261my($fileHash)="deleted";42624263my($retVal)={};4264$retVal->{name}=$filename;4265$retVal->{filehash}=$fileHash;42664267# not needed and difficult to compute:4268$retVal->{revision}="0";# $revision;4269$retVal->{commithash}=$revCommit;4270#$retVal->{author}=$commit->{author};4271#$retVal->{modified}=convertToCvsDate($commit->{date});4272#$retVal->{mode}=convertToDbMode($mode);42734274return$retVal;4275}42764277my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");4278chomp$fileHash;4279if(!($fileHash=~/^[0-9a-f]{40}$/))4280{4281die"Invalid fileHash '$fileHash' looking up"4282." '$revCommit:$filename'\n";4283}42844285# information about most recent commit to modify $filename:4286open(my$gitLogPipe,'-|','git','rev-list',4287'--max-count=1','--pretty','--parents',4288$revCommit,'--',$filename)4289or die"Cannot call git-rev-list:$!";4290my@commits=readCommits($gitLogPipe);4291close$gitLogPipe;4292if(scalar(@commits)!=1)4293{4294die"Can't find most recent commit changing$filename\n";4295}4296my($commit)=$commits[0];4297if( !defined($commit) || !defined($commit->{hash}) )4298{4299returnundef;4300}43014302# does this (commit,file) have a real assigned CVS revision number?4303my$tablename_rev=$self->tablename("revision");4304my$db_query;4305$db_query=$self->{dbh}->prepare_cached(4306"SELECT * FROM$tablename_revWHERE name=? AND commithash=?",4307{},1);4308$db_query->execute($filename,$commit->{hash});4309my($meta)=$db_query->fetchrow_hashref;4310if($meta)4311{4312$meta->{revision} ="1.$meta->{revision}";4313return$meta;4314}43154316# fall back on special revision number4317my($revision)=$commit->{hash};4318$revision=~s/(..)/'.' . (hex($1)+100)/eg;4319$revision="2.1.1.2000$revision";43204321# meta data about $filename:4322open(my$filePipe,'-|','git','ls-tree','-z',4323$commit->{hash},'--',$filename)4324or die("Cannot call git-ls-tree :$!");4325local$/="\0";4326my$line;4327$line=<$filePipe>;4328if(defined(<$filePipe>))4329{4330die"Expected only a single file for git-ls-tree$filename\n";4331}4332close$filePipe;43334334chomp$line;4335unless($line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o)4336{4337die("Couldn't process git-ls-tree line :$line\n");4338}4339my($mode,$git_type,$git_hash,$git_filename) = ($1,$2,$3,$4);43404341# save result:4342my($retVal)={};4343$retVal->{name}=$filename;4344$retVal->{revision}=$revision;4345$retVal->{filehash}=$fileHash;4346$retVal->{commithash}=$revCommit;4347$retVal->{author}=$commit->{author};4348$retVal->{modified}=convertToCvsDate($commit->{date});4349$retVal->{mode}=convertToDbMode($mode);43504351return$retVal;4352}43534354=head2 lookupCommitRef43554356Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches4357the result so looking it up again is fast.43584359=cut43604361sub lookupCommitRef4362{4363my$self=shift;4364my$ref=shift;43654366my$commitHash=$self->{commitRefCache}{$ref};4367if(defined($commitHash))4368{4369return$commitHash;4370}43714372$commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",4373$self->unescapeRefName($ref));4374$commitHash=~s/\s*$//;4375if(!($commitHash=~/^[0-9a-f]{40}$/))4376{4377$commitHash=undef;4378}43794380if(defined($commitHash) )4381{4382my$type=safe_pipe_capture("git","cat-file","-t",$commitHash);4383if( ! ($type=~/^commit\s*$/) )4384{4385$commitHash=undef;4386}4387}4388if(defined($commitHash))4389{4390$self->{commitRefCache}{$ref}=$commitHash;4391}4392return$commitHash;4393}43944395=head2 clearCommitRefCaches43964397Clears cached commit cache (sha1's for various tags/abbeviations/etc),4398and related caches.43994400=cut44014402sub clearCommitRefCaches4403{4404my$self=shift;4405$self->{commitRefCache} = {};4406$self->{revisionDirMapCache} =undef;4407$self->{gethead_cache} =undef;4408}44094410=head2 commitmessage44114412this function takes a commithash and returns the commit message for that commit44134414=cut4415sub commitmessage4416{4417my$self=shift;4418my$commithash=shift;4419my$tablename=$self->tablename("commitmsgs");44204421die("Need commithash")unless(defined($commithash)and$commithash=~/^[a-zA-Z0-9]{40}$/);44224423my$db_query;4424$db_query=$self->{dbh}->prepare_cached("SELECT value FROM$tablenameWHERE key=?",{},1);4425$db_query->execute($commithash);44264427my($message) =$db_query->fetchrow_array;44284429if(defined($message) )4430{4431$message.=" "if($message=~/\n$/);4432return$message;4433}44344435my@lines= safe_pipe_capture("git","cat-file","commit",$commithash);4436shift@lineswhile($lines[0] =~/\S/);4437$message=join("",@lines);4438$message.=" "if($message=~/\n$/);4439return$message;4440}44414442=head2 gethistorydense44434444This function takes a filename (with path) argument and returns an arrayofarrays4445containing revision,filehash,commithash ordered by revision descending.44464447This version of gethistory skips deleted entries -- so it is useful for annotate.4448The 'dense' part is a reference to a '--dense' option available for git-rev-list4449and other git tools that depend on it.44504451See also getlog().44524453=cut4454sub gethistorydense4455{4456my$self=shift;4457my$filename=shift;4458my$tablename=$self->tablename("revision");44594460my$db_query;4461$db_query=$self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM$tablenameWHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);4462$db_query->execute($filename);44634464my$result=$db_query->fetchall_arrayref;44654466my$i;4467for($i=0;$i<scalar(@$result) ;$i++)4468{4469$result->[$i][0]="1.".$result->[$i][0];4470}44714472return$result;4473}44744475=head2 escapeRefName44764477Apply an escape mechanism to compensate for characters that4478git ref names can have that CVS tags can not.44794480=cut4481sub escapeRefName4482{4483my($self,$refName)=@_;44844485# CVS officially only allows [-_A-Za-z0-9] in tag names (or in4486# many contexts it can also be a CVS revision number).4487#4488# Git tags commonly use '/' and '.' as well, but also handle4489# anything else just in case:4490#4491# = "_-s-" For '/'.4492# = "_-p-" For '.'.4493# = "_-u-" For underscore, in case someone wants a literal "_-" in4494# a tag name.4495# = "_-xx-" Where "xx" is the hexadecimal representation of the4496# desired ASCII character byte. (for anything else)44974498if(!$refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)4499{4500$refName=~s/_-/_-u--/g;4501$refName=~s/\./_-p-/g;4502$refName=~s%/%_-s-%g;4503$refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;4504}4505}45064507=head2 unescapeRefName45084509Undo an escape mechanism to compensate for characters that4510git ref names can have that CVS tags can not.45114512=cut4513sub unescapeRefName4514{4515my($self,$refName)=@_;45164517# see escapeRefName() for description of escape mechanism.45184519$refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;45204521# allowed tag names4522# TODO: Perhaps use git check-ref-format, with an in-process cache of4523# validated names?4524if( !($refName=~m%^[^-][-a-zA-Z0-9_/.]*$%) ||4525($refName=~m%[/.]$%) ||4526($refName=~/\.lock$/) ||4527($refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) )# matching }4528{4529# Error:4530$log->warn("illegal refName:$refName");4531$refName=undef;4532}4533return$refName;4534}45354536sub unescapeRefNameChar4537{4538my($char)=@_;45394540if($chareq"s")4541{4542$char="/";4543}4544elsif($chareq"p")4545{4546$char=".";4547}4548elsif($chareq"u")4549{4550$char="_";4551}4552elsif($char=~/^[0-9a-f][0-9a-f]$/)4553{4554$char=chr(hex($char));4555}4556else4557{4558# Error case: Maybe it has come straight from user, and4559# wasn't supposed to be escaped? Restore it the way we got it:4560$char="_-$char-";4561}45624563return$char;4564}45654566=head2 in_array()45674568from Array::PAT - mimics the in_array() function4569found in PHP. Yuck but works for small arrays.45704571=cut4572sub in_array4573{4574my($check,@array) =@_;4575my$retval=0;4576foreachmy$test(@array){4577if($checkeq$test){4578$retval=1;4579}4580}4581return$retval;4582}45834584=head2 safe_pipe_capture45854586an alternative to `command` that allows input to be passed as an array4587to work around shell problems with weird characters in arguments45884589=cut4590sub safe_pipe_capture {45914592my@output;45934594if(my$pid=open my$child,'-|') {4595@output= (<$child>);4596close$childor die join(' ',@_).":$!$?";4597}else{4598exec(@_)or die"$!$?";# exec() can fail the executable can't be found4599}4600returnwantarray?@output:join('',@output);4601}46024603=head2 mangle_dirname46044605create a string from a directory name that is suitable to use as4606part of a filename, mainly by converting all chars except \w.- to _46074608=cut4609sub mangle_dirname {4610my$dirname=shift;4611return unlessdefined$dirname;46124613$dirname=~s/[^\w.-]/_/g;46144615return$dirname;4616}46174618=head2 mangle_tablename46194620create a string from a that is suitable to use as part of an SQL table4621name, mainly by converting all chars except \w to _46224623=cut4624sub mangle_tablename {4625my$tablename=shift;4626return unlessdefined$tablename;46274628$tablename=~s/[^\w_]/_/g;46294630return$tablename;4631}463246331;