+ return ($ret, $output ? 1 : 0);
+}
+
+sub action {
+ my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
+ my $fulldir=fulldir($topdir, $subdir);
+ my $checkout_dir;
+
+ $ENV{MR_CONFIG}=$configfiles{$topdir};
+ my $is_checkout=($action eq 'checkout');
+ my $is_update=($action =~ /update/);
+
+ ($ENV{MR_REPO}=$dir) =~ s!/$!!;
+ $ENV{MR_ACTION}=$action;
+
+ foreach my $testname ("skip", "deleted") {
+ next if $force && $testname eq "skip";
+
+ my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
+
+ if (defined $testcommand) {
+ my $ret=runsh "$testname test", $topdir, $subdir,
+ $testcommand, [$action],
+ sub { system(shift()) };
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $action: interrupted\n";
+ return ABORT;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
+ return ABORT;
+ }
+ }
+ if ($ret >> 8 == 0) {
+ if ($testname eq "deleted") {
+ if (-d $dir) {
+ print STDERR "mr error: $dir should be deleted yet still exists\n";
+ return FAILED;
+ }
+ }
+ print "mr $action: skip $dir skipped\n" if $verbose;
+ return SKIPPED;
+ }
+ }
+ }
+
+ if ($is_checkout) {
+ $checkout_dir=$dir;
+ if (! $force_checkout) {
+ if (-d $dir) {
+ print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
+ return SKIPPED;
+ }
+
+ $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
+ }
+ }
+ elsif ($is_update) {
+ if (! -d $dir) {
+ return action("checkout", $dir, $topdir, $subdir);
+ }
+ }
+
+ my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
+
+ if ($is_checkout && ! -d $dir) {
+ print "mr $action: creating parent directory $dir\n" if $verbose;
+ system("mkdir", "-p", $dir);
+ }
+
+ if (! $no_chdir && ! chdir($dir)) {
+ print STDERR "mr $action: failed to chdir to $dir: $!\n";
+ return FAILED;
+ }
+ elsif (! defined $command) {
+ my $vcs=vcs_test(@_);
+ if (! defined $vcs) {
+ print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
+ return FAILED;
+ }
+ else {
+ print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n" unless $minimal;
+ return SKIPPED;
+ }
+ }
+ else {
+ my $actionmsg;
+ if (! $no_chdir) {
+ $actionmsg="mr $action: $fulldir";
+ }
+ else {
+ my $s=$directory;
+ $s=~s/^\Q$fulldir\E\/?//;
+ $actionmsg="mr $action: $fulldir (in subdir $s)";
+ }
+ print "$actionmsg\n" unless $quiet || $minimal;
+
+ my ($hookret, $hook_out)=hook("pre_$action", $topdir, $subdir);
+ return $hookret if $hookret != OK;
+
+ my ($ret, $out)=runsh $action, $topdir, $subdir,
+ $command, \@ARGV, sub {
+ my $sh=shift;
+ if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+ return terminal_friendly_spawn($actionmsg, $sh, $quiet, $minimal);
+ }
+ else {
+ system($sh);
+ }
+ };
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $action: interrupted\n";
+ return ABORT;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: received signal ".($? & 127)."\n";
+ return ABORT;
+ }
+ print STDERR "mr $action: failed ($ret)\n" if $verbose;
+ if ($ret >> 8 != 0) {
+ print STDERR "mr $action: command failed\n";
+ if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
+ # recreate original command line to
+ # remember, and avoid recursing
+ my @orig=@ARGV;
+ @ARGV=('-n', $action, @orig);
+ action("remember", $dir, $topdir, $subdir);
+ @ARGV=@orig;
+ }
+ }
+ elsif ($ret != 0) {
+ print STDERR "mr $action: command died ($ret)\n";
+ }
+ return FAILED;
+ }
+ else {
+ if ($is_checkout && ! -d $dir) {
+ print STDERR "mr $action: $dir missing after checkout\n";;
+ return FAILED;
+ }
+
+ my ($ret, $hook_out)=hook("post_$action", $topdir, $subdir);
+ return $ret if $ret != OK;
+
+ if ($is_checkout || $is_update) {
+ if ($is_checkout && ! $no_chdir) {
+ if (! chdir($checkout_dir)) {
+ print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
+ return FAILED;
+ }
+ }
+ my ($ret, $hook_out)=hook("fixups", $topdir, $subdir);
+ return $ret if $ret != OK;
+ }
+
+ return (OK, $out || $hook_out);
+ }
+ }
+}
+
+sub hook {
+ my ($hook, $topdir, $subdir) = @_;
+
+ my $command=$config{$topdir}{$subdir}{$hook};
+ return OK unless defined $command;
+ my ($ret,$out)=runsh $hook, $topdir, $subdir, $command, [], sub {
+ my $sh=shift;
+ if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+ return terminal_friendly_spawn(undef, $sh, $quiet, $minimal);
+ }
+ else {
+ system($sh);
+ }
+ };
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $hook: interrupted\n";
+ return ABORT;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $hook: received signal ".($? & 127)."\n";
+ return ABORT;
+ }
+ else {
+ return FAILED;
+ }
+ }
+
+ return (OK, $out);
+}
+
+# run actions on multiple repos, in parallel
+sub mrs {
+ my $action=shift;
+ my @repos=@_;
+
+ $| = 1;
+ my @active;
+ my @fhs;
+ my @out;
+ my $running=0;
+ while (@fhs or @repos) {
+ while ((!$jobs || $running < $jobs) && @repos) {
+ $running++;
+ my $repo = shift @repos;
+ pipe(my $outfh, CHILD_STDOUT);
+ pipe(my $errfh, CHILD_STDERR);
+ my $pid;
+ unless ($pid = fork) {
+ die "mr $action: cannot fork: $!" unless defined $pid;
+ open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
+ open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
+ close CHILD_STDOUT;
+ close CHILD_STDERR;
+ close $outfh;
+ close $errfh;
+ exit +(action($action, @$repo))[0];
+ }
+ close CHILD_STDOUT;
+ close CHILD_STDERR;
+ push @active, [$pid, $repo];
+ push @fhs, [$outfh, $errfh];
+ push @out, ['', ''];
+ }
+ my ($rin, $rout) = ('','');
+ my $nfound;
+ foreach my $fh (@fhs) {
+ next unless defined $fh;
+ vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
+ vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
+ }
+ $nfound = select($rout=$rin, undef, undef, 1);
+ foreach my $channel (0, 1) {
+ foreach my $i (0..$#fhs) {
+ next unless defined $fhs[$i];
+ my $fh = $fhs[$i][$channel];
+ next unless defined $fh;
+ if (vec($rout, fileno($fh), 1) == 1) {
+ my $r = '';
+ if (sysread($fh, $r, 1024) == 0) {
+ close($fh);
+ $fhs[$i][$channel] = undef;
+ if (! defined $fhs[$i][0] &&
+ ! defined $fhs[$i][1]) {
+ waitpid($active[$i][0], 0);
+ print STDOUT $out[$i][0];
+ print STDERR $out[$i][1];
+ record($active[$i][1], $? >> 8, $out[$i][0] || $out[$i][1]);
+ splice(@fhs, $i, 1);
+ splice(@active, $i, 1);
+ splice(@out, $i, 1);
+ $running--;
+ }
+ }
+ $out[$i][$channel] .= $r;
+ }
+ }
+ }
+ }
+}
+
+sub record {
+ my $dir=shift()->[0];
+ my $ret=shift;
+ my $out=shift;
+
+ if ($ret == OK) {
+ push @ok, $dir;
+ print "\n" unless $quiet || ($minimal && !$out);
+ }
+ elsif ($ret == FAILED) {
+ if ($interactive) {
+ chdir($dir) unless $no_chdir;
+ print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
+ system((getpwuid($<))[8], "-i");
+ }
+ push @failed, $dir;
+ print "\n";
+ }
+ elsif ($ret == SKIPPED) {
+ push @skipped, $dir;
+ }
+ elsif ($ret == ABORT) {
+ exit 1;
+ }
+ else {
+ die "unknown exit status $ret";
+ }
+}
+
+sub showstats {
+ my $action=shift;
+ if (! @ok && ! @failed && ! @skipped) {
+ die "mr $action: no repositories found to work on\n";
+ }
+ print "mr $action: finished (".join("; ",
+ showstat($#ok+1, "ok", "ok"),
+ showstat($#failed+1, "failed", "failed"),
+ showstat($#skipped+1, "skipped", "skipped"),
+ ).")\n" unless $quiet || $minimal;
+ if ($stats) {
+ if (@skipped) {
+ print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet || $minimal;
+ }
+ if (@failed) {
+ print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+ }
+ }
+}
+
+sub showstat {
+ my $count=shift;
+ my $singular=shift;
+ my $plural=shift;
+ if ($count) {
+ return "$count ".($count > 1 ? $plural : $singular);
+ }
+ return;
+}
+
+# an ordered list of repos
+sub repolist {
+ my @list;
+ foreach my $topdir (sort keys %config) {
+ foreach my $subdir (sort keys %{$config{$topdir}}) {
+ push @list, {
+ topdir => $topdir,
+ subdir => $subdir,
+ order => $config{$topdir}{$subdir}{order},
+ };
+ }
+ }
+ return sort {
+ $a->{order} <=> $b->{order}
+ ||
+ $a->{topdir} cmp $b->{topdir}
+ ||
+ $a->{subdir} cmp $b->{subdir}
+ } @list;
+}
+
+sub repodir {
+ my $repo=shift;
+ my $topdir=$repo->{topdir};
+ my $subdir=$repo->{subdir};
+ my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+ $ret=~s/\/\.$//;
+ return $ret;
+}
+
+# Figure out which repos to act on. Returns a list of array refs
+# in the format:
+#
+# [ "$full_repo_path/", "$mr_config_path/", $section_header ]
+sub selectrepos {
+ my @repos;
+ foreach my $repo (repolist()) {
+ my $topdir=$repo->{topdir};
+ my $subdir=$repo->{subdir};
+
+ next if $subdir eq 'DEFAULT';
+ my $dir=repodir($repo);
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ next if $dir ne $d && $dir !~ /^\Q$d\E/;
+ if (defined $max_depth) {
+ my @a=split('/', $dir);
+ my @b=split('/', $d);
+ do { } while (@a && @b && shift(@a) eq shift(@b));
+ next if @a > $max_depth || @b > $max_depth;
+ }
+ push @repos, [$dir, $topdir, $subdir];
+ }
+ if (! @repos) {
+ # fallback to find a leaf repo
+ foreach my $repo (reverse repolist()) {
+ my $topdir=$repo->{topdir};
+ my $subdir=$repo->{subdir};
+
+ next if $subdir eq 'DEFAULT';
+ my $dir=repodir($repo);
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ if ($d=~/^\Q$dir\E/) {
+ push @repos, [$dir, $topdir, $subdir];
+ last;
+ }
+ }
+ $no_chdir=1;
+ }
+ return @repos;
+}
+
+sub expandenv {
+ my $val=shift;
+
+
+ if ($val=~/\$/) {
+ $val=`echo "$val"`;
+ chomp $val;
+ }
+
+ return $val;
+}
+
+my %trusted;
+sub is_trusted_config {
+ my $config=shift; # must be abs_pathed already
+
+ # We always trust ~/.mrconfig.
+ return 1 if $config eq abs_path($HOME_MR_CONFIG);
+
+ return 1 if $trust_all;
+
+ my $trustfile=$ENV{HOME}."/.mrtrust";
+
+ if (! %trusted) {
+ $trusted{$HOME_MR_CONFIG}=1;
+ if (open (TRUST, "<", $trustfile)) {
+ while (<TRUST>) {
+ chomp;
+ s/^~\//$ENV{HOME}\//;
+ my $d=abs_path($_);
+ $trusted{$d}=1 if defined $d;
+ }
+ close TRUST;
+ }
+ }
+
+ return $trusted{$config};
+}
+
+
+sub is_trusted_repo {
+ my $repo=shift;
+
+ # Tightly limit what is allowed in a repo name.
+ # No ../, no absolute paths, and no unusual filenames
+ # that might try to escape to the shell.
+ return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
+ $repo !~ /\.\./ && $repo !~ /^\//;
+}
+
+sub is_trusted_checkout {
+ my $command=shift;
+
+ # To determine if the command is safe, compare it with the
+ # *_trusted_checkout config settings. Those settings are
+ # templates for allowed commands, so make sure that each word
+ # of the command matches the corresponding word of the template.
+
+ my @words;
+ foreach my $word (split(' ', $command)) {
+ # strip quoting
+ if ($word=~/^'(.*)'$/) {
+ $word=$1;
+ }
+ elsif ($word=~/^"(.*)"$/) {
+ $word=$1;
+ }
+
+ push @words, $word;
+ }
+
+ foreach my $key (grep { /_trusted_checkout$/ }
+ keys %{$config{''}{DEFAULT}}) {
+ my @twords=split(' ', $config{''}{DEFAULT}{$key});
+ next if @words > @twords;
+
+ my $match=1;
+ my $url;
+ for (my $c=0; $c < @twords && $match; $c++) {
+ if ($twords[$c] eq '$url') {
+ # Match all the typical characters found in
+ # urls, plus @ which svn can use. Note
+ # that the "url" might also be a local
+ # directory.
+ $match=(
+ defined $words[$c] &&
+ $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
+ );
+ $url=$words[$c];
+ }
+ elsif ($twords[$c] eq '$repo') {
+ # If a repo is not specified, assume it
+ # will be the last path component of the
+ # url, or something derived from it, and
+ # check that.
+ if (! defined $words[$c] && defined $url) {
+ ($words[$c])=$url=~/\/([^\/]+)\/?$/;
+ }
+
+ $match=(
+ defined $words[$c] &&
+ is_trusted_repo($words[$c])
+ );
+ }
+ elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
+ $match=1;
+ }
+ else {
+ $match=0;
+ }
+ }
+ return 1 if $match;
+ }
+
+ return 0;
+}
+
+my %loaded;
+sub loadconfig {
+ my $f=shift;
+ my $dir=shift;
+ my $bootstrap_src=shift;
+
+ my @toload;
+
+ my $in;
+ my $trusted;
+ if (ref $f eq 'GLOB') {
+ $dir="";
+ $in=$f;
+ $trusted=1;
+ }
+ else {
+ my $absf=abs_path($f);
+ if ($loaded{$absf}) {
+ return;
+ }
+ $loaded{$absf}=1;
+
+ $trusted=is_trusted_config($absf);
+
+ if (! defined $dir) {
+ ($dir)=$f=~/^(.*\/)[^\/]+$/;
+ if (! defined $dir) {
+ $dir=".";
+ }
+ }
+
+ $dir=abs_path($dir)."/";
+
+ if (! exists $configfiles{$dir}) {
+ $configfiles{$dir}=$f;
+ }
+
+ # copy in defaults from first parent
+ my $parent=$dir;
+ while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
+ if ($parent eq '/') {
+ $parent="";
+ }
+ if (exists $config{$parent} &&
+ exists $config{$parent}{DEFAULT}) {
+ $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
+ last;
+ }
+ }
+
+ if (! -e $f) {
+ return;
+ }
+
+ print "mr: loading config $f\n" if $verbose;
+ open($in, "<", $f) || die "mr: open $f: $!\n";
+ }
+ my @lines=<$in>;
+ close $in unless ref $f eq 'GLOB';
+
+ my $section;
+
+ # Keep track of the current line in the config file;
+ # when a file is included track the current line from the include.
+ my $lineno=0;
+ my $included=undef;
+
+ my $line;
+ my $nextline = sub {
+ if ($included) {
+ $included--;
+ }
+ else {
+ $included=undef;
+ $lineno++;
+ }
+ $line=shift @lines;
+ chomp $line;
+ return $line;
+ };
+ my $lineerror = sub {
+ my $msg=shift;
+ if (defined $included) {
+ die "mr: $msg at $f line $lineno, included line: $line\n";
+ }
+ else {
+ die "mr: $msg at $f line $lineno\n";
+ }
+ };
+ my $trusterror = sub {
+ my $msg=shift;
+
+ if (defined $bootstrap_src) {
+ die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
+ "(To trust this url, --trust-all can be used; but please use caution;\n".
+ "this can allow arbitrary code execution!)\n";
+ }
+ else {
+ die "mr: $msg in untrusted $f line $lineno\n".
+ "(To trust this file, list it in ~/.mrtrust.)\n";
+ }
+ };
+
+ while (@lines) {
+ $_=$nextline->();