1#!/usr/bin/perl -w 2# 3# This tool is copyright (c) 2005, Martin Langhoff. 4# It is released under the Gnu Public License, version 2. 5# 6# The basic idea is to walk the output of tla abrowse, 7# fetch the changesets and apply them. 8# 9=head1 Invocation 10 11 git-archimport -i <archive>/<branch> [<archive>/<branch>] 12 [ <archive>/<branch> ] 13 14 The script expects you to provide the key roots where it can start the 15 import from an 'initial import' or 'tag' type of Arch commit. It will 16 then follow all the branching and tagging within the provided roots. 17 18 It will die if it sees branches that have different roots. 19 20=head2 TODO 21 22 - keep track of merged patches, and mark a git merge when it happens 23 - smarter rules to parse the archive history "up" and "down" 24 - be able to continue an import where we left off 25 - audit shell-escaping of filenames 26 27=head1 Devel tricks 28 29Add print in front of the shell commands invoked via backticks. 30 31=cut 32 33use strict; 34use warnings; 35use Getopt::Std; 36use File::Spec; 37use File::Temp qw(tempfile); 38use File::Path qw(mkpath); 39use File::Basename qw(basename dirname); 40use String::ShellQuote; 41use Time::Local; 42use IO::Socket; 43use IO::Pipe; 44use POSIX qw(strftime dup2); 45use Data::Dumper qw/ Dumper /; 46use IPC::Open2; 47 48$SIG{'PIPE'}="IGNORE"; 49$ENV{'TZ'}="UTC"; 50 51our($opt_h,$opt_v,$opt_T, 52$opt_C,$opt_t); 53 54sub usage() { 55print STDERR <<END; 56Usage: ${\basename$0} # fetch/update GIT from Arch 57 [ -h ] [ -v ] [ -T ] 58 [ -C GIT_repository ] [ -t tempdir ] 59 repository/arch-branch [ repository/arch-branch] ... 60END 61exit(1); 62} 63 64getopts("ThviC:t:")or usage(); 65usage if$opt_h; 66 67@ARGV>=1or usage(); 68my@arch_roots=@ARGV; 69 70my$tmp=$opt_t; 71$tmp||='/tmp'; 72$tmp.='/git-archimport/'; 73 74my$git_tree=$opt_C; 75$git_tree||="."; 76 77 78my@psets= ();# the collection 79my%psets= ();# the collection, by name 80 81my%rptags= ();# my reverse private tags 82# to map a SHA1 to a commitid 83 84foreachmy$root(@arch_roots) { 85my($arepo,$abranch) =split(m!/!,$root); 86open ABROWSE,"tla abrowse -f -A$arepo--desc --merges$abranch|" 87or die"Problems with tla abrowse:$!"; 88 89my%ps= ();# the current one 90my$mode=''; 91my$lastseen=''; 92 93while(<ABROWSE>) { 94chomp; 95 96# first record padded w 8 spaces 97if(s/^\s{8}\b//) { 98 99# store the record we just captured 100if(%ps) { 101my%temp=%ps;# break references 102push(@psets, \%temp); 103$psets{$temp{id}} = \%temp; 104%ps= (); 105} 106 107my($id,$type) =split(m/\s{3}/,$_); 108$ps{id} =$id; 109$ps{repo} =$arepo; 110 111# deal with types 112if($type=~m/^\(simple changeset\)/) { 113$ps{type} ='s'; 114}elsif($typeeq'(initial import)') { 115$ps{type} ='i'; 116}elsif($type=~m/^\(tag revision of (.+)\)/) { 117$ps{type} ='t'; 118$ps{tag} =$1; 119}else{ 120warn"Unknown type$type"; 121} 122$lastseen='id'; 123} 124 125if(s/^\s{10}//) { 126# 10 leading spaces or more 127# indicate commit metadata 128 129# date & author 130if($lastseeneq'id'&&m/^\d{4}-\d{2}-\d{2}/) { 131 132my($date,$authoremail) =split(m/\s{2,}/,$_); 133$ps{date} =$date; 134$ps{date} =~s/\bGMT$//;# strip off trailign GMT 135if($ps{date} =~m/\b\w+$/) { 136warn'Arch dates not in GMT?! - imported dates will be wrong'; 137} 138 139$authoremail=~m/^(.+)\s(\S+)$/; 140$ps{author} =$1; 141$ps{email} =$2; 142 143$lastseen='date'; 144 145}elsif($lastseeneq'date') { 146# the only hint is position 147# subject is after date 148$ps{subj} =$_; 149$lastseen='subj'; 150 151}elsif($lastseeneq'subj'&&$_eq'merges in:') { 152$ps{merges} = []; 153$lastseen='merges'; 154 155}elsif($lastseeneq'merges'&&s/^\s{2}//) { 156push(@{$ps{merges}},$_); 157}else{ 158warn'more metadata after merges!?'; 159} 160 161} 162} 163 164if(%ps) { 165my%temp=%ps;# break references 166push(@psets, \%temp); 167$psets{$temp{id} } = \%temp; 168%ps= (); 169} 170close ABROWSE; 171}# end foreach $root 172 173## Order patches by time 174@psets=sort{$a->{date}.$b->{id}cmp$b->{date}.$b->{id}}@psets; 175 176#print Dumper \@psets; 177 178## 179## TODO cleanup irrelevant patches 180## and put an initial import 181## or a full tag 182my$import=0; 183unless(-d '.git') {# initial import 184if($psets[0]{type}eq'i'||$psets[0]{type}eq't') { 185print"Starting import from$psets[0]{id}\n"; 186`git-init-db`; 187die$!if$?; 188$import=1; 189}else{ 190die"Need to start from an import or a tag -- cannot use$psets[0]{id}"; 191} 192}else{# progressing an import 193# load the rptags 194opendir(DIR,".git/archimport/tags") 195||die"can't opendir:$!"; 196while(my$file=readdir(DIR)) { 197# skip non-interesting-files 198next unless-f ".git/archimport/tags/$file"; 199next if$file=~m/--base-0$/;# don't care for base-0 200my$sha= ptag($file); 201chomp$sha; 202# reconvert the 3rd '--' sequence from the end 203# into a slash 204# $file = reverse $file; 205# $file =~ s!^(.+?--.+?--.+?--.+?)--(.+)$!$1/$2!; 206# $file = reverse $file; 207$rptags{$sha} =$file; 208} 209closedir DIR; 210} 211 212# process patchsets 213foreachmy$ps(@psets) { 214 215$ps->{branch} = branchname($ps->{id}); 216 217# 218# ensure we have a clean state 219# 220if(`git diff-files`) { 221die"Unclean tree when about to process$ps->{id} ". 222" - did we fail to commit cleanly before?"; 223} 224die$!if$?; 225 226# 227# skip commits already in repo 228# 229if(ptag($ps->{id})) { 230$opt_v&&print"Skipping already imported:$ps->{id}\n"; 231next; 232} 233 234# 235# create the branch if needed 236# 237if($ps->{type}eq'i'&& !$import) { 238die"Should not have more than one 'Initial import' per GIT import:$ps->{id}"; 239} 240 241unless($import) {# skip for import 242if( -e ".git/refs/heads/$ps->{branch}") { 243# we know about this branch 244`git checkout$ps->{branch}`; 245}else{ 246# new branch! we need to verify a few things 247die"Branch on a non-tag!"unless$ps->{type}eq't'; 248my$branchpoint= ptag($ps->{tag}); 249die"Tagging from unknown id unsupported:$ps->{tag}" 250unless$branchpoint; 251 252# find where we are supposed to branch from 253`git checkout -b$ps->{branch}$branchpoint`; 254 255 # If we trust Arch with the fact that this is just 256 # a tag, and it does not affect the state of the tree 257 # then we just tag and move on 258 tag($ps->{id},$branchpoint); 259 ptag($ps->{id},$branchpoint); 260 print " * Tagged$ps->{id} at$branchpoint\n"; 261 next; 262 } 263 die$!if$?; 264 } 265 266 # 267 # Apply the import/changeset/merge into the working tree 268 # 269 if ($ps->{type} eq 'i' ||$ps->{type} eq 't') { 270 apply_import($ps) or die$!; 271$import=0; 272 } elsif ($ps->{type} eq 's') { 273 apply_cset($ps); 274 } 275 276 # 277 # prepare update git's index, based on what arch knows 278 # about the pset, resolve parents, etc 279 # 280 my$tree; 281 282 my$commitlog= `tla cat-archive-log -A $ps->{repo}$ps->{id}`; 283 die "Error in cat-archive-log:$!" if$?; 284 285 # parselog will git-add/rm files 286 # and generally prepare things for the commit 287 # NOTE: parselog will shell-quote filenames! 288 my ($sum,$msg,$add,$del,$mod,$ren) = parselog($commitlog); 289 my$logmessage= "$sum\n$msg"; 290 291 292 # imports don't give us good info 293 # on added files. Shame on them 294 if ($ps->{type} eq 'i' ||$ps->{type} eq 't') { 295 `find . -type f -print0 |grep-zv '^./.git'| xargs -0-l100 git-update-index --add`; 296 `git-ls-files --deleted -z | xargs --no-run-if-empty -0-l100 git-update-index --remove`; 297 } 298 299 if (@$add) { 300 while (@$add) { 301 my@slice= splice(@$add, 0, 100); 302 my$slice= join(' ',@slice); 303 `git-update-index --add $slice`; 304die"Error in git-update-index --add:$!"if$?; 305} 306} 307if(@$del) { 308foreachmy$file(@$del) { 309unlink$fileor die"Problems deleting$file:$!"; 310} 311while(@$del) { 312my@slice=splice(@$del,0,100); 313my$slice=join(' ',@slice); 314`git-update-index --remove$slice`; 315 die "Error in git-update-index --remove:$!" if$?; 316 } 317 } 318 if (@$ren) { # renamed 319 if (@$ren% 2) { 320 die "Odd number of entries in rename!?"; 321 } 322 ; 323 while (@$ren) { 324 my$from= pop@$ren; 325 my$to= pop@$ren; 326 327 unless (-d dirname($to)) { 328 mkpath(dirname($to)); # will die on err 329 } 330 #print "moving$from$to"; 331 `mv $from $to`; 332die"Error renaming$from$to:$!"if$?; 333`git-update-index --remove$from`; 334 die "Error in git-update-index --remove:$!" if$?; 335 `git-update-index --add $to`; 336die"Error in git-update-index --add:$!"if$?; 337} 338 339} 340if(@$mod) {# must be _after_ renames 341while(@$mod) { 342my@slice=splice(@$mod,0,100); 343my$slice=join(' ',@slice); 344`git-update-index$slice`; 345 die "Error in git-update-index:$!" if$?; 346 } 347 } 348 349 # warn "errors when running git-update-index!$!"; 350$tree= `git-write-tree`; 351 die "cannot write tree$!" if$?; 352 chomp$tree; 353 354 355 # 356 # Who's your daddy? 357 # 358 my@par; 359 if ( -e ".git/refs/heads/$ps->{branch}") { 360 if (open HEAD, "<.git/refs/heads/$ps->{branch}") { 361 my$p= <HEAD>; 362 close HEAD; 363 chomp$p; 364 push@par, '-p',$p; 365 } else { 366 if ($ps->{type} eq 's') { 367 warn "Could not find the right head for the branch$ps->{branch}"; 368 } 369 } 370 } 371 372 if ($ps->{merges}) { 373 push@par, find_parents($ps); 374 } 375 my$par= join (' ',@par); 376 377 # 378 # Commit, tag and clean state 379 # 380$ENV{TZ} = 'GMT'; 381$ENV{GIT_AUTHOR_NAME} =$ps->{author}; 382$ENV{GIT_AUTHOR_EMAIL} =$ps->{email}; 383$ENV{GIT_AUTHOR_DATE} =$ps->{date}; 384$ENV{GIT_COMMITTER_NAME} =$ps->{author}; 385$ENV{GIT_COMMITTER_EMAIL} =$ps->{email}; 386$ENV{GIT_COMMITTER_DATE} =$ps->{date}; 387 388 my ($pid,$commit_rh,$commit_wh); 389$commit_rh= 'commit_rh'; 390$commit_wh= 'commit_wh'; 391 392$pid= open2(*READER, *WRITER, "git-commit-tree$tree$par") 393 or die$!; 394 print WRITER$logmessage; # write 395 close WRITER; 396 my$commitid= <READER>; # read 397 chomp$commitid; 398 close READER; 399 waitpid$pid,0; # close; 400 401 if (length$commitid!= 40) { 402 die "Something went wrong with the commit!$!$commitid"; 403 } 404 # 405 # Update the branch 406 # 407 open HEAD, ">.git/refs/heads/$ps->{branch}"; 408 print HEAD$commitid; 409 close HEAD; 410 unlink ('.git/HEAD'); 411 symlink("refs/heads/$ps->{branch}",".git/HEAD"); 412 413 # tag accordingly 414 ptag($ps->{id},$commitid); # private tag 415 if ($opt_T||$ps->{type} eq 't' ||$ps->{type} eq 'i') { 416 tag($ps->{id},$commitid); 417 } 418 print " * Committed$ps->{id}\n"; 419 print " + tree$tree\n"; 420 print " + commit$commitid\n"; 421$opt_v&& print " + commit date is$ps->{date}\n"; 422$opt_v&& print " + parents:$par\n"; 423} 424 425sub branchname { 426 my$id= shift; 427$id=~ s#^.+?/##; 428 my@parts= split(m/--/,$id); 429 return join('--',@parts[0..1]); 430} 431 432sub apply_import { 433 my$ps= shift; 434 my$bname= branchname($ps->{id}); 435 436 `mkdir-p $tmp`; 437 438`tla get -s --no-pristine -A$ps->{repo}$ps->{id}$tmp/import`; 439 die "Cannot get import:$!" if$?; 440 `rsync -v --archive --delete--exclude '.git'--exclude '.arch-ids'--exclude '{arch}'$tmp/import/*./`; 441 die "Cannot rsync import:$!" if$?; 442 443 `rm -fr $tmp/import`; 444die"Cannot remove tempdir:$!"if$?; 445 446 447return1; 448} 449 450sub apply_cset { 451my$ps=shift; 452 453`mkdir -p$tmp`; 454 455 # get the changeset 456 `tla get-changeset -A $ps->{repo}$ps->{id}$tmp/changeset`; 457die"Cannot get changeset:$!"if$?; 458 459# apply patches 460if(`find$tmp/changeset/patches-type f -name '*.patch'`) { 461# this can be sped up considerably by doing 462# (find | xargs cat) | patch 463# but that cna get mucked up by patches 464# with missing trailing newlines or the standard 465# 'missing newline' flag in the patch - possibly 466# produced with an old/buggy diff. 467# slow and safe, we invoke patch once per patchfile 468`find$tmp/changeset/patches-type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`; 469die"Problem applying patches!$!"if$?; 470} 471 472# apply changed binary files 473if(my@modified=`find$tmp/changeset/patches-type f -name '*.modified'`) { 474foreachmy$mod(@modified) { 475chomp$mod; 476my$orig=$mod; 477$orig=~s/\.modified$//;# lazy 478$orig=~s!^\Q$tmp\E/changeset/patches/!!; 479#print "rsync -p '$mod' '$orig'"; 480`rsync -p$mod./$orig`; 481 die "Problem applying binary changes!$!" if$?; 482 } 483 } 484 485 # bring in new files 486 `rsync --archive --exclude '.git'--exclude '.arch-ids'--exclude '{arch}'$tmp/changeset/new-files-archive/* ./`; 487 488 # deleted files are hinted from the commitlog processing 489 490 `rm -fr $tmp/changeset`; 491} 492 493 494# =for reference 495# A log entry looks like 496# Revision: moodle-org--moodle--1.3.3--patch-15 497# Archive: arch-eduforge@catalyst.net.nz--2004 498# Creator: Penny Leach <penny@catalyst.net.nz> 499# Date: Wed May 25 14:15:34 NZST 2005 500# Standard-date: 2005-05-25 02:15:34 GMT 501# New-files: lang/de/.arch-ids/block_glossary_random.php.id 502# lang/de/.arch-ids/block_html.php.id 503# New-directories: lang/de/help/questionnaire 504# lang/de/help/questionnaire/.arch-ids 505# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id 506# db_sears.sql db/db_sears.sql 507# Removed-files: lang/be/docs/.arch-ids/release.html.id 508# lang/be/docs/.arch-ids/releaseold.html.id 509# Modified-files: admin/cron.php admin/delete.php 510# admin/editor.html backup/lib.php backup/restore.php 511# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15 512# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+) 513# Keywords: 514# 515# Updating yadda tadda tadda madda 516sub parselog { 517my$log=shift; 518#print $log; 519 520my(@add,@del,@mod,@ren,@kw,$sum,$msg); 521 522if($log=~m/(?:\n|^)New-files:(.*?)(?=\n\w)/s) { 523my$files=$1; 524@add=split(m/\s+/s,$files); 525} 526 527if($log=~m/(?:\n|^)Removed-files:(.*?)(?=\n\w)/s) { 528my$files=$1; 529@del=split(m/\s+/s,$files); 530} 531 532if($log=~m/(?:\n|^)Modified-files:(.*?)(?=\n\w)/s) { 533my$files=$1; 534@mod=split(m/\s+/s,$files); 535} 536 537if($log=~m/(?:\n|^)Renamed-files:(.*?)(?=\n\w)/s) { 538my$files=$1; 539@ren=split(m/\s+/s,$files); 540} 541 542$sum=''; 543if($log=~m/^Summary:(.+?)$/m) { 544$sum=$1; 545$sum=~s/^\s+//; 546$sum=~s/\s+$//; 547} 548 549$msg=''; 550if($log=~m/\n\n(.+)$/s) { 551$msg=$1; 552$msg=~s/^\s+//; 553$msg=~s/\s+$//; 554} 555 556 557# cleanup the arrays 558foreachmy$ref( (\@add, \@del, \@mod, \@ren) ) { 559my@tmp= (); 560while(my$t=pop@$ref) { 561next unlesslength($t); 562next if$t=~m!\{arch\}/!; 563next if$t=~m!\.arch-ids/!; 564next if$t=~m!\.arch-inventory$!; 565push(@tmp, shell_quote($t)); 566} 567@$ref=@tmp; 568} 569 570#print Dumper [$sum, $msg, \@add, \@del, \@mod, \@ren]; 571return($sum,$msg, \@add, \@del, \@mod, \@ren); 572} 573 574# write/read a tag 575sub tag { 576my($tag,$commit) =@_; 577$tag=~ s|/|--|g; 578$tag= shell_quote($tag); 579 580if($commit) { 581open(C,">.git/refs/tags/$tag") 582or die"Cannot create tag$tag:$!\n"; 583print C "$commit\n" 584or die"Cannot write tag$tag:$!\n"; 585close(C) 586or die"Cannot write tag$tag:$!\n"; 587print" * Created tag '$tag' on '$commit'\n"if$opt_v; 588}else{# read 589open(C,"<.git/refs/tags/$tag") 590or die"Cannot read tag$tag:$!\n"; 591$commit= <C>; 592chomp$commit; 593die"Error reading tag$tag:$!\n"unlesslength$commit==40; 594close(C) 595or die"Cannot read tag$tag:$!\n"; 596return$commit; 597} 598} 599 600# write/read a private tag 601# reads fail softly if the tag isn't there 602sub ptag { 603my($tag,$commit) =@_; 604$tag=~ s|/|--|g; 605$tag= shell_quote($tag); 606 607unless(-d '.git/archimport/tags') { 608 mkpath('.git/archimport/tags'); 609} 610 611if($commit) {# write 612open(C,">.git/archimport/tags/$tag") 613or die"Cannot create tag$tag:$!\n"; 614print C "$commit\n" 615or die"Cannot write tag$tag:$!\n"; 616close(C) 617or die"Cannot write tag$tag:$!\n"; 618$rptags{$commit} =$tag 619unless$tag=~m/--base-0$/; 620}else{# read 621# if the tag isn't there, return 0 622unless( -s ".git/archimport/tags/$tag") { 623return0; 624} 625open(C,"<.git/archimport/tags/$tag") 626or die"Cannot read tag$tag:$!\n"; 627$commit= <C>; 628chomp$commit; 629die"Error reading tag$tag:$!\n"unlesslength$commit==40; 630close(C) 631or die"Cannot read tag$tag:$!\n"; 632unless(defined$rptags{$commit}) { 633$rptags{$commit} =$tag; 634} 635return$commit; 636} 637} 638 639sub find_parents { 640# 641# Identify what branches are merging into me 642# and whether we are fully merged 643# git-merge-base <headsha> <headsha> should tell 644# me what the base of the merge should be 645# 646my$ps=shift; 647 648my%branches;# holds an arrayref per branch 649# the arrayref contains a list of 650# merged patches between the base 651# of the merge and the current head 652 653my@parents;# parents found for this commit 654 655# simple loop to split the merges 656# per branch 657foreachmy$merge(@{$ps->{merges}}) { 658my$branch= branchname($merge); 659unless(defined$branches{$branch} ){ 660$branches{$branch} = []; 661} 662push@{$branches{$branch}},$merge; 663} 664 665# 666# foreach branch find a merge base and walk it to the 667# head where we are, collecting the merged patchsets that 668# Arch has recorded. Keep that in @have 669# Compare that with the commits on the other branch 670# between merge-base and the tip of the branch (@need) 671# and see if we have a series of consecutive patches 672# starting from the merge base. The tip of the series 673# of consecutive patches merged is our new parent for 674# that branch. 675# 676foreachmy$branch(keys%branches) { 677my$mergebase=`git-merge-base$branch$ps->{branch}`; 678die"Cannot find merge base for$branchand$ps->{branch}"if$?; 679chomp$mergebase; 680 681# now walk up to the mergepoint collecting what patches we have 682my$branchtip= git_rev_parse($ps->{branch}); 683my@ancestors=`git-rev-list --merge-order$branchtip^$mergebase`; 684 my%have; # collected merges this branch has 685 foreach my$merge(@{$ps->{merges}}) { 686$have{$merge} = 1; 687 } 688 my%ancestorshave; 689 foreach my$par(@ancestors) { 690$par= commitid2pset($par); 691 if (defined$par->{merges}) { 692 foreach my$merge(@{$par->{merges}}) { 693$ancestorshave{$merge}=1; 694 } 695 } 696 } 697 # print "++++ Merges in$ps->{id} are....\n"; 698 # my@have= sort keys%have; print Dumper(\@have); 699 700 # merge what we have with what ancestors have 701%have= (%have,%ancestorshave); 702 703 # see what the remote branch has - these are the merges we 704 # will want to have in a consecutive series from the mergebase 705 my$otherbranchtip= git_rev_parse($branch); 706 my@needraw= `git-rev-list --merge-order $otherbranchtip^$mergebase`; 707my@need; 708foreachmy$needps(@needraw) {# get the psets 709$needps= commitid2pset($needps); 710# git-rev-list will also 711# list commits merged in via earlier 712# merges. we are only interested in commits 713# from the branch we're looking at 714if($brancheq$needps->{branch}) { 715push@need,$needps->{id}; 716} 717} 718 719# print "++++ Merges from $branch we want are....\n"; 720# print Dumper(\@need); 721 722my$newparent; 723while(my$needed_commit=pop@need) { 724if($have{$needed_commit}) { 725$newparent=$needed_commit; 726}else{ 727last;# break out of the while 728} 729} 730if($newparent) { 731push@parents,$newparent; 732} 733 734 735}# end foreach branch 736 737# prune redundant parents 738my%parents; 739foreachmy$p(@parents) { 740$parents{$p} =1; 741} 742foreachmy$p(@parents) { 743next unlessexists$psets{$p}{merges}; 744next unlessref$psets{$p}{merges}; 745my@merges= @{$psets{$p}{merges}}; 746foreachmy$merge(@merges) { 747if($parents{$merge}) { 748delete$parents{$merge}; 749} 750} 751} 752@parents=keys%parents; 753@parents=map{" -p ". ptag($_) }@parents; 754return@parents; 755} 756 757sub git_rev_parse { 758my$name=shift; 759my$val=`git-rev-parse$name`; 760 die "Error: git-rev-parse$name" if$?; 761 chomp$val; 762 return$val; 763} 764 765# resolve a SHA1 to a known patchset 766sub commitid2pset { 767 my$commitid= shift; 768 chomp$commitid; 769 my$name=$rptags{$commitid} 770 || die "Cannot find reverse tag mapping for$commitid"; 771 # the keys in%rptagare slightly munged; unmunge 772 # reconvert the 3rd '--' sequence from the end 773 # into a slash 774$name= reverse$name; 775$name=~ s!^(.+?--.+?--.+?--.+?)--(.+)$!$1/$2!; 776$name= reverse$name; 777 my$ps=$psets{$name} 778 || (print Dumper(sort keys%psets)) && die "Cannot find patchset for$name"; 779 return$ps; 780}