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