X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/89d68dbad8ce10ea3844d95565d121ba23bf5875..8863dd020e175247f423aed1dc63dbe0269af923:/mr?ds=sidebyside

diff --git a/mr b/mr
index 80ccc78..0bded23 100755
--- a/mr
+++ b/mr
@@ -22,17 +22,17 @@ B<mr> [options] log
 
 B<mr> [options] register [repository]
 
-B<mr> [options] config section [parameter=[value] ...]
+B<mr> [options] config section ["parameter=[value]" ...]
 
 B<mr> [options] action [params ...]
 
 =head1 DESCRIPTION
 
-B<mr> is a Multiple Repository management tool. It
-can checkout, update, or perform other actions on
-a set of repositories as if they were one combined respository. It
-supports any combination of subversion, git, cvs, and bzr repositories, 
-and support for other revision control systems can easily be added.
+B<mr> is a Multiple Repository management tool. It can checkout, update, or
+perform other actions on a set of repositories as if they were one combined
+respository. It supports any combination of subversion, git, cvs, mecurial and
+bzr repositories, and support for other revision control systems can easily be
+added.
 
 B<mr> cds into and operates on all registered repositories at or below your
 working directory. Or, if you are in a subdirectory of a repository that
@@ -108,6 +108,10 @@ To show the command that mr uses to update the repository in src/foo:
 
   mr config src/foo update
 
+To see the built-in library of shell functions contained in mr:
+
+  mr config DEFAULT lib
+
 The ~/.mrconfig file is used by default. To use a different config file,
 use the -c option.
 
@@ -153,6 +157,12 @@ about exactly which repositories failed and were skipped, if any.
 Just operate on the repository for the current directory, do not 
 recurse into deeper repositories.
 
+=item -j number
+
+Run the specified number of jobs in parallel. This can greatly speed up
+operations such as updates. It is not recommended for interactive
+operations.
+
 =back
 
 =head1 FILES
@@ -188,9 +198,9 @@ that contains the mrconfig file, but you can also choose to use absolute
 paths.
 
 Within a section, each parameter defines a shell command to run to handle a
-given action. mr contains default handlers for the "update", "status", and
-"commit" actions, so normally you only need to specify what to do for
-"checkout".
+given action. mr contains default handlers for "update", "status",
+"commit", and other standard actions. Normally you only need to specify what
+to do for "checkout".
 
 Note that these shell commands are run in a "set -e" shell
 environment, where any additional parameters you pass are available in
@@ -219,7 +229,18 @@ mr is run by joey. The second uses the hours_since function
 been at least 12 hours since the last update.
 
   skip = test $(whoami) != joey
-  skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
+  skip = [ "$1" = update ] && ! hours_since "$1" 12
+
+=item order
+
+The "order" parameter can be used to override the default ordering of
+repositories. The default order value is 10. Use smaller values to make
+repositories be processed earlier, and larger values to make repositories
+be processed later.
+
+Note that if a repository is located in a subdirectory of another
+repository, ordering it to be processed earlier is not recommended, as this
+can cause confusion during checkouts.
 
 =item chain
 
@@ -227,14 +248,6 @@ If the "chain" parameter is set and its command returns true, then B<mr>
 will try to load a .mrconfig file from the root of the repository. (You
 should avoid chaining from repositories with untrusted committers.)
 
-=item deleted
-
-If the "deleted" parameter is set and its command returns true, then
-B<mr> will treat the repository as deleted. It won't ever actually delete
-the repository, but it will warn if it sees the repository's directory.
-This is useful when one mrconfig file is shared amoung multiple machines,
-to keep track of and remember to delete old repositories.
-
 =item lib
 
 The "lib" parameter can specify some shell code that will be run before each
@@ -259,6 +272,13 @@ use warnings;
 use strict;
 use Getopt::Long;
 use Cwd qw(getcwd abs_path);
