+sub hook {
+ my ($hook, $topdir, $subdir) = @_;
+
+ my $command=$config{$topdir}{$subdir}{$hook};
+ return OK unless defined $command;
+ my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
+ my $sh=shift;
+ if (!$jobs || $jobs > 1 || $quiet) {
+ return terminal_friendly_spawn(undef, $sh, $quiet);
+ }
+ 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;
+}
+
+# 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);
+ }
+ 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);
+ 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;
+
+ if ($ret == OK) {
+ push @ok, $dir;
+ print "\n" unless $quiet;
+ }
+ 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;
+ if ($stats) {
+ if (@skipped) {
+ print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
+ }
+ 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;
+}