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 235# 236# create the branch if needed 237# 238if($ps->{type}eq'i'&& !$import) { 239die"Should not have more than one 'Initial import' per GIT import:$ps->{id}"; 240} 241 242unless($import) {# skip for import 243if( -e "$git_dir/refs/heads/$ps->{branch}") { 244# we know about this branch 245`git checkout$ps->{branch}`; 246}else{ 247# new branch! we need to verify a few things 248die"Branch on a non-tag!"unless$ps->{type}eq't'; 249my$branchpoint= ptag($ps->{tag}); 250die"Tagging from unknown id unsupported:$ps->{tag}" 251unless$branchpoint; 252 253# find where we are supposed to branch from 254`git checkout -b$ps->{branch}$branchpoint`; 255 256 # If we trust Arch with the fact that this is just 257 # a tag, and it does not affect the state of the tree 258 # then we just tag and move on 259 tag($ps->{id},$branchpoint); 260 ptag($ps->{id},$branchpoint); 261 print " * Tagged$ps->{id} at$branchpoint\n"; 262 next; 263 } 264 die$!if$?; 265 } 266 267 # 268 # Apply the import/changeset/merge into the working tree 269 # 270 if ($ps->{type} eq 'i' ||$ps->{type} eq 't') { 271 apply_import($ps) or die$!; 272$import=0; 273 } elsif ($ps->{type} eq 's') { 274 apply_cset($ps); 275 } 276 277 # 278 # prepare update git's index, based on what arch knows 279 # about the pset, resolve parents, etc 280 # 281 my$tree; 282 283 my$commitlog= `tla cat-archive-log -A $ps->{repo}$ps->{id}`; 284 die "Error in cat-archive-log:$!" if$?; 285 286 # parselog will git-add/rm files 287 # and generally prepare things for the commit 288 # NOTE: parselog will shell-quote filenames! 289 my ($sum,$msg,$add,$del,$mod,$ren) = parselog($commitlog); 290 my$logmessage= "$sum\n$msg"; 291 292 293 # imports don't give us good info 294 # on added files. Shame on them 295 if ($ps->{type} eq 'i' ||$ps->{type} eq 't') { 296 `find . -type f -print0 |grep-zv '^./$git_dir'| xargs -0-l100 git-update-index --add`; 297 `git-ls-files --deleted -z | xargs --no-run-if-empty -0-l100 git-update-index --remove`; 298 } 299 300 if (@$add) { 301 while (@$add) { 302 my@slice= splice(@$add, 0, 100); 303 my$slice= join(' ',@slice); 304 `git-update-index --add $slice`; 305die"Error in git-update-index --add:$!"if$?; 306} 307} 308if(@$del) { 309foreachmy$file(@$del) { 310unlink$fileor die"Problems deleting$file:$!"; 311} 312while(@$del) { 313my@slice=splice(@$del,0,100); 314my$slice=join(' ',@slice); 315`git-update-index --remove$slice`; 316 die "Error in git-update-index --remove:$!" if$?; 317 } 318 } 319 if (@$ren) { # renamed 320 if (@$ren% 2) { 321 die "Odd number of entries in rename!?"; 322 } 323 ; 324 while (@$ren) { 325 my$from= pop@$ren; 326 my$to= pop@$ren; 327 328 unless (-d dirname($to)) { 329 mkpath(dirname($to)); # will die on err 330 } 331 #print "moving$from$to"; 332 `mv $from $to`; 333die"Error renaming$from$to:$!"if$?; 334`git-update-index --remove$from`; 335 die "Error in git-update-index --remove:$!" if$?; 336 `git-update-index --add $to`; 337die"Error in git-update-index --add:$!"if$?; 338} 339 340} 341if(@$mod) {# must be _after_ renames 342while(@$mod) { 343my@slice=splice(@$mod,0,100); 344my$slice=join(' ',@slice); 345`git-update-index$slice`; 346 die "Error in git-update-index:$!" if$?; 347 } 348 } 349 350 # warn "errors when running git-update-index!$!"; 351$tree= `git-write-tree`; 352 die "cannot write tree$!" if$?; 353 chomp$tree; 354 355 356 # 357 # Who's your daddy? 358 # 359 my@par; 360 if ( -e "$git_dir/refs/heads/$ps->{branch}") { 361 if (open HEAD, "<$git_dir/refs/heads/$ps->{branch}") { 362 my$p= <HEAD>; 363 close HEAD; 364 chomp$p; 365 push@par, '-p',$p; 366 } else { 367 if ($ps->{type} eq 's') { 368 warn "Could not find the right head for the branch$ps->{branch}"; 369 } 370 } 371 } 372 373 if ($ps->{merges}) { 374 push@par, find_parents($ps); 375 } 376 my$par= join (' ',@par); 377 378 # 379 # Commit, tag and clean state 380 # 381$ENV{TZ} = 'GMT'; 382$ENV{GIT_AUTHOR_NAME} =$ps->{author}; 383$ENV{GIT_AUTHOR_EMAIL} =$ps->{email}; 384$ENV{GIT_AUTHOR_DATE} =$ps->{date}; 385$ENV{GIT_COMMITTER_NAME} =$ps->{author}; 386$ENV{GIT_COMMITTER_EMAIL} =$ps->{email}; 387$ENV{GIT_COMMITTER_DATE} =$ps->{date}; 388 389 my ($pid,$commit_rh,$commit_wh); 390$commit_rh= 'commit_rh'; 391$commit_wh= 'commit_wh'; 392 393$pid= open2(*READER, *WRITER, "git-commit-tree$tree$par") 394 or die$!; 395 print WRITER$logmessage; # write 396 close WRITER; 397 my$commitid= <READER>; # read 398 chomp$commitid; 399 close READER; 400 waitpid$pid,0; # close; 401 402 if (length$commitid!= 40) { 403 die "Something went wrong with the commit!$!$commitid"; 404 } 405 # 406 # Update the branch 407 # 408 open HEAD, ">$git_dir/refs/heads/$ps->{branch}"; 409 print HEAD$commitid; 410 close HEAD; 411 unlink ("$git_dir/HEAD"); 412 symlink("refs/heads/$ps->{branch}","$git_dir/HEAD"); 413 414 # tag accordingly 415 ptag($ps->{id},$commitid); # private tag 416 if ($opt_T||$ps->{type} eq 't' ||$ps->{type} eq 'i') { 417 tag($ps->{id},$commitid); 418 } 419 print " * Committed$ps->{id}\n"; 420 print " + tree$tree\n"; 421 print " + commit$commitid\n"; 422$opt_v&& print " + commit date is$ps->{date}\n"; 423$opt_v&& print " + parents:$par\n"; 424} 425 426sub branchname { 427 my$id= shift; 428$id=~ s#^.+?/##; 429 my@parts= split(m/--/,$id); 430 return join('--',@parts[0..1]); 431} 432 433sub apply_import { 434 my$ps= shift; 435 my$bname= branchname($ps->{id}); 436 437 `mkdir-p $tmp`; 438 439`tla get -s --no-pristine -A$ps->{repo}$ps->{id}$tmp/import`; 440 die "Cannot get import:$!" if$?; 441 `rsync -v --archive --delete--exclude '$git_dir'--exclude '.arch-ids'--exclude '{arch}'$tmp/import/*./`; 442 die "Cannot rsync import:$!" if$?; 443 444 `rm -fr $tmp/import`; 445die"Cannot remove tempdir:$!"if$?; 446 447 448return1; 449} 450 451sub apply_cset { 452my$ps=shift; 453 454`mkdir -p$tmp`; 455 456 # get the changeset 457 `tla get-changeset -A $ps->{repo}$ps->{id}$tmp/changeset`; 458die"Cannot get changeset:$!"if$?; 459 460# apply patches 461if(`find$tmp/changeset/patches-type f -name '*.patch'`) { 462# this can be sped up considerably by doing 463# (find | xargs cat) | patch 464# but that cna get mucked up by patches 465# with missing trailing newlines or the standard 466# 'missing newline' flag in the patch - possibly 467# produced with an old/buggy diff. 468# slow and safe, we invoke patch once per patchfile 469`find$tmp/changeset/patches-type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`; 470die"Problem applying patches!$!"if$?; 471} 472 473# apply changed binary files 474if(my@modified=`find$tmp/changeset/patches-type f -name '*.modified'`) { 475foreachmy$mod(@modified) { 476chomp$mod; 477my$orig=$mod; 478$orig=~s/\.modified$//;# lazy 479$orig=~s!^\Q$tmp\E/changeset/patches/!!; 480#print "rsync -p '$mod' '$orig'"; 481`rsync -p$mod./$orig`; 482 die "Problem applying binary changes!$!" if$?; 483 } 484 } 485 486 # bring in new files 487 `rsync --archive --exclude '$git_dir'--exclude '.arch-ids'--exclude '{arch}'$tmp/changeset/new-files-archive/* ./`; 488 489 # deleted files are hinted from the commitlog processing 490 491 `rm -fr $tmp/changeset`; 492} 493 494 495# =for reference 496# A log entry looks like 497# Revision: moodle-org--moodle--1.3.3--patch-15 498# Archive: arch-eduforge@catalyst.net.nz--2004 499# Creator: Penny Leach <penny@catalyst.net.nz> 500# Date: Wed May 25 14:15:34 NZST 2005 501# Standard-date: 2005-05-25 02:15:34 GMT 502# New-files: lang/de/.arch-ids/block_glossary_random.php.id 503# lang/de/.arch-ids/block_html.php.id 504# New-directories: lang/de/help/questionnaire 505# lang/de/help/questionnaire/.arch-ids 506# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id 507# db_sears.sql db/db_sears.sql 508# Removed-files: lang/be/docs/.arch-ids/release.html.id 509# lang/be/docs/.arch-ids/releaseold.html.id 510# Modified-files: admin/cron.php admin/delete.php 511# admin/editor.html backup/lib.php backup/restore.php 512# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15 513# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+) 514# Keywords: 515# 516# Updating yadda tadda tadda madda 517sub parselog { 518my$log=shift; 519#print $log; 520 521my(@add,@del,@mod,@ren,@kw,$sum,$msg); 522 523if($log=~m/(?:\n|^)New-files:(.*?)(?=\n\w)/s) { 524my$files=$1; 525@add=split(m/\s+/s,$files); 526} 527 528if($log=~m/(?:\n|^)Removed-files:(.*?)(?=\n\w)/s) { 529my$files=$1; 530@del=split(m/\s+/s,$files); 531} 532 533if($log=~m/(?:\n|^)Modified-files:(.*?)(?=\n\w)/s) { 534my$files=$1; 535@mod=split(m/\s+/s,$files); 536} 537 538if($log=~m/(?:\n|^)Renamed-files:(.*?)(?=\n\w)/s) { 539my$files=$1; 540@ren=split(m/\s+/s,$files); 541} 542 543$sum=''; 544if($log=~m/^Summary:(.+?)$/m) { 545$sum=$1; 546$sum=~s/^\s+//; 547$sum=~s/\s+$//; 548} 549 550$msg=''; 551if($log=~m/\n\n(.+)$/s) { 552$msg=$1; 553$msg=~s/^\s+//; 554$msg=~s/\s+$//; 555} 556 557 558# cleanup the arrays 559foreachmy$ref( (\@add, \@del, \@mod, \@ren) ) { 560my@tmp= (); 561while(my$t=pop@$ref) { 562next unlesslength($t); 563next if$t=~m!\{arch\}/!; 564next if$t=~m!\.arch-ids/!; 565next if$t=~m!\.arch-inventory$!; 566push(@tmp, shell_quote($t)); 567} 568@$ref=@tmp; 569} 570 571#print Dumper [$sum, $msg, \@add, \@del, \@mod, \@ren]; 572return($sum,$msg, \@add, \@del, \@mod, \@ren); 573} 574 575# write/read a tag 576sub tag { 577my($tag,$commit) =@_; 578$tag=~ s|/|--|g; 579$tag= shell_quote($tag); 580 581if($commit) { 582open(C,">$git_dir/refs/tags/$tag") 583or die"Cannot create tag$tag:$!\n"; 584print C "$commit\n" 585or die"Cannot write tag$tag:$!\n"; 586close(C) 587or die"Cannot write tag$tag:$!\n"; 588print" * Created tag '$tag' on '$commit'\n"if$opt_v; 589}else{# read 590open(C,"<$git_dir/refs/tags/$tag") 591or die"Cannot read tag$tag:$!\n"; 592$commit= <C>; 593chomp$commit; 594die"Error reading tag$tag:$!\n"unlesslength$commit==40; 595close(C) 596or die"Cannot read tag$tag:$!\n"; 597return$commit; 598} 599} 600 601# write/read a private tag 602# reads fail softly if the tag isn't there 603sub ptag { 604my($tag,$commit) =@_; 605$tag=~ s|/|--|g; 606$tag= shell_quote($tag); 607 608unless(-d "$git_dir/archimport/tags") { 609 mkpath("$git_dir/archimport/tags"); 610} 611 612if($commit) {# write 613open(C,">$git_dir/archimport/tags/$tag") 614or die"Cannot create tag$tag:$!\n"; 615print C "$commit\n" 616or die"Cannot write tag$tag:$!\n"; 617close(C) 618or die"Cannot write tag$tag:$!\n"; 619$rptags{$commit} =$tag 620unless$tag=~m/--base-0$/; 621}else{# read 622# if the tag isn't there, return 0 623unless( -s "$git_dir/archimport/tags/$tag") { 624return0; 625} 626open(C,"<$git_dir/archimport/tags/$tag") 627or die"Cannot read tag$tag:$!\n"; 628$commit= <C>; 629chomp$commit; 630die"Error reading tag$tag:$!\n"unlesslength$commit==40; 631close(C) 632or die"Cannot read tag$tag:$!\n"; 633unless(defined$rptags{$commit}) { 634$rptags{$commit} =$tag; 635} 636return$commit; 637} 638} 639 640sub find_parents { 641# 642# Identify what branches are merging into me 643# and whether we are fully merged 644# git-merge-base <headsha> <headsha> should tell 645# me what the base of the merge should be 646# 647my$ps=shift; 648 649my%branches;# holds an arrayref per branch 650# the arrayref contains a list of 651# merged patches between the base 652# of the merge and the current head 653 654my@parents;# parents found for this commit 655 656# simple loop to split the merges 657# per branch 658foreachmy$merge(@{$ps->{merges}}) { 659my$branch= branchname($merge); 660unless(defined$branches{$branch} ){ 661$branches{$branch} = []; 662} 663push@{$branches{$branch}},$merge; 664} 665 666# 667# foreach branch find a merge base and walk it to the 668# head where we are, collecting the merged patchsets that 669# Arch has recorded. Keep that in @have 670# Compare that with the commits on the other branch 671# between merge-base and the tip of the branch (@need) 672# and see if we have a series of consecutive patches 673# starting from the merge base. The tip of the series 674# of consecutive patches merged is our new parent for 675# that branch. 676# 677foreachmy$branch(keys%branches) { 678my$mergebase=`git-merge-base$branch$ps->{branch}`; 679die"Cannot find merge base for$branchand$ps->{branch}"if$?; 680chomp$mergebase; 681 682# now walk up to the mergepoint collecting what patches we have 683my$branchtip= git_rev_parse($ps->{branch}); 684my@ancestors=`git-rev-list --merge-order$branchtip^$mergebase`; 685 my%have; # collected merges this branch has 686 foreach my$merge(@{$ps->{merges}}) { 687$have{$merge} = 1; 688 } 689 my%ancestorshave; 690 foreach my$par(@ancestors) { 691$par= commitid2pset($par); 692 if (defined$par->{merges}) { 693 foreach my$merge(@{$par->{merges}}) { 694$ancestorshave{$merge}=1; 695 } 696 } 697 } 698 # print "++++ Merges in$ps->{id} are....\n"; 699 # my@have= sort keys%have; print Dumper(\@have); 700 701 # merge what we have with what ancestors have 702%have= (%have,%ancestorshave); 703 704 # see what the remote branch has - these are the merges we 705 # will want to have in a consecutive series from the mergebase 706 my$otherbranchtip= git_rev_parse($branch); 707 my@needraw= `git-rev-list --merge-order $otherbranchtip^$mergebase`; 708my@need; 709foreachmy$needps(@needraw) {# get the psets 710$needps= commitid2pset($needps); 711# git-rev-list will also 712# list commits merged in via earlier 713# merges. we are only interested in commits 714# from the branch we're looking at 715if($brancheq$needps->{branch}) { 716push@need,$needps->{id}; 717} 718} 719 720# print "++++ Merges from $branch we want are....\n"; 721# print Dumper(\@need); 722 723my$newparent; 724while(my$needed_commit=pop@need) { 725if($have{$needed_commit}) { 726$newparent=$needed_commit; 727}else{ 728last;# break out of the while 729} 730} 731if($newparent) { 732push@parents,$newparent; 733} 734 735 736}# end foreach branch 737 738# prune redundant parents 739my%parents; 740foreachmy$p(@parents) { 741$parents{$p} =1; 742} 743foreachmy$p(@parents) { 744next unlessexists$psets{$p}{merges}; 745next unlessref$psets{$p}{merges}; 746my@merges= @{$psets{$p}{merges}}; 747foreachmy$merge(@merges) { 748if($parents{$merge}) { 749delete$parents{$merge}; 750} 751} 752} 753@parents=keys%parents; 754@parents=map{" -p ". ptag($_) }@parents; 755return@parents; 756} 757 758sub git_rev_parse { 759my$name=shift; 760my$val=`git-rev-parse$name`; 761 die "Error: git-rev-parse$name" if$?; 762 chomp$val; 763 return$val; 764} 765 766# resolve a SHA1 to a known patchset 767sub commitid2pset { 768 my$commitid= shift; 769 chomp$commitid; 770 my$name=$rptags{$commitid} 771 || die "Cannot find reverse tag mapping for$commitid"; 772 # the keys in%rptagare slightly munged; unmunge 773 # reconvert the 3rd '--' sequence from the end 774 # into a slash 775$name= reverse$name; 776$name=~ s!^(.+?--.+?--.+?--.+?)--(.+)$!$1/$2!; 777$name= reverse$name; 778 my$ps=$psets{$name} 779 || (print Dumper(sort keys%psets)) && die "Cannot find patchset for$name"; 780 return$ps; 781}