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 ] [ -o ] [ -a ] [ -f ] [ -T ] 13 [ -D depth] [ -t tempdir ] <archive>/<branch> [ <archive>/<branch> ] 14 15Imports a project from one or more Arch repositories. It will follow branches 16and repositories within the namespaces defined by the <archive/branch> 17parameters suppplied. If it cannot find the remote branch a merge comes from 18it will just import it as a regular commit. If it can find it, it will mark it 19as a merge whenever possible. 20 21See man (1) git-archimport for more details. 22 23=head1 TODO 24 25 - create tag objects instead of ref tags 26 - audit shell-escaping of filenames 27 - hide our private tags somewhere smarter 28 - find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines 29 - sort and apply patches by graphing ancestry relations instead of just 30 relying in dates supplied in the changeset itself. 31 tla ancestry-graph -m could be helpful here... 32 33=head1 Devel tricks 34 35Add print in front of the shell commands invoked via backticks. 36 37=head1 Devel Notes 38 39There are several places where Arch and git terminology are intermixed 40and potentially confused. 41 42The notion of a "branch" in git is approximately equivalent to 43a "archive/category--branch--version" in Arch. Also, it should be noted 44that the "--branch" portion of "archive/category--branch--version" is really 45optional in Arch although not many people (nor tools!) seem to know this. 46This means that "archive/category--version" is also a valid "branch" 47in git terms. 48 49We always refer to Arch names by their fully qualified variant (which 50means the "archive" name is prefixed. 51 52For people unfamiliar with Arch, an "archive" is the term for "repository", 53and can contain multiple, unrelated branches. 54 55=cut 56 57use strict; 58use warnings; 59use Getopt::Std; 60use File::Temp qw(tempdir); 61use File::Path qw(mkpath rmtree); 62use File::Basename qw(basename dirname); 63use Data::Dumper qw/ Dumper /; 64use IPC::Open2; 65 66$SIG{'PIPE'}="IGNORE"; 67$ENV{'TZ'}="UTC"; 68 69my$git_dir=$ENV{"GIT_DIR"} ||".git"; 70$ENV{"GIT_DIR"} =$git_dir; 71my$ptag_dir="$git_dir/archimport/tags"; 72 73our($opt_h,$opt_f,$opt_v,$opt_T,$opt_t,$opt_D,$opt_a,$opt_o); 74 75sub usage() { 76print STDERR <<END; 77Usage: ${\basename$0} # fetch/update GIT from Arch 78 [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ] [ -D depth ] [ -t tempdir ] 79 repository/arch-branch [ repository/arch-branch] ... 80END 81exit(1); 82} 83 84getopts("fThvat:D:")or usage(); 85usage if$opt_h; 86 87@ARGV>=1or usage(); 88# $arch_branches: 89# values associated with keys: 90# =1 - Arch version / git 'branch' detected via abrowse on a limit 91# >1 - Arch version / git 'branch' of an auxilliary branch we've merged 92my%arch_branches=map{$_=>1}@ARGV; 93 94$ENV{'TMPDIR'} =$opt_tif$opt_t;# $ENV{TMPDIR} will affect tempdir() calls: 95my$tmp= tempdir('git-archimport-XXXXXX', TMPDIR =>1, CLEANUP =>1); 96$opt_v&&print"+ Using$tmpas temporary directory\n"; 97 98my%reachable= ();# Arch repositories we can access 99my%unreachable= ();# Arch repositories we can't access :< 100my@psets= ();# the collection 101my%psets= ();# the collection, by name 102my%stats= (# Track which strategy we used to import: 103 get_tag =>0, replay =>0, get_new =>0, get_delta =>0, 104 simple_changeset =>0, import_or_tag =>0 105); 106 107my%rptags= ();# my reverse private tags 108# to map a SHA1 to a commitid 109my$TLA=$ENV{'ARCH_CLIENT'} ||'tla'; 110 111sub do_abrowse { 112my$stage=shift; 113while(my($limit,$level) =each%arch_branches) { 114next unless$level==$stage; 115 116open ABROWSE,"$TLAabrowse -fkD --merges$limit|" 117or die"Problems with tla abrowse:$!"; 118 119my%ps= ();# the current one 120my$lastseen=''; 121 122while(<ABROWSE>) { 123chomp; 124 125# first record padded w 8 spaces 126if(s/^\s{8}\b//) { 127my($id,$type) =split(m/\s+/,$_,2); 128 129my%last_ps; 130# store the record we just captured 131if(%ps&& !exists$psets{$ps{id} }) { 132%last_ps=%ps;# break references 133push(@psets, \%last_ps); 134$psets{$last_ps{id} } = \%last_ps; 135} 136 137my$branch= extract_versionname($id); 138%ps= ( id =>$id, branch =>$branch); 139if(%last_ps&& ($last_ps{branch}eq$branch)) { 140$ps{parent_id} =$last_ps{id}; 141} 142 143$arch_branches{$branch} =1; 144$lastseen='id'; 145 146# deal with types (should work with baz or tla): 147if($type=~m/\(.*changeset\)/) { 148$ps{type} ='s'; 149}elsif($type=~/\(.*import\)/) { 150$ps{type} ='i'; 151}elsif($type=~m/\(tag.*?(\S+\@\S+).*?\)/) { 152$ps{type} ='t'; 153# read which revision we've tagged when we parse the log 154$ps{tag} =$1; 155}else{ 156warn"Unknown type$type"; 157} 158 159$arch_branches{$branch} =1; 160$lastseen='id'; 161}elsif(s/^\s{10}//) { 162# 10 leading spaces or more 163# indicate commit metadata 164 165# date 166if($lastseeneq'id'&&m/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)/){ 167$ps{date} =$1; 168$lastseen='date'; 169}elsif($_eq'merges in:') { 170$ps{merges} = []; 171$lastseen='merges'; 172}elsif($lastseeneq'merges'&&s/^\s{2}//) { 173my$id=$_; 174push(@{$ps{merges}},$id); 175 176# aggressive branch finding: 177if($opt_D) { 178my$branch= extract_versionname($id); 179my$repo= extract_reponame($branch); 180 181if(archive_reachable($repo) && 182!defined$arch_branches{$branch}) { 183$arch_branches{$branch} =$stage+1; 184} 185} 186}else{ 187warn"more metadata after merges!?:$_\n"unless/^\s*$/; 188} 189} 190} 191 192if(%ps&& !exists$psets{$ps{id} }) { 193my%temp=%ps;# break references 194if(@psets&&$psets[$#psets]{branch}eq$ps{branch}) { 195$temp{parent_id} =$psets[$#psets]{id}; 196} 197push(@psets, \%temp); 198$psets{$temp{id} } = \%temp; 199} 200 201close ABROWSE or die"$TLAabrowse failed on$limit\n"; 202} 203}# end foreach $root 204 205do_abrowse(1); 206my$depth=2; 207$opt_D||=0; 208while($depth<=$opt_D) { 209 do_abrowse($depth); 210$depth++; 211} 212 213## Order patches by time 214# FIXME see if we can find a more optimal way to do this by graphing 215# the ancestry data and walking it, that way we won't have to rely on 216# client-supplied dates 217@psets=sort{$a->{date}.$b->{id}cmp$b->{date}.$b->{id}}@psets; 218 219#print Dumper \@psets; 220 221## 222## TODO cleanup irrelevant patches 223## and put an initial import 224## or a full tag 225my$import=0; 226unless(-d $git_dir) {# initial import 227if($psets[0]{type}eq'i'||$psets[0]{type}eq't') { 228print"Starting import from$psets[0]{id}\n"; 229`git-init-db`; 230die$!if$?; 231$import=1; 232}else{ 233die"Need to start from an import or a tag -- cannot use$psets[0]{id}"; 234} 235}else{# progressing an import 236# load the rptags 237opendir(DIR,$ptag_dir) 238||die"can't opendir:$!"; 239while(my$file=readdir(DIR)) { 240# skip non-interesting-files 241next unless-f "$ptag_dir/$file"; 242 243# convert first '--' to '/' from old git-archimport to use 244# as an archivename/c--b--v private tag 245if($file!~m!,!) { 246my$oldfile=$file; 247$file=~s!--!,!; 248print STDERR "converting old tag$oldfileto$file\n"; 249rename("$ptag_dir/$oldfile","$ptag_dir/$file")or die$!; 250} 251my$sha= ptag($file); 252chomp$sha; 253$rptags{$sha} =$file; 254} 255closedir DIR; 256} 257 258# process patchsets 259# extract the Arch repository name (Arch "archive" in Arch-speak) 260sub extract_reponame { 261my$fq_cvbr=shift;# archivename/[[[[category]branch]version]revision] 262return(split(/\//,$fq_cvbr))[0]; 263} 264 265sub extract_versionname { 266my$name=shift; 267$name=~s/--(?:patch|version(?:fix)?|base)-\d+$//; 268return$name; 269} 270 271# convert a fully-qualified revision or version to a unique dirname: 272# normalperson@yhbt.net-05/mpd--uclinux--1--patch-2 273# becomes: normalperson@yhbt.net-05,mpd--uclinux--1 274# 275# the git notion of a branch is closer to 276# archive/category--branch--version than archive/category--branch, so we 277# use this to convert to git branch names. 278# Also, keep archive names but replace '/' with ',' since it won't require 279# subdirectories, and is safer than swapping '--' which could confuse 280# reverse-mapping when dealing with bastard branches that 281# are just archive/category--version (no --branch) 282sub tree_dirname { 283my$revision=shift; 284my$name= extract_versionname($revision); 285$name=~ s#/#,#; 286return$name; 287} 288 289# old versions of git-archimport just use the <category--branch> part: 290sub old_style_branchname { 291my$id=shift; 292my$ret= safe_pipe_capture($TLA,'parse-package-name','-p',$id); 293chomp$ret; 294return$ret; 295} 296 297*git_branchname =$opt_o? *old_style_branchname : *tree_dirname; 298 299sub process_patchset_accurate { 300my$ps=shift; 301 302# switch to that branch if we're not already in that branch: 303if(-e "$git_dir/refs/heads/$ps->{branch}") { 304system('git-checkout','-f',$ps->{branch}) ==0or die"$!$?\n"; 305 306# remove any old stuff that got leftover: 307my$rm= safe_pipe_capture('git-ls-files','--others','-z'); 308 rmtree(split(/\0/,$rm))if$rm; 309} 310 311# Apply the import/changeset/merge into the working tree 312my$dir= sync_to_ps($ps); 313# read the new log entry: 314my@commitlog= safe_pipe_capture($TLA,'cat-log','-d',$dir,$ps->{id}); 315die"Error in cat-log:$!"if$?; 316chomp@commitlog; 317 318# grab variables we want from the log, new fields get added to $ps: 319# (author, date, email, summary, message body ...) 320 parselog($ps, \@commitlog); 321 322if($ps->{id} =~/--base-0$/&&$ps->{id}ne$psets[0]{id}) { 323# this should work when importing continuations 324if($ps->{tag} && (my$branchpoint=eval{ ptag($ps->{tag}) })) { 325 326# find where we are supposed to branch from 327system('git-checkout','-f','-b',$ps->{branch}, 328$branchpoint) ==0or die"$!$?\n"; 329 330# remove any old stuff that got leftover: 331my$rm= safe_pipe_capture('git-ls-files','--others','-z'); 332 rmtree(split(/\0/,$rm))if$rm; 333 334# If we trust Arch with the fact that this is just 335# a tag, and it does not affect the state of the tree 336# then we just tag and move on 337 tag($ps->{id},$branchpoint); 338 ptag($ps->{id},$branchpoint); 339print" * Tagged$ps->{id} at$branchpoint\n"; 340return0; 341}else{ 342warn"Tagging from unknown id unsupported\n"if$ps->{tag}; 343} 344# allow multiple bases/imports here since Arch supports cherry-picks 345# from unrelated trees 346} 347 348# update the index with all the changes we got 349system('git-ls-files --others -z | '. 350'git-update-index --add -z --stdin') ==0or die"$!$?\n"; 351system('git-ls-files --deleted -z | '. 352'git-update-index --remove -z --stdin') ==0or die"$!$?\n"; 353system('git-ls-files -z | '. 354'git-update-index -z --stdin') ==0or die"$!$?\n"; 355return1; 356} 357 358# the native changeset processing strategy. This is very fast, but 359# does not handle permissions or any renames involving directories 360sub process_patchset_fast { 361my$ps=shift; 362# 363# create the branch if needed 364# 365if($ps->{type}eq'i'&& !$import) { 366die"Should not have more than one 'Initial import' per GIT import:$ps->{id}"; 367} 368 369unless($import) {# skip for import 370if( -e "$git_dir/refs/heads/$ps->{branch}") { 371# we know about this branch 372system('git-checkout',$ps->{branch}); 373}else{ 374# new branch! we need to verify a few things 375die"Branch on a non-tag!"unless$ps->{type}eq't'; 376my$branchpoint= ptag($ps->{tag}); 377die"Tagging from unknown id unsupported:$ps->{tag}" 378unless$branchpoint; 379 380# find where we are supposed to branch from 381system('git-checkout','-b',$ps->{branch},$branchpoint); 382 383# If we trust Arch with the fact that this is just 384# a tag, and it does not affect the state of the tree 385# then we just tag and move on 386 tag($ps->{id},$branchpoint); 387 ptag($ps->{id},$branchpoint); 388print" * Tagged$ps->{id} at$branchpoint\n"; 389return0; 390} 391die$!if$?; 392} 393 394# 395# Apply the import/changeset/merge into the working tree 396# 397if($ps->{type}eq'i'||$ps->{type}eq't') { 398 apply_import($ps)or die$!; 399$stats{import_or_tag}++; 400$import=0; 401}elsif($ps->{type}eq's') { 402 apply_cset($ps); 403$stats{simple_changeset}++; 404} 405 406# 407# prepare update git's index, based on what arch knows 408# about the pset, resolve parents, etc 409# 410 411my@commitlog= safe_pipe_capture($TLA,'cat-archive-log',$ps->{id}); 412die"Error in cat-archive-log:$!"if$?; 413 414 parselog($ps,\@commitlog); 415 416# imports don't give us good info 417# on added files. Shame on them 418if($ps->{type}eq'i'||$ps->{type}eq't') { 419system('git-ls-files --others -z | '. 420'git-update-index --add -z --stdin') ==0or die"$!$?\n"; 421system('git-ls-files --deleted -z | '. 422'git-update-index --remove -z --stdin') ==0or die"$!$?\n"; 423} 424 425# TODO: handle removed_directories and renamed_directories: 426 427if(my$add=$ps->{new_files}) { 428while(@$add) { 429my@slice=splice(@$add,0,100); 430system('git-update-index','--add','--',@slice) ==0or 431die"Error in git-update-index --add:$!$?\n"; 432} 433} 434 435if(my$del=$ps->{removed_files}) { 436unlink@$del; 437while(@$del) { 438my@slice=splice(@$del,0,100); 439system('git-update-index','--remove','--',@slice) ==0or 440die"Error in git-update-index --remove:$!$?\n"; 441} 442} 443 444if(my$ren=$ps->{renamed_files}) {# renamed 445if(@$ren%2) { 446die"Odd number of entries in rename!?"; 447} 448 449while(@$ren) { 450my$from=shift@$ren; 451my$to=shift@$ren; 452 453unless(-d dirname($to)) { 454 mkpath(dirname($to));# will die on err 455} 456# print "moving $from $to"; 457rename($from,$to)or die"Error renaming '$from' '$to':$!\n"; 458system('git-update-index','--remove','--',$from) ==0or 459die"Error in git-update-index --remove:$!$?\n"; 460system('git-update-index','--add','--',$to) ==0or 461die"Error in git-update-index --add:$!$?\n"; 462} 463} 464 465if(my$mod=$ps->{modified_files}) { 466while(@$mod) { 467my@slice=splice(@$mod,0,100); 468system('git-update-index','--',@slice) ==0or 469die"Error in git-update-index:$!$?\n"; 470} 471} 472return1;# we successfully applied the changeset 473} 474 475if($opt_f) { 476print"Will import patchsets using the fast strategy\n", 477"Renamed directories and permission changes will be missed\n"; 478*process_patchset = *process_patchset_fast; 479}else{ 480print"Using the default (accurate) import strategy.\n", 481"Things may be a bit slow\n"; 482*process_patchset = *process_patchset_accurate; 483} 484 485foreachmy$ps(@psets) { 486# process patchsets 487$ps->{branch} = git_branchname($ps->{id}); 488 489# 490# ensure we have a clean state 491# 492if(my$dirty=`git-diff-files`) { 493die"Unclean tree when about to process$ps->{id} ". 494" - did we fail to commit cleanly before?\n$dirty"; 495} 496die$!if$?; 497 498# 499# skip commits already in repo 500# 501if(ptag($ps->{id})) { 502$opt_v&&print" * Skipping already imported:$ps->{id}\n"; 503next; 504} 505 506print" * Starting to work on$ps->{id}\n"; 507 508 process_patchset($ps)ornext; 509 510# warn "errors when running git-update-index! $!"; 511my$tree=`git-write-tree`; 512die"cannot write tree$!"if$?; 513chomp$tree; 514 515# 516# Who's your daddy? 517# 518my@par; 519if( -e "$git_dir/refs/heads/$ps->{branch}") { 520if(open HEAD,"<","$git_dir/refs/heads/$ps->{branch}") { 521my$p= <HEAD>; 522close HEAD; 523chomp$p; 524push@par,'-p',$p; 525}else{ 526if($ps->{type}eq's') { 527warn"Could not find the right head for the branch$ps->{branch}"; 528} 529} 530} 531 532if($ps->{merges}) { 533push@par, find_parents($ps); 534} 535 536# 537# Commit, tag and clean state 538# 539$ENV{TZ} ='GMT'; 540$ENV{GIT_AUTHOR_NAME} =$ps->{author}; 541$ENV{GIT_AUTHOR_EMAIL} =$ps->{email}; 542$ENV{GIT_AUTHOR_DATE} =$ps->{date}; 543$ENV{GIT_COMMITTER_NAME} =$ps->{author}; 544$ENV{GIT_COMMITTER_EMAIL} =$ps->{email}; 545$ENV{GIT_COMMITTER_DATE} =$ps->{date}; 546 547my$pid= open2(*READER, *WRITER,'git-commit-tree',$tree,@par) 548or die$!; 549print WRITER $ps->{summary},"\n"; 550print WRITER $ps->{message},"\n"; 551 552# make it easy to backtrack and figure out which Arch revision this was: 553print WRITER 'git-archimport-id: ',$ps->{id},"\n"; 554 555close WRITER; 556my$commitid= <READER>;# read 557chomp$commitid; 558close READER; 559waitpid$pid,0;# close; 560 561if(length$commitid!=40) { 562die"Something went wrong with the commit!$!$commitid"; 563} 564# 565# Update the branch 566# 567open HEAD,">","$git_dir/refs/heads/$ps->{branch}"; 568print HEAD $commitid; 569close HEAD; 570system('git-update-ref','HEAD',"$ps->{branch}"); 571 572# tag accordingly 573 ptag($ps->{id},$commitid);# private tag 574if($opt_T||$ps->{type}eq't'||$ps->{type}eq'i') { 575 tag($ps->{id},$commitid); 576} 577print" * Committed$ps->{id}\n"; 578print" + tree$tree\n"; 579print" + commit$commitid\n"; 580$opt_v&&print" + commit date is$ps->{date}\n"; 581$opt_v&&print" + parents: ",join(' ',@par),"\n"; 582} 583 584if($opt_v) { 585foreach(sort keys%stats) { 586print"$_:$stats{$_}\n"; 587} 588} 589exit0; 590 591# used by the accurate strategy: 592sub sync_to_ps { 593my$ps=shift; 594my$tree_dir=$tmp.'/'.tree_dirname($ps->{id}); 595 596$opt_v&&print"sync_to_ps($ps->{id}) method: "; 597 598if(-d $tree_dir) { 599if($ps->{type}eq't') { 600$opt_v&&print"get (tag)\n"; 601# looks like a tag-only or (worse,) a mixed tags/changeset branch, 602# can't rely on replay to work correctly on these 603 rmtree($tree_dir); 604 safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir); 605$stats{get_tag}++; 606}else{ 607my$tree_id= arch_tree_id($tree_dir); 608if($ps->{parent_id} && ($ps->{parent_id}eq$tree_id)) { 609# the common case (hopefully) 610$opt_v&&print"replay\n"; 611 safe_pipe_capture($TLA,'replay','-d',$tree_dir,$ps->{id}); 612$stats{replay}++; 613}else{ 614# getting one tree is usually faster than getting two trees 615# and applying the delta ... 616 rmtree($tree_dir); 617$opt_v&&print"apply-delta\n"; 618 safe_pipe_capture($TLA,'get','--no-pristine', 619$ps->{id},$tree_dir); 620$stats{get_delta}++; 621} 622} 623}else{ 624# new branch work 625$opt_v&&print"get (new tree)\n"; 626 safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir); 627$stats{get_new}++; 628} 629 630# added -I flag to rsync since we're going to fast! AIEEEEE!!!! 631system('rsync','-aI','--delete','--exclude',$git_dir, 632# '--exclude','.arch-inventory', 633'--exclude','.arch-ids','--exclude','{arch}', 634'--exclude','+*','--exclude',',*', 635"$tree_dir/",'./') ==0or die"Cannot rsync$tree_dir:$!$?"; 636return$tree_dir; 637} 638 639sub apply_import { 640my$ps=shift; 641my$bname= git_branchname($ps->{id}); 642 643 mkpath($tmp); 644 645 safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import"); 646die"Cannot get import:$!"if$?; 647system('rsync','-aI','--delete','--exclude',$git_dir, 648'--exclude','.arch-ids','--exclude','{arch}', 649"$tmp/import/",'./'); 650die"Cannot rsync import:$!"if$?; 651 652 rmtree("$tmp/import"); 653die"Cannot remove tempdir:$!"if$?; 654 655 656return1; 657} 658 659sub apply_cset { 660my$ps=shift; 661 662 mkpath($tmp); 663 664# get the changeset 665 safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset"); 666die"Cannot get changeset:$!"if$?; 667 668# apply patches 669if(`find$tmp/changeset/patches-type f -name '*.patch'`) { 670# this can be sped up considerably by doing 671# (find | xargs cat) | patch 672# but that cna get mucked up by patches 673# with missing trailing newlines or the standard 674# 'missing newline' flag in the patch - possibly 675# produced with an old/buggy diff. 676# slow and safe, we invoke patch once per patchfile 677`find$tmp/changeset/patches-type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`; 678die"Problem applying patches!$!"if$?; 679} 680 681# apply changed binary files 682if(my@modified=`find$tmp/changeset/patches-type f -name '*.modified'`) { 683foreachmy$mod(@modified) { 684chomp$mod; 685my$orig=$mod; 686$orig=~s/\.modified$//;# lazy 687$orig=~s!^\Q$tmp\E/changeset/patches/!!; 688#print "rsync -p '$mod' '$orig'"; 689system('rsync','-p',$mod,"./$orig"); 690die"Problem applying binary changes!$!"if$?; 691} 692} 693 694# bring in new files 695system('rsync','-aI','--exclude',$git_dir, 696'--exclude','.arch-ids', 697'--exclude','{arch}', 698"$tmp/changeset/new-files-archive/",'./'); 699 700# deleted files are hinted from the commitlog processing 701 702 rmtree("$tmp/changeset"); 703} 704 705 706# =for reference 707# notes: *-files/-directories keys cannot have spaces, they're always 708# pika-escaped. Everything after the first newline 709# A log entry looks like: 710# Revision: moodle-org--moodle--1.3.3--patch-15 711# Archive: arch-eduforge@catalyst.net.nz--2004 712# Creator: Penny Leach <penny@catalyst.net.nz> 713# Date: Wed May 25 14:15:34 NZST 2005 714# Standard-date: 2005-05-25 02:15:34 GMT 715# New-files: lang/de/.arch-ids/block_glossary_random.php.id 716# lang/de/.arch-ids/block_html.php.id 717# New-directories: lang/de/help/questionnaire 718# lang/de/help/questionnaire/.arch-ids 719# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id 720# db_sears.sql db/db_sears.sql 721# Removed-files: lang/be/docs/.arch-ids/release.html.id 722# lang/be/docs/.arch-ids/releaseold.html.id 723# Modified-files: admin/cron.php admin/delete.php 724# admin/editor.html backup/lib.php backup/restore.php 725# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15 726# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+) 727# summary can be multiline with a leading space just like the above fields 728# Keywords: 729# 730# Updating yadda tadda tadda madda 731sub parselog { 732my($ps,$log) =@_; 733my$key=undef; 734 735# headers we want that contain filenames: 736my%want_headers= ( 737 new_files =>1, 738 modified_files =>1, 739 renamed_files =>1, 740 renamed_directories =>1, 741 removed_files =>1, 742 removed_directories =>1, 743); 744 745chomp(@$log); 746while($_=shift@$log) { 747if(/^Continuation-of:\s*(.*)/) { 748$ps->{tag} =$1; 749$key=undef; 750}elsif(/^Summary:\s*(.*)$/) { 751# summary can be multiline as long as it has a leading space 752$ps->{summary} = [$1]; 753$key='summary'; 754}elsif(/^Creator: (.*)\s*<([^\>]+)>/) { 755$ps->{author} =$1; 756$ps->{email} =$2; 757$key=undef; 758# any *-files or *-directories can be read here: 759}elsif(/^([A-Z][a-z\-]+):\s*(.*)$/) { 760my$val=$2; 761$key=lc$1; 762$key=~tr/-/_/;# too lazy to quote :P 763if($want_headers{$key}) { 764push@{$ps->{$key}},split(/\s+/,$val); 765}else{ 766$key=undef; 767} 768}elsif(/^$/) { 769last;# remainder of @$log that didn't get shifted off is message 770}elsif($key) { 771if(/^\s+(.*)$/) { 772if($keyeq'summary') { 773push@{$ps->{$key}},$1; 774}else{# files/directories: 775push@{$ps->{$key}},split(/\s+/,$1); 776} 777}else{ 778$key=undef; 779} 780} 781} 782 783# post-processing: 784$ps->{summary} =join("\n",@{$ps->{summary}})."\n"; 785$ps->{message} =join("\n",@$log); 786 787# skip Arch control files, unescape pika-escaped files 788foreachmy$k(keys%want_headers) { 789next unless(defined$ps->{$k}); 790my@tmp= (); 791foreachmy$t(@{$ps->{$k}}) { 792next unlesslength($t); 793next if$t=~m!\{arch\}/!; 794next if$t=~m!\.arch-ids/!; 795# should we skip this? 796next if$t=~m!\.arch-inventory$!; 797# tla cat-archive-log will give us filenames with spaces as file\(sp)name - why? 798# we can assume that any filename with \ indicates some pika escaping that we want to get rid of. 799if($t=~/\\/){ 800$t= (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0]; 801} 802push@tmp,$t; 803} 804$ps->{$k} = \@tmp; 805} 806} 807 808# write/read a tag 809sub tag { 810my($tag,$commit) =@_; 811 812if($opt_o) { 813$tag=~ s|/|--|g; 814}else{ 815# don't use subdirs for tags yet, it could screw up other porcelains 816$tag=~ s|/|,|g; 817} 818 819if($commit) { 820open(C,">","$git_dir/refs/tags/$tag") 821or die"Cannot create tag$tag:$!\n"; 822print C "$commit\n" 823or die"Cannot write tag$tag:$!\n"; 824close(C) 825or die"Cannot write tag$tag:$!\n"; 826print" * Created tag '$tag' on '$commit'\n"if$opt_v; 827}else{# read 828open(C,"<","$git_dir/refs/tags/$tag") 829or die"Cannot read tag$tag:$!\n"; 830$commit= <C>; 831chomp$commit; 832die"Error reading tag$tag:$!\n"unlesslength$commit==40; 833close(C) 834or die"Cannot read tag$tag:$!\n"; 835return$commit; 836} 837} 838 839# write/read a private tag 840# reads fail softly if the tag isn't there 841sub ptag { 842my($tag,$commit) =@_; 843 844# don't use subdirs for tags yet, it could screw up other porcelains 845$tag=~ s|/|,|g; 846 847my$tag_file="$ptag_dir/$tag"; 848my$tag_branch_dir= dirname($tag_file); 849 mkpath($tag_branch_dir)unless(-d $tag_branch_dir); 850 851if($commit) {# write 852open(C,">",$tag_file) 853or die"Cannot create tag$tag:$!\n"; 854print C "$commit\n" 855or die"Cannot write tag$tag:$!\n"; 856close(C) 857or die"Cannot write tag$tag:$!\n"; 858$rptags{$commit} =$tag 859unless$tag=~m/--base-0$/; 860}else{# read 861# if the tag isn't there, return 0 862unless( -s $tag_file) { 863return0; 864} 865open(C,"<",$tag_file) 866or die"Cannot read tag$tag:$!\n"; 867$commit= <C>; 868chomp$commit; 869die"Error reading tag$tag:$!\n"unlesslength$commit==40; 870close(C) 871or die"Cannot read tag$tag:$!\n"; 872unless(defined$rptags{$commit}) { 873$rptags{$commit} =$tag; 874} 875return$commit; 876} 877} 878 879sub find_parents { 880# 881# Identify what branches are merging into me 882# and whether we are fully merged 883# git-merge-base <headsha> <headsha> should tell 884# me what the base of the merge should be 885# 886my$ps=shift; 887 888my%branches;# holds an arrayref per branch 889# the arrayref contains a list of 890# merged patches between the base 891# of the merge and the current head 892 893my@parents;# parents found for this commit 894 895# simple loop to split the merges 896# per branch 897foreachmy$merge(@{$ps->{merges}}) { 898my$branch= git_branchname($merge); 899unless(defined$branches{$branch} ){ 900$branches{$branch} = []; 901} 902push@{$branches{$branch}},$merge; 903} 904 905# 906# foreach branch find a merge base and walk it to the 907# head where we are, collecting the merged patchsets that 908# Arch has recorded. Keep that in @have 909# Compare that with the commits on the other branch 910# between merge-base and the tip of the branch (@need) 911# and see if we have a series of consecutive patches 912# starting from the merge base. The tip of the series 913# of consecutive patches merged is our new parent for 914# that branch. 915# 916foreachmy$branch(keys%branches) { 917 918# check that we actually know about the branch 919next unless-e "$git_dir/refs/heads/$branch"; 920 921my$mergebase=`git-merge-base$branch$ps->{branch}`; 922if($?) { 923# Don't die here, Arch supports one-way cherry-picking 924# between branches with no common base (or any relationship 925# at all beforehand) 926warn"Cannot find merge base for$branchand$ps->{branch}"; 927next; 928} 929chomp$mergebase; 930 931# now walk up to the mergepoint collecting what patches we have 932my$branchtip= git_rev_parse($ps->{branch}); 933my@ancestors=`git-rev-list --merge-order$branchtip^$mergebase`; 934 my%have; # collected merges this branch has 935 foreach my$merge(@{$ps->{merges}}) { 936$have{$merge} = 1; 937 } 938 my%ancestorshave; 939 foreach my$par(@ancestors) { 940$par= commitid2pset($par); 941 if (defined$par->{merges}) { 942 foreach my$merge(@{$par->{merges}}) { 943$ancestorshave{$merge}=1; 944 } 945 } 946 } 947 # print "++++ Merges in$ps->{id} are....\n"; 948 # my@have= sort keys%have; print Dumper(\@have); 949 950 # merge what we have with what ancestors have 951%have= (%have,%ancestorshave); 952 953 # see what the remote branch has - these are the merges we 954 # will want to have in a consecutive series from the mergebase 955 my$otherbranchtip= git_rev_parse($branch); 956 my@needraw= `git-rev-list --merge-order $otherbranchtip^$mergebase`; 957my@need; 958foreachmy$needps(@needraw) {# get the psets 959$needps= commitid2pset($needps); 960# git-rev-list will also 961# list commits merged in via earlier 962# merges. we are only interested in commits 963# from the branch we're looking at 964if($brancheq$needps->{branch}) { 965push@need,$needps->{id}; 966} 967} 968 969# print "++++ Merges from $branch we want are....\n"; 970# print Dumper(\@need); 971 972my$newparent; 973while(my$needed_commit=pop@need) { 974if($have{$needed_commit}) { 975$newparent=$needed_commit; 976}else{ 977last;# break out of the while 978} 979} 980if($newparent) { 981push@parents,$newparent; 982} 983 984 985}# end foreach branch 986 987# prune redundant parents 988my%parents; 989foreachmy$p(@parents) { 990$parents{$p} =1; 991} 992foreachmy$p(@parents) { 993next unlessexists$psets{$p}{merges}; 994next unlessref$psets{$p}{merges}; 995my@merges= @{$psets{$p}{merges}}; 996foreachmy$merge(@merges) { 997if($parents{$merge}) { 998delete$parents{$merge}; 999}1000}1001}10021003@parents= ();1004foreach(keys%parents) {1005push@parents,'-p', ptag($_);1006}1007return@parents;1008}10091010sub git_rev_parse {1011my$name=shift;1012my$val=`git-rev-parse$name`;1013 die "Error: git-rev-parse$name" if$?;1014 chomp$val;1015 return$val;1016}10171018# resolve a SHA1 to a known patchset1019sub commitid2pset {1020 my$commitid= shift;1021 chomp$commitid;1022 my$name=$rptags{$commitid} 1023 || die "Cannot find reverse tag mapping for$commitid";1024$name=~ s|,|/|;1025 my$ps=$psets{$name} 1026 || (print Dumper(sort keys%psets)) && die "Cannot find patchset for$name";1027 return$ps;1028}102910301031# an alterative to `command` that allows input to be passed as an array1032# to work around shell problems with weird characters in arguments1033sub safe_pipe_capture {1034 my@output;1035 if (my$pid= open my$child, '-|') {1036@output= (<$child>);1037 close$childor die join(' ',@_).":$!$?";1038 } else {1039 exec(@_) or die "$!$?"; # exec() can fail the executable can't be found1040 }1041 return wantarray ?@output: join('',@output);1042}10431044# `tla logs -rf -d <dir> | head -n1` or `baz tree-id <dir>`1045sub arch_tree_id {1046 my$dir= shift;1047 chomp( my$ret= (safe_pipe_capture($TLA,'logs','-rf','-d',$dir))[0] );1048 return$ret;1049}10501051sub archive_reachable {1052 my$archive= shift;1053 return 1 if$reachable{$archive};1054 return 0 if$unreachable{$archive};10551056 if (system "$TLAwhereis-archive$archive>/dev/null") {1057 if ($opt_a&& (system($TLA,'register-archive',1058 "http://mirrors.sourcecontrol.net/$archive") == 0)) {1059$reachable{$archive} = 1;1060 return 1;1061 }1062 print STDERR "Archive is unreachable:$archive\n";1063$unreachable{$archive} = 1;1064 return 0;1065 } else {1066$reachable{$archive} = 1;1067 return 1;1068 }1069}1070