B<mr> [options] status
-B<mr> [options] commit -m "message"
+B<mr> [options] commit [-m "message"]
B<mr> [options] action [params ...]
perform other actions on all of the repositories at once.
Any mix of revision control systems can be used with B<mr>, and you can
-define arbitrary actions like "update", "checkout", or "commit".
+define arbitrary actions for commands like "update", "checkout", or "commit".
+
+The predefined commands should be fairly familiar to users of any revision
+control system:
+
+=over 4
+
+=item checkout
+
+Checks out all the registered repositories that are not already checked
+out.
+
+=item update
+
+Updates each registered repository from its configured remote repository.
+
+If a repository isn't checked out yet, it will first check it out.
+
+=item status
+
+Displays a status report for each registered repository, showing what
+uncommitted changes are present in the repository.
+
+=item commit
+
+Commits changes to each registered repository. (By default, changes
+are pushed to the remote repository too, when using distributed systems
+like git.)
+
+The optional -m parameter allows specifying a commit message.
+
+=back
+
+Actions can be abbreviated to any unambiguous subsctring, so
+"mr st" is equivilant to "mr status".
=head1 OPTIONS
=head1 FILES
-B<mr> is configured by .mrconfig files. It searches for .mrconfig files in
-your home directory, and in the root directory of each repository specified
-in a .mrconfig file. So you could have a ~/.mrconfig that registers a
+B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
+file in your home directory. Each repository specified in a .mrconfig file
+can also have its own .mrconfig file in its root directory that can
+optionally be used as well. So you could have a ~/.mrconfig that registers a
repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
registers several additional repositories.
Within a section, each parameter defines a shell command to run to handle a
given action. Note that these shell commands are run in a "set -e" shell
-environment, and B<mr> cds into the repository directory before running
-them, except for the "checkout" command, which is run in the parent of the
-repository directory, since the repository isn't checked out yet.
+environment, where any additional parameters you pass are available in
+"$@". B<mr> cds into the repository directory before running
+a command, except for the "checkout" command, which is run in the parent
+of the repository directory, since the repository isn't checked out yet.
-There are two special parameters. If the "skip" parameter is set and
+There are three special parameters. If the "skip" parameter is set and
its command returns nonzero, then B<mr> will skip acting on that repository.
+If the "chain" parameter is set and its command returns nonzero, 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.) The
+"lib" parameter can specify some shell code that will be run before each
+command, this can be a useful way to define shell functions other commands
+can use.
The "default" section allows setting up default handlers for each action,
and is overridden by the contents of other sections. mr contains default
[src]
checkout = svn co svn://svn.example.com/src/trunk src
+ chain = true
[src/linux-2.6]
- # only check this out on kodama
- skip = test $(hostname) != kodama
+ skip = small
checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+ [default]
+ lib = \
+ small() {
+ case "$(hostname)" in; \
+ slug|snail); \
+ return 0; ;; ; \
+ esac; \
+ return 1; \
+ }
+
=head1 AUTHOR
Copyright 2007 Joey Hess <joey@kitenet.net>
my $config="$ENV{HOME}/.mrconfig";
my $verbose=0;
my %config;
+my %knownactions;
Getopt::Long::Configure("no_permute");
my $result=GetOptions(
if (! $result || @ARGV < 1) {
die("Usage: mr [-d directory] action [params ...]\n");
}
-my $action=shift @ARGV;
loadconfig(\*DATA);
loadconfig($config);
#use Data::Dumper;
#print Dumper(\%config);
-my (@failures, @successes, @skipped);
+my $action=shift @ARGV;
+if (! $knownactions{$action}) {
+ my @matches = grep { /^\Q$action\E/ } keys %knownactions;
+ if (@matches == 1) {
+ $action=$matches[0];
+ }
+ else {
+ die "mr: ambiguous action \"$action\" (matches @matches)\n";
+ }
+}
+
+my (@failed, @successful, @skipped);
my $first=1;
foreach my $topdir (sort keys %config) {
foreach my $subdir (sort keys %{$config{$topdir}}) {
my $dir=$topdir.$subdir;
if (defined $directory &&
+ $dir ne $directory &&
$dir !~ /^\Q$directory\E\//) {
- print "mr $action: $dir skipped per -d parameter\n" if $verbose;
+ print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
push @skipped, $dir;
next;
}
print "\n" unless $first;
$first=0;
- if (exists $config{$topdir}{$subdir}{skip}) {
- my $ret=system($config{$topdir}{$subdir}{skip});
- if ($ret >> 8 == 0) {
- print "mr $action: $dir skipped per config file\n" if $verbose;
- push @skipped, $dir;
- next;
- }
- }
+ action($action, $dir, $topdir, $subdir);
- if ($action eq 'checkout') {
- if (-e $dir) {
- print "mr $action: $dir already exists, skipping checkout\n";
- push @skipped, $dir;
- next;
- }
- $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
- }
- if (! chdir($dir)) {
- print STDERR "mr $action: failed to chdir to $dir: $!\n";
+ }
+}
+
+sub action {
+ my ($action, $dir, $topdir, $subdir) = @_;
+
+ if ($action eq 'checkout') {
+ if (-d $dir) {
+ print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
push @skipped, $dir;
+ return;
}
- elsif (! exists $config{$topdir}{$subdir}{$action}) {
- print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
+ $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
+ }
+ elsif ($action eq 'update') {
+ if (! -d $dir) {
+ return action("checkout", $dir, $topdir, $subdir);
+ }
+ }
+
+ if (! chdir($dir)) {
+ print STDERR "mr $action: failed to chdir to $dir: $!\n";
+ push @skipped, $dir;
+ }
+
+ if (exists $config{$topdir}{$subdir}{skip}) {
+ my $ret=system($config{$topdir}{$subdir}{skip});
+ if ($ret >> 8 == 0) {
+ print "mr $action: $dir skipped per config file\n" if $verbose;
push @skipped, $dir;
+ next;
}
- else {
- print "mr $action: in $dir\n";
- my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action @ARGV";
- my $ret=system($command);
- if ($ret != 0) {
- print STDERR "mr $action: failed to run: $command\n" if $verbose;
- push @failures, $topdir.$subdir;
- if ($ret >> 8 != 0) {
- print STDERR "mr $action: command failed\n";
- }
- elsif ($ret != 0) {
- print STDERR "mr $action: command died ($ret)\n";
- }
+ }
+
+ if (! exists $config{$topdir}{$subdir}{$action}) {
+ print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
+ push @skipped, $dir;
+ }
+ else {
+ print "mr $action: in $dir\n";
+ my $command="set -e; ".
+ (exists $config{$topdir}{$subdir}{lib} ?
+ $config{$topdir}{$subdir}{lib} : "").
+ "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
+ join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+ my $ret=system($command);
+ if ($ret != 0) {
+ print STDERR "mr $action: failed to run: $command\n" if $verbose;
+ push @failed, $topdir.$subdir;
+ if ($ret >> 8 != 0) {
+ print STDERR "mr $action: command failed\n";
}
- else {
- push @successes, $dir;
+ elsif ($ret != 0) {
+ print STDERR "mr $action: command died ($ret)\n";
}
}
+ else {
+ push @successful, $dir;
+ }
}
}
return;
}
print "\nmr $action: finished (".join("; ",
- showstat($#successes+1, "success", "sucesses"),
- showstat($#failures+1, "failure", "failures"),
+ showstat($#successful+1, "successful", "successful"),
+ showstat($#failed+1, "failed", "failed"),
showstat($#skipped+1, "skipped", "skipped"),
).")\n";
-exit @failures ? 1 : 0;
+if (@failed) {
+ exit 1;
+}
+elsif (! @successful && @skipped) {
+ exit 1;
+}
+exit 0;
my %loaded;
sub loadconfig {
$dir="";
}
else {
- $f=abs_path($f);
-
- if ($loaded{$f}) {
+ # $f might be a symlink
+ my $absf=abs_path($f);
+ if ($loaded{$absf}) {
return;
}
- $loaded{$f}=1;
+ $loaded{$absf}=1;
print "mr: loading config $f\n" if $verbose;
open($in, "<", $f) || die "mr: open $f: $!\n";
($dir)=$f=~/^(.*\/)[^\/]+$/;
+ if (! defined $dir) {
+ $dir=".";
+ }
+ $dir=abs_path($dir)."/";
# copy in defaults from first parent
my $parent=$dir;
next if /^\s*\#/ || /^\s*$/;
if (/^\s*\[([^\]]*)\]\s*$/) {
$section=$1;
- if (length $dir && $section ne "default" &&
- -e $dir.$section."/.mrconfig") {
- push @toload, $dir.$section."/.mrconfig";
- }
}
elsif (/^\s*(\w+)\s*=\s*(.*)/) {
my $parameter=$1;
# copy in defaults
$config{$dir}{$section}={ %{$config{$dir}{default}} };
}
- $config{$dir}{$section}{$parameter}=$value;
+ if ($parameter ne 'lib') {
+ $config{$dir}{$section}{$parameter}=$value;
+ $knownactions{$parameter}=1;
+ }
+ else {
+ $config{$dir}{$section}{$parameter}.=$value." ; ";
+ }
+
+ if ($parameter eq 'chain' &&
+ length $dir && $section ne "default" &&
+ -e $dir.$section."/.mrconfig" &&
+ system($value) >> 8 == 0) {
+ push @toload, $dir.$section."/.mrconfig";
+ }
}
else {
die "$f line $.: parse error\n";
}
}
-__DATA__
-# Some useful actions that mr knows about by default.
+# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
+__DATA__
[default]
+lib = \
+ error() { \
+ echo "mr: $@" >&2; \
+ exit 1; \
+ }
update = \
if [ -d .svn ]; then \
svn update; \
elif [ -d .git ]; then \
git pull origin master; \
else \
- echo "mr update: unknown RCS"; \
- exit 1; \
+ error "unknown repo type"; \
fi
status = \
if [ -d .svn ]; then \
elif [ -d .git ]; then \
git status || true; \
else \
- echo "mr status: unknown RCS"; \
- exit 1; \
+ error "unknown repo type"; \
fi
commit = \
if [ -d .svn ]; then \
svn commit "$@"; \
elif [ -d .git ]; then \
- git commit -a "$@"; \
+ git commit -a "$@" && git push --all; \
else \
- echo "mr commit: unknown RCS"; \
- exit 1; \
+ error "unknown repo type"; \
fi