3 # This tool is copyright (c) 2005, Martin Langhoff.
4 # It is released under the Gnu Public License, version 2.
6 # The basic idea is to walk the output of tla abrowse,
7 # fetch the changesets and apply them.
12 git-archimport [ -h ] [ -v ] [ -T ] [ -t tempdir ] <archive>/<branch> [ <archive>/<branch> ]
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.
20 See man (1) git-archimport for more details.
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
31 Add print in front of the shell commands invoked via backticks.
35 There are several places where Arch and git terminology are intermixed
36 and potentially confused.
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"
45 We always refer to Arch names by their fully qualified variant (which
46 means the "archive" name is prefixed.
48 For people unfamiliar with Arch, an "archive" is the term for "repository",
49 and can contain multiple, unrelated branches.
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 /;
62 $SIG{'PIPE'}="IGNORE";
65 my $git_dir = $ENV{"GIT_DIR"} || ".git";
66 $ENV{"GIT_DIR"} = $git_dir;
67 my $ptag_dir = "$git_dir/archimport/tags";
69 our($opt_h,$opt_v,$opt_T,$opt_t,$opt_D,$opt_a,$opt_o);
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] ...
80 getopts("Thvat:D:") or usage();
83 @ARGV >= 1 or usage();
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;
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";
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
99 my %rptags = (); # my reverse private tags
100 # to map a SHA1 to a commitid
101 my $TLA = $ENV{'ARCH_CLIENT'} || 'tla';
105 while (my ($limit, $level) = each %arch_branches) {
106 next unless $level == $stage;
108 open ABROWSE, "$TLA abrowse -fkD --merges $limit |"
109 or die "Problems with tla abrowse: $!";
111 my %ps = (); # the current one
117 # first record padded w 8 spaces
119 my ($id, $type) = split(m/\s+/, $_, 2);
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;
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};
135 $arch_branches{$branch} = 1;
138 # deal with types (should work with baz or tla):
139 if ($type =~ m/\(.*changeset\)/) {
141 } elsif ($type =~ /\(.*import\)/) {
143 } elsif ($type =~ m/\(tag.*?(\S+\@\S+).*?\)/) {
145 # read which revision we've tagged when we parse the log
148 warn "Unknown type $type";
151 $arch_branches{$branch} = 1;
153 } elsif (s/^\s{10}//) {
154 # 10 leading spaces or more
155 # indicate commit metadata
158 if ($lastseen eq 'id' && m/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)/){
161 } elsif ($_ eq 'merges in:') {
163 $lastseen = 'merges';
164 } elsif ($lastseen eq 'merges' && s/^\s{2}//) {
166 push (@{$ps{merges}}, $id);
168 # aggressive branch finding:
170 my $branch = extract_versionname($id);
171 my $repo = extract_reponame($branch);
173 if (archive_reachable($repo) &&
174 !defined $arch_branches{$branch}) {
175 $arch_branches{$branch} = $stage + 1;
179 warn "more metadata after merges!?: $_\n" unless /^\s*$/;
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};
189 push (@psets, \%temp);
190 $psets{ $temp{id} } = \%temp;
193 close ABROWSE or die "$TLA abrowse failed on $limit\n";
195 } # end foreach $root
200 while ($depth <= $opt_D) {
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;
211 #print Dumper \@psets;
214 ## TODO cleanup irrelevant patches
215 ## and put an initial import
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";
225 die "Need to start from an import or a tag -- cannot use $psets[0]{id}";
227 } else { # progressing an import
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";
235 # convert first '--' to '/' from old git-archimport to use
236 # as an archivename/c--b--v private tag
240 print STDERR "converting old tag $oldfile to $file\n";
241 rename("$ptag_dir/$oldfile", "$ptag_dir/$file") or die $!;
243 my $sha = ptag($file);
245 $rptags{$sha} = $file;
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];
257 sub extract_versionname {
259 $name =~ s/--(?:patch|version(?:fix)?|base)-\d+$//;
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
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)
275 my $revision = shift;
276 my $name = extract_versionname($revision);
281 # old versions of git-archimport just use the <category--branch> part:
282 sub old_style_branchname {
284 my $ret = safe_pipe_capture($TLA,'parse-package-name','-p',$id);
289 *git_branchname = $opt_o ? *old_style_branchname : *tree_dirname;
292 foreach my $ps (@psets) {
293 $ps->{branch} = git_branchname($ps->{id});
296 # ensure we have a clean state
298 if (`git-diff-files`) {
299 die "Unclean tree when about to process $ps->{id} " .
300 " - did we fail to commit cleanly before?";
305 # skip commits already in repo
307 if (ptag($ps->{id})) {
308 $opt_v && print " * Skipping already imported: $ps->{id}\n";
312 print " * Starting to work on $ps->{id}\n";
315 # create the branch if needed
317 if ($ps->{type} eq 'i' && !$import) {
318 die "Should not have more than one 'Initial import' per GIT import: $ps->{id}";
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});
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}"
332 # find where we are supposed to branch from
333 system('git-checkout','-b',$ps->{branch},$branchpoint);
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";
347 # Apply the import/changeset/merge into the working tree
349 if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
350 apply_import($ps) or die $!;
352 } elsif ($ps->{type} eq 's') {
357 # prepare update git's index, based on what arch knows
358 # about the pset, resolve parents, etc
362 my @commitlog = safe_pipe_capture($TLA,'cat-archive-log',$ps->{id});
363 die "Error in cat-archive-log: $!" if $?;
365 parselog($ps,\@commitlog);
367 # imports don't give us good info
368 # on added files. Shame on them
369 if ($ps->{type} eq 'i' || $ps->{type} eq 't') {
370 system('git-ls-files --others -z | '.
371 'git-update-index --add -z --stdin') == 0 or die "$! $?\n";
372 system('git-ls-files --deleted -z | '.
373 'git-update-index --remove -z --stdin') == 0 or die "$! $?\n";
376 # TODO: handle removed_directories and renamed_directories:
378 if (my $add = $ps->{new_files}) {
380 my @slice = splice(@$add, 0, 100);
381 system('git-update-index','--add','--',@slice) == 0 or
382 die "Error in git-update-index --add: $! $?\n";
386 if (my $del = $ps->{removed_files}) {
389 my @slice = splice(@$del, 0, 100);
390 system('git-update-index','--remove','--',@slice) == 0 or
391 die "Error in git-update-index --remove: $! $?\n";
395 if (my $ren = $ps->{renamed_files}) { # renamed
397 die "Odd number of entries in rename!?";
401 my $from = shift @$ren;
402 my $to = shift @$ren;
404 unless (-d dirname($to)) {
405 mkpath(dirname($to)); # will die on err
407 print "moving $from $to";
408 rename($from, $to) or die "Error renaming '$from' '$to': $!\n";
409 system('git-update-index','--remove','--',$from) == 0 or
410 die "Error in git-update-index --remove: $! $?\n";
411 system('git-update-index','--add','--',$to) == 0 or
412 die "Error in git-update-index --add: $! $?\n";
417 if (my $mod = $ps->{modified_files}) {
419 my @slice = splice(@$mod, 0, 100);
420 system('git-update-index','--',@slice) == 0 or
421 die "Error in git-update-index: $! $?\n";
425 # warn "errors when running git-update-index! $!";
426 $tree = `git-write-tree`;
427 die "cannot write tree $!" if $?;
434 if ( -e "$git_dir/refs/heads/$ps->{branch}") {
435 if (open HEAD, "<","$git_dir/refs/heads/$ps->{branch}") {
441 if ($ps->{type} eq 's') {
442 warn "Could not find the right head for the branch $ps->{branch}";
448 push @par, find_parents($ps);
452 # Commit, tag and clean state
455 $ENV{GIT_AUTHOR_NAME} = $ps->{author};
456 $ENV{GIT_AUTHOR_EMAIL} = $ps->{email};
457 $ENV{GIT_AUTHOR_DATE} = $ps->{date};
458 $ENV{GIT_COMMITTER_NAME} = $ps->{author};
459 $ENV{GIT_COMMITTER_EMAIL} = $ps->{email};
460 $ENV{GIT_COMMITTER_DATE} = $ps->{date};
462 my $pid = open2(*READER, *WRITER,'git-commit-tree',$tree,@par)
464 print WRITER $ps->{summary},"\n";
465 print WRITER $ps->{message},"\n";
467 # make it easy to backtrack and figure out which Arch revision this was:
468 print WRITER 'git-archimport-id: ',$ps->{id},"\n";
471 my $commitid = <READER>; # read
474 waitpid $pid,0; # close;
476 if (length $commitid != 40) {
477 die "Something went wrong with the commit! $! $commitid";
482 open HEAD, ">","$git_dir/refs/heads/$ps->{branch}";
483 print HEAD $commitid;
485 system('git-update-ref', 'HEAD', "$ps->{branch}");
488 ptag($ps->{id}, $commitid); # private tag
489 if ($opt_T || $ps->{type} eq 't' || $ps->{type} eq 'i') {
490 tag($ps->{id}, $commitid);
492 print " * Committed $ps->{id}\n";
493 print " + tree $tree\n";
494 print " + commit $commitid\n";
495 $opt_v && print " + commit date is $ps->{date} \n";
496 $opt_v && print " + parents: ",join(' ',@par),"\n";
501 my $bname = git_branchname($ps->{id});
505 safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import");
506 die "Cannot get import: $!" if $?;
507 system('rsync','-aI','--delete', '--exclude',$git_dir,
508 '--exclude','.arch-ids','--exclude','{arch}',
509 "$tmp/import/", './');
510 die "Cannot rsync import:$!" if $?;
512 rmtree("$tmp/import");
513 die "Cannot remove tempdir: $!" if $?;
525 safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset");
526 die "Cannot get changeset: $!" if $?;
529 if (`find $tmp/changeset/patches -type f -name '*.patch'`) {
530 # this can be sped up considerably by doing
531 # (find | xargs cat) | patch
532 # but that cna get mucked up by patches
533 # with missing trailing newlines or the standard
534 # 'missing newline' flag in the patch - possibly
535 # produced with an old/buggy diff.
536 # slow and safe, we invoke patch once per patchfile
537 `find $tmp/changeset/patches -type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`;
538 die "Problem applying patches! $!" if $?;
541 # apply changed binary files
542 if (my @modified = `find $tmp/changeset/patches -type f -name '*.modified'`) {
543 foreach my $mod (@modified) {
546 $orig =~ s/\.modified$//; # lazy
547 $orig =~ s!^\Q$tmp\E/changeset/patches/!!;
548 #print "rsync -p '$mod' '$orig'";
549 system('rsync','-p',$mod,"./$orig");
550 die "Problem applying binary changes! $!" if $?;
555 system('rsync','-aI','--exclude',$git_dir,
556 '--exclude','.arch-ids',
557 '--exclude', '{arch}',
558 "$tmp/changeset/new-files-archive/",'./');
560 # deleted files are hinted from the commitlog processing
562 rmtree("$tmp/changeset");
567 # notes: *-files/-directories keys cannot have spaces, they're always
568 # pika-escaped. Everything after the first newline
569 # A log entry looks like:
570 # Revision: moodle-org--moodle--1.3.3--patch-15
571 # Archive: arch-eduforge@catalyst.net.nz--2004
572 # Creator: Penny Leach <penny@catalyst.net.nz>
573 # Date: Wed May 25 14:15:34 NZST 2005
574 # Standard-date: 2005-05-25 02:15:34 GMT
575 # New-files: lang/de/.arch-ids/block_glossary_random.php.id
576 # lang/de/.arch-ids/block_html.php.id
577 # New-directories: lang/de/help/questionnaire
578 # lang/de/help/questionnaire/.arch-ids
579 # Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id
580 # db_sears.sql db/db_sears.sql
581 # Removed-files: lang/be/docs/.arch-ids/release.html.id
582 # lang/be/docs/.arch-ids/releaseold.html.id
583 # Modified-files: admin/cron.php admin/delete.php
584 # admin/editor.html backup/lib.php backup/restore.php
585 # New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15
586 # Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+)
587 # summary can be multiline with a leading space just like the above fields
590 # Updating yadda tadda tadda madda
595 # headers we want that contain filenames:
600 renamed_directories => 1,
602 removed_directories => 1,
606 while ($_ = shift @$log) {
607 if (/^Continuation-of:\s*(.*)/) {
610 } elsif (/^Summary:\s*(.*)$/ ) {
611 # summary can be multiline as long as it has a leading space
612 $ps->{summary} = [ $1 ];
614 } elsif (/^Creator: (.*)\s*<([^\>]+)>/) {
618 # any *-files or *-directories can be read here:
619 } elsif (/^([A-Z][a-z\-]+):\s*(.*)$/) {
622 $key =~ tr/-/_/; # too lazy to quote :P
623 if ($want_headers{$key}) {
624 push @{$ps->{$key}}, split(/\s+/, $val);
629 last; # remainder of @$log that didn't get shifted off is message
632 if ($key eq 'summary') {
633 push @{$ps->{$key}}, $1;
634 } else { # files/directories:
635 push @{$ps->{$key}}, split(/\s+/, $1);
644 $ps->{summary} = join("\n",@{$ps->{summary}})."\n";
645 $ps->{message} = join("\n",@$log);
647 # skip Arch control files, unescape pika-escaped files
648 foreach my $k (keys %want_headers) {
649 next unless (defined $ps->{$k});
651 foreach my $t (@{$ps->{$k}}) {
652 next unless length ($t);
653 next if $t =~ m!\{arch\}/!;
654 next if $t =~ m!\.arch-ids/!;
655 # should we skip this?
656 next if $t =~ m!\.arch-inventory$!;
657 # tla cat-archive-log will give us filenames with spaces as file\(sp)name - why?
658 # we can assume that any filename with \ indicates some pika escaping that we want to get rid of.
660 $t = (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0];
664 $ps->{$k} = \@tmp if scalar @tmp;
670 my ($tag, $commit) = @_;
675 # don't use subdirs for tags yet, it could screw up other porcelains
680 open(C,">","$git_dir/refs/tags/$tag")
681 or die "Cannot create tag $tag: $!\n";
683 or die "Cannot write tag $tag: $!\n";
685 or die "Cannot write tag $tag: $!\n";
686 print " * Created tag '$tag' on '$commit'\n" if $opt_v;
688 open(C,"<","$git_dir/refs/tags/$tag")
689 or die "Cannot read tag $tag: $!\n";
692 die "Error reading tag $tag: $!\n" unless length $commit == 40;
694 or die "Cannot read tag $tag: $!\n";
699 # write/read a private tag
700 # reads fail softly if the tag isn't there
702 my ($tag, $commit) = @_;
704 # don't use subdirs for tags yet, it could screw up other porcelains
707 my $tag_file = "$ptag_dir/$tag";
708 my $tag_branch_dir = dirname($tag_file);
709 mkpath($tag_branch_dir) unless (-d $tag_branch_dir);
711 if ($commit) { # write
712 open(C,">",$tag_file)
713 or die "Cannot create tag $tag: $!\n";
715 or die "Cannot write tag $tag: $!\n";
717 or die "Cannot write tag $tag: $!\n";
718 $rptags{$commit} = $tag
719 unless $tag =~ m/--base-0$/;
721 # if the tag isn't there, return 0
722 unless ( -s $tag_file) {
725 open(C,"<",$tag_file)
726 or die "Cannot read tag $tag: $!\n";
729 die "Error reading tag $tag: $!\n" unless length $commit == 40;
731 or die "Cannot read tag $tag: $!\n";
732 unless (defined $rptags{$commit}) {
733 $rptags{$commit} = $tag;
741 # Identify what branches are merging into me
742 # and whether we are fully merged
743 # git-merge-base <headsha> <headsha> should tell
744 # me what the base of the merge should be
748 my %branches; # holds an arrayref per branch
749 # the arrayref contains a list of
750 # merged patches between the base
751 # of the merge and the current head
753 my @parents; # parents found for this commit
755 # simple loop to split the merges
757 foreach my $merge (@{$ps->{merges}}) {
758 my $branch = git_branchname($merge);
759 unless (defined $branches{$branch} ){
760 $branches{$branch} = [];
762 push @{$branches{$branch}}, $merge;
766 # foreach branch find a merge base and walk it to the
767 # head where we are, collecting the merged patchsets that
768 # Arch has recorded. Keep that in @have
769 # Compare that with the commits on the other branch
770 # between merge-base and the tip of the branch (@need)
771 # and see if we have a series of consecutive patches
772 # starting from the merge base. The tip of the series
773 # of consecutive patches merged is our new parent for
776 foreach my $branch (keys %branches) {
778 # check that we actually know about the branch
779 next unless -e "$git_dir/refs/heads/$branch";
781 my $mergebase = `git-merge-base $branch $ps->{branch}`;
783 # Don't die here, Arch supports one-way cherry-picking
784 # between branches with no common base (or any relationship
786 warn "Cannot find merge base for $branch and $ps->{branch}";
791 # now walk up to the mergepoint collecting what patches we have
792 my $branchtip = git_rev_parse($ps->{branch});
793 my @ancestors = `git-rev-list --merge-order $branchtip ^$mergebase`;
794 my %have; # collected merges this branch has
795 foreach my $merge (@{$ps->{merges}}) {
799 foreach my $par (@ancestors) {
800 $par = commitid2pset($par);
801 if (defined $par->{merges}) {
802 foreach my $merge (@{$par->{merges}}) {
803 $ancestorshave{$merge}=1;
807 # print "++++ Merges in $ps->{id} are....\n";
808 # my @have = sort keys %have; print Dumper(\@have);
810 # merge what we have with what ancestors have
811 %have = (%have, %ancestorshave);
813 # see what the remote branch has - these are the merges we
814 # will want to have in a consecutive series from the mergebase
815 my $otherbranchtip = git_rev_parse($branch);
816 my @needraw = `git-rev-list --merge-order $otherbranchtip ^$mergebase`;
818 foreach my $needps (@needraw) { # get the psets
819 $needps = commitid2pset($needps);
820 # git-rev-list will also
821 # list commits merged in via earlier
822 # merges. we are only interested in commits
823 # from the branch we're looking at
824 if ($branch eq $needps->{branch}) {
825 push @need, $needps->{id};
829 # print "++++ Merges from $branch we want are....\n";
830 # print Dumper(\@need);
833 while (my $needed_commit = pop @need) {
834 if ($have{$needed_commit}) {
835 $newparent = $needed_commit;
837 last; # break out of the while
841 push @parents, $newparent;
845 } # end foreach branch
847 # prune redundant parents
849 foreach my $p (@parents) {
852 foreach my $p (@parents) {
853 next unless exists $psets{$p}{merges};
854 next unless ref $psets{$p}{merges};
855 my @merges = @{$psets{$p}{merges}};
856 foreach my $merge (@merges) {
857 if ($parents{$merge}) {
858 delete $parents{$merge};
864 foreach (keys %parents) {
865 push @parents, '-p', ptag($_);
872 my $val = `git-rev-parse $name`;
873 die "Error: git-rev-parse $name" if $?;
878 # resolve a SHA1 to a known patchset
880 my $commitid = shift;
882 my $name = $rptags{$commitid}
883 || die "Cannot find reverse tag mapping for $commitid";
885 my $ps = $psets{$name}
886 || (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name";
891 # an alterative to `command` that allows input to be passed as an array
892 # to work around shell problems with weird characters in arguments
893 sub safe_pipe_capture {
895 if (my $pid = open my $child, '-|') {
896 @output = (<$child>);
897 close $child or die join(' ',@_).": $! $?";
899 exec(@_) or die $?; # exec() can fail the executable can't be found
901 return wantarray ? @output : join('',@output);
904 # `tla logs -rf -d <dir> | head -n1` or `baz tree-id <dir>`
907 chomp( my $ret = (safe_pipe_capture($TLA,'logs','-rf','-d',$dir))[0] );
911 sub archive_reachable {
913 return 1 if $reachable{$archive};
914 return 0 if $unreachable{$archive};
916 if (system "$TLA whereis-archive $archive >/dev/null") {
917 if ($opt_a && (system($TLA,'register-archive',
918 "http://mirrors.sourcecontrol.net/$archive") == 0)) {
919 $reachable{$archive} = 1;
922 print STDERR "Archive is unreachable: $archive\n";
923 $unreachable{$archive} = 1;
926 $reachable{$archive} = 1;