+use POSIX "WNOHANG";
+use constant {
+	OK => 0,
+	FAILED => 1,
+	SKIPPED => 2,
+	ABORT => 3,
+};
 
 $SIG{INT}=sub {
 	print STDERR "mr: interrupted\n";
@@ -271,6 +291,7 @@ my $directory=getcwd();
 my $verbose=0;
 my $stats=0;
 my $no_recurse=0;
+my $jobs=1;
 my %config;
 my %configfiles;
 my %knownactions;
@@ -283,6 +304,7 @@ my $result=GetOptions(
 	"v|verbose" => \$verbose,
 	"s|stats" => \$stats,
 	"n|no-recurse" => \$no_recurse,
+	"j|jobs=i" => \$jobs,
 );
 if (! $result || @ARGV < 1) {
 	die("Usage: mr [-d directory] action [params ...]\n".
@@ -296,17 +318,17 @@ if (! $result || @ARGV < 1) {
 if ($ENV{MR_CONFIG} !~ /^\//) {
 	$ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
 }
+# Try to set MR_PATH to the path to the program.
+eval {
+	use FindBin qw($Bin $Script);
+	$ENV{MR_PATH}=$Bin."/".$Script;
+};
 
 loadconfig(\*DATA);
 loadconfig($ENV{MR_CONFIG});
 #use Data::Dumper;
 #print Dumper(\%config);
 
-eval {
-	use FindBin qw($Bin $Script);
-	$ENV{MR_PATH}=$Bin."/".$Script;
-};
-
 # alias expansion and command stemming
 my $action=shift @ARGV;
 if (exists $alias{$action}) {
@@ -358,6 +380,7 @@ elsif ($action eq 'config') {
 				    exists $config{$topdir}{$section}{$_}) {
 					print $config{$topdir}{$section}{$_}."\n";
 					$found=1;
+					last if $section eq 'DEFAULT';
 				}
 			}
 			if (! $found) {
@@ -388,43 +411,94 @@ elsif ($action eq 'register') {
 	exec($command) || die "exec: $!";
 }
 
+# an ordered list of repos
+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},
+		};
+	}
+}
+@list = sort {
+		$a->{order}  <=> $b->{order}
+		             ||
+		$a->{topdir} cmp $b->{topdir}
+		             ||
+		$a->{subdir} cmp $b->{subdir}
+	} @list;
+
 # work out what repos to act on
 my @repos;
 my $nochdir=0;
-foreach my $topdir (sort keys %config) {
-	foreach my $subdir (sort keys %{$config{$topdir}}) {
+foreach my $repo (@list) {
+	my $topdir=$repo->{topdir};
+	my $subdir=$repo->{subdir};
+
+	next if $subdir eq 'DEFAULT';
+	my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+	my $d=$directory;
+	$dir.="/" unless $dir=~/\/$/;
+	$d.="/" unless $d=~/\/$/;
+	next if $no_recurse && $d ne $dir;
+	next if $dir ne $d && $dir !~ /^\Q$d\E/;
+	push @repos, [$dir, $topdir, $subdir];
+}
+if (! @repos) {
+	# fallback to find a leaf repo
+	foreach my $repo (@list) {
+		my $topdir=$repo->{topdir};
+		my $subdir=$repo->{subdir};
+		
 		next if $subdir eq 'DEFAULT';
 		my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
 		my $d=$directory;
 		$dir.="/" unless $dir=~/\/$/;
 		$d.="/" unless $d=~/\/$/;
-		next if $no_recurse && $d ne $dir;
-		next if $dir ne $d && $dir !~ /^\Q$d\E/;
-		push @repos, [$dir, $topdir, $subdir];
-	}
-}
-if (! @repos) {
-	# fallback to find a leaf repo
-	LEAF: foreach my $topdir (reverse sort keys %config) {
-		foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
-			next if $subdir eq 'DEFAULT';
-			my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
-			my $d=$directory;
-			$dir.="/" unless $dir=~/\/$/;
-			$d.="/" unless $d=~/\/$/;
-			if ($d=~/^\Q$dir\E/) {
-				push @repos, [$dir, $topdir, $subdir];
-				last LEAF;
-			}
+		if ($d=~/^\Q$dir\E/) {
+			push @repos, [$dir, $topdir, $subdir];
+			last;
 		}
 	}
 	$nochdir=1;
 }
 
-my (@failed, @ok, @skipped);
-foreach my $repo (@repos) {
-	action($action, @$repo);
+# run the action on each repository and print stats
+my (@ok, @failed, @skipped);
+if ($jobs > 1) {
+	mrs(@repos);
+}
+else {
+	foreach my $repo (@repos) {
+		record($repo, action($action, @$repo));
+		print "\n";
+	}
+}
+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";
+if ($stats) {
+	if (@skipped) {
+		print "mr $action: (skipped: ".join(" ", @skipped).")\n";
+	}
+	if (@failed) {
+		print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+	}
+}
+if (@failed) {
+	exit 1;
 }
+elsif (! @ok && @skipped) {
+	exit 1;
+}
+exit 0;
 
 sub action { #{{{
 	my ($action, $dir, $topdir, $subdir) = @_;
@@ -433,48 +507,20 @@ sub action { #{{{
 	my $lib=exists $config{$topdir}{$subdir}{lib} ?
 	               $config{$topdir}{$subdir}{lib}."\n" : "";
 
-	if (exists $config{$topdir}{$subdir}{deleted}) {
-		my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
-		print "mr $action: running deleted test >>$test<<\n" if $verbose;
-		my $ret=system($test);
-		if ($ret != 0) {
-			if (($? & 127) == 2) {
-				print STDERR "mr $action: interrupted\n";
-				exit 2;
-			}
-			elsif ($? & 127) {
-				print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
-			}
-		}
-		if ($ret >> 8 == 0) {
-			if (-d $dir) {
-				print STDERR "mr error: $dir should be deleted yet still exists\n\n";
-				push @failed, $dir;
-				return;
-			}
-			else {
-				print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
-				push @skipped, $dir;
-				return;
-			}
-		}
-	}
-
 	if ($action eq 'checkout') {
 		if (-d $dir) {
 			print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
-			push @skipped, $dir;
-			return;
+			return SKIPPED;
 		}
 
 		$dir=~s/^(.*)\/[^\/]+\/?$/$1/;
 
 		if (! -d $dir) {
 			print "mr $action: creating parent directory $dir\n" if $verbose;
-			my $ret=system("mkdir", "-p", $dir);
+			system("mkdir", "-p", $dir);
 		}
 	}
-	elsif ($action eq 'update') {
+	elsif ($action =~ /update/) {
 		if (! -d $dir) {
 			return action("checkout", $dir, $topdir, $subdir);
 		}
@@ -490,27 +536,26 @@ sub action { #{{{
 		if ($ret != 0) {
 			if (($? & 127) == 2) {
 				print STDERR "mr $action: interrupted\n";
-				exit 2;
+				return ABORT;
 			}
 			elsif ($? & 127) {
 				print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
-				exit 1;
+				return ABORT;
 			}
 		}
 		if ($ret >> 8 == 0) {
 			print "mr $action: $dir skipped per config file\n" if $verbose;
-			push @skipped, $dir;
-			return;
+			return SKIPPED;
 		}
 	}
 	
 	if (! $nochdir && ! chdir($dir)) {
 		print STDERR "mr $action: failed to chdir to $dir: $!\n";
-		push @failed, $dir;
+		return FAILED;
 	}
 	elsif (! exists $config{$topdir}{$subdir}{$action}) {
 		print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
-		push @skipped, $dir;
+		return SKIPPED;
 	}
 	else {
 		if (! $nochdir) {
@@ -527,31 +572,118 @@ sub action { #{{{
 		if ($ret != 0) {
 			if (($? & 127) == 2) {
 				print STDERR "mr $action: interrupted\n";
-				exit 2;
+				return ABORT;
 			}
 			elsif ($? & 127) {
 				print STDERR "mr $action: received signal ".($? & 127)."\n";
+				return ABORT;
 			}
 			print STDERR "mr $action: failed ($ret)\n" if $verbose;
-			push @failed, $dir;
 			if ($ret >> 8 != 0) {
 				print STDERR "mr $action: command failed\n";
 			}
 			elsif ($ret != 0) {
 				print STDERR "mr $action: command died ($ret)\n";
 			}
+			return FAILED;
 		}
 		else {
 			if ($action eq 'checkout' && ! -d $dir) {
 				print STDERR "mr $action: $dir missing after checkout\n";;
-				push @failed, $dir;
-				return;
+				return FAILED;
 			}
 
-			push @ok, $dir;
+			return OK;
 		}
+	}
+} #}}}
 
-		print "\n";
+# run actions on multiple repos, in parallel
+sub mrs { #{{{
+	$| = 1;
+	my @active;
+	my @fhs;
+	my @out;
+	my $running=0;
+	while (@fhs or @repos) {
+		while ($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];
+							print "\n";
+							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;
+	}
+	elsif ($ret == FAILED) {
+		push @failed, $dir;
+	}
+	elsif ($ret == SKIPPED) {
+		push @skipped, $dir;
+	}
+	elsif ($ret == ABORT) {
+		exit 1;
+	}
+	else {
+		die "unknown exit status $ret";
 	}
 } #}}}
 
@@ -564,29 +696,6 @@ sub showstat { #{{{
 	}
 	return;
 } #}}}
-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";
-if ($stats) {
-	if (@skipped) {
-		print "mr $action: (skipped: ".join(" ", @skipped).")\n";
-	}
-	if (@failed) {
-		print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
-	}
-}
-if (@failed) {
-	exit 1;
-}
-elsif (! @ok && @skipped) {
-	exit 1;
-}
-exit 0;
 
 my %loaded;
 sub loadconfig { #{{{
@@ -816,13 +925,17 @@ ci = commit
 ls = list
 
 [DEFAULT]
+order = 10
 lib =
 	error() {
 		echo "mr: $@" >&2
 		exit 1
 	}
 	hours_since() {
-		for dir in .git .svn .bzr CVS; do
+		if [ -z "$1" ] || [ -z "$2" ]; then
+			error "mr: usage: hours_since action num"
+		fi
+		for dir in .git .svn .bzr CVS .hg; do
 			if [ -e "$MR_REPO/$dir" ]; then
 				flagfile="$MR_REPO/$dir/.mr_last$1"
 				break
@@ -831,19 +944,30 @@ lib =
 		if [ -z "$flagfile" ]; then
 			error "cannot determine flag filename"
 		fi
-		perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
-		touch "$flagfile"
+		delta=$(perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile")
+		if [ "$delta" -lt "$2" ]; then
+			exit 0
+		else
+			touch "$flagfile"
+			exit 1
+		fi
 	}
 
 update =
 	if [ -d "$MR_REPO"/.svn ]; then
 		svn update "$@"
 	elif [ -d "$MR_REPO"/.git ]; then
-		git pull origin master "$@"
+		if [ -z "$@" ]; then
+			git pull -t origin master
+		else
+			git pull "$@"
+		fi
 	elif [ -d "$MR_REPO"/.bzr ]; then
 		bzr merge "$@"
 	elif [ -d "$MR_REPO"/CVS ]; then
 		cvs update "$@"
+	elif [ -d "$MR_REPO"/.hg ]; then
+		hg pull "$@" && hg update "$@"
 	else
 		error "unknown repo type"
 	fi
@@ -856,6 +980,8 @@ status =
 		bzr status "$@"
 	elif [ -d "$MR_REPO"/CVS ]; then
 		cvs status "$@"
+	elif [ -d "$MR_REPO"/.hg ]; then
+		hg status "$@"
 	else
 		error "unknown repo type"
 	fi
@@ -868,6 +994,8 @@ commit =
 		bzr commit "$@" && bzr push
 	elif [ -d "$MR_REPO"/CVS ]; then
 		cvs commit "$@"
+	elif [ -d "$MR_REPO"/.hg ]; then
+		hg commit -m "$@" && hg push
 	else
 		error "unknown repo type"
 	fi
@@ -880,6 +1008,8 @@ diff =
 		bzr diff "$@"
 	elif [ -d "$MR_REPO"/CVS ]; then
 		cvs diff "$@"
+	elif [ -d "$MR_REPO"/.hg ]; then
+		hg diff "$@"
 	else
 		error "unknown repo type"
 	fi
@@ -892,6 +1022,8 @@ log =
 		bzr log "$@"
 	elif [ -d "$MR_REPO"/CVS ]; then
 		cvs log "$@"
+	elif [ -d "$MR_REPO"/.hg ]; then
+		hg log "$@"
 	else
 		error "unknown repo type"
 	fi
@@ -930,6 +1062,11 @@ register =
 		echo "Registering cvs repository $repo at root $root"
 		mr -c "$MR_CONFIG" config "$(pwd)" \
 			checkout="cvs -d '$root' co -d $basedir $repo"
+	elif [ -d .hg ]; then
+		url=$(hg showconfig paths.default)
+		echo "Registering mercurial repo url: $url in $MR_CONFIG"
+		mr -c "$MR_CONFIG" config "$(pwd)" \
+			checkout="hg clone $url $basedir"
 	else
 		error "unable to register this repo type"
 	fi