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