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