B<mr> [options] commit [-m "message"]
+B<mr> [options] diff
+
B<mr> [options] action [params ...]
=head1 DESCRIPTION
B<mr> is a Multiple Repository management tool. It allows you to register a
set of repositories in a .mrconfig file, and then checkout, update, or
-perform other actions on all of the repositories at once.
+perform other actions on the repositories as if they were one big
+respository.
Any mix of revision control systems can be used with B<mr>, and you can
define arbitrary actions for commands like "update", "checkout", or "commit".
=over 4
-=item checkout
+=item checkout (or co)
-Checks out all the registered repositories that are not already checked
-out.
+Checks out any repositories that are not already checked out.
=item update
-Updates each registered repository from its configured remote repository.
+Updates each 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
+Displays a status report for each repository, showing what
uncommitted changes are present in the repository.
-=item commit
+=item commit (or ci)
-Commits changes to each registered repository. (By default, changes
-are pushed to the remote repository too, when using distributed systems
-like git.)
+Commits changes to each 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.
+=item diff
+
+Show a diff of uncommitted changes.
+
+=item list
+
+List the repositories that mr will act on.
+
+=item help
+
+Displays this help.
+
=back
Actions can be abbreviated to any unambiguous subsctring, so
"mr st" is equivilant to "mr status".
+B<mr> operates on all registered repsitories at or below your working
+directory. Or, if you are in a subdirectory of a repository that contains
+no other registered repositories, it will act on only that repository.
+
+Additional parameters can be passed to other commands than "commit", they
+will be passed on unchanged to the underlying revision control system.
+This is mostly useful if the repositories mr will act on all use the same
+revision control system.
+
=head1 OPTIONS
=over 4
=item -d directory
Specifies the topmost directory that B<mr> should work in. The default is
-the current working directory. B<mr> will operate on all registered
-repositories at or under the directory.
+the current working directory.
=item -c mrconfig
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, 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.
+"$@". The "checkout" command is run in the parent of the repository
+directory, since the repository isn't checked out yet. All other commands
+are run inside the repository, though not necessarily at the top of it.
+The "MR_REPO" environment variable is set to the path to the top of the
+repository.
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.
handlers for the "update", "status", and "commit" actions, so normally
you only need to specify what to do for "checkout".
+The "alias" section allows adding aliases for commands. Each parameter
+is an alias, and its value is the command to run.
+
For example:
[src]
my $verbose=0;
my %config;
my %knownactions;
+my %alias;
Getopt::Long::Configure("no_permute");
my $result=GetOptions(
"v" => \$verbose,
);
if (! $result || @ARGV < 1) {
- die("Usage: mr [-d directory] action [params ...]\n");
+ die("Usage: mr [-d directory] action [params ...]\n".
+ "(Use mr help for man page.)\n");
+
}
loadconfig(\*DATA);
#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 (! $knownactions{$action}) {
- my @matches = grep { /^\Q$action\E/ } keys %knownactions;
- if (@matches == 1) {
- $action=$matches[0];
+if (! exists $knownactions{$action}) {
+ if (exists $alias{$action}) {
+ $action=$alias{$action};
}
else {
- die "mr: ambiguous action \"$action\" (matches @matches)\n";
+ my @matches = grep { /^\Q$action\E/ }
+ keys %knownactions, keys %alias;
+ if (@matches == 1) {
+ $action=$matches[0];
+ }
+ else {
+ die "mr: ambiguous action \"$action\" (matches @matches)\n";
+ }
}
}
-my (@failed, @successful, @skipped);
-my $first=1;
+if ($action eq 'help') {
+ exec($config{''}{default}{help});
+}
+
+# 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}}) {
- next if $subdir eq 'default';
-
my $dir=$topdir.$subdir;
-
- if (defined $directory &&
- $dir ne $directory &&
- $dir !~ /^\Q$directory\E\//) {
- print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
- push @skipped, $dir;
- next;
+ next if $dir ne $directory && $dir !~ /^\Q$directory\E\//;
+ push @repos, [$dir, $topdir, $subdir];
+ }
+}
+if (! @repos) {
+ # fallback to find a leaf repo
+ foreach my $topdir (sort keys %config) {
+ foreach my $subdir (sort keys %{$config{$topdir}}) {
+ my $dir=$topdir.$subdir;
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ if ($d=~/^\Q$dir\E/) {
+ push @repos, [$dir, $topdir, $subdir];
+ }
}
-
- print "\n" unless $first;
- $first=0;
-
- action($action, $dir, $topdir, $subdir);
-
+ }
+ if (@repos > 1) {
+ die "mr: found multiple leaf repos (should never happen)";
}
}
+my (@failed, @successful, @skipped);
+foreach my $repo (@repos) {
+ action($action, @$repo);
+}
+
sub action {
my ($action, $dir, $topdir, $subdir) = @_;
+
+ my $lib= exists $config{$topdir}{$subdir}{lib} ?
+ $config{$topdir}{$subdir}{lib} : "";
if ($action eq 'checkout') {
if (-d $dir) {
}
}
- if (! chdir($dir)) {
+ $ENV{MR_REPO}=$dir;
+ if (! $nochdir && ! 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});
+ my $ret=system($lib.$config{$topdir}{$subdir}{skip});
if ($ret >> 8 == 0) {
print "mr $action: $dir skipped per config file\n" if $verbose;
push @skipped, $dir;
- next;
+ return;
}
}
push @skipped, $dir;
}
else {
- print "mr $action: in $dir\n";
- my $command="set -e; ".
- (exists $config{$topdir}{$subdir}{lib} ?
- $config{$topdir}{$subdir}{lib} : "").
+ print "mr $action: $dir\n";
+ my $command="set -e; ".$lib.
"my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
my $ret=system($command);
else {
push @successful, $dir;
}
+
+ print "\n";
}
}
}
return;
}
-print "\nmr $action: finished (".join("; ",
+if (! @successful && ! @failed && ! @skipped) {
+ die "mr $action: no repositories found to work on\n";
+}
+print "mr $action: finished (".join("; ",
showstat($#successful+1, "successful", "successful"),
showstat($#failed+1, "failed", "failed"),
showstat($#skipped+1, "skipped", "skipped"),
if (! defined $section) {
die "$f line $.: parameter ($parameter) not in section\n";
}
- if (! exists $config{$dir}{$section} &&
- exists $config{$dir}{default}) {
+ if ($section ne 'alias' &&
+ ! exists $config{$dir}{$section} &&
+ exists $config{$dir}{default}) {
# copy in defaults
$config{$dir}{$section}={ %{$config{$dir}{default}} };
}
- if ($parameter ne 'lib') {
- $config{$dir}{$section}{$parameter}=$value;
- $knownactions{$parameter}=1;
+ if ($section eq 'alias') {
+ $alias{$parameter}=$value;
+ }
+ elsif ($parameter eq 'lib') {
+ $config{$dir}{$section}{lib}.=$value." ; ";
}
else {
- $config{$dir}{$section}{$parameter}.=$value." ; ";
+ $config{$dir}{$section}{$parameter}=$value;
+ $knownactions{$parameter}=1;
+ if ($parameter eq 'chain' &&
+ length $dir && $section ne "default" &&
+ -e $dir.$section."/.mrconfig" &&
+ system($value) >> 8 == 0) {
+ push @toload, $dir.$section."/.mrconfig";
+ }
}
-
- 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";
+ die "$f line $.: parse error\n";
}
}
close $in;
# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
__DATA__
+[alias]
+ co = checkout
+ ci = commit
[default]
lib = \
error() { \
exit 1; \
}
update = \
- if [ -d .svn ]; then \
- svn update; \
- elif [ -d .git ]; then \
- git pull origin master; \
+ if [ -d "$MR_REPO"/.svn ]; then \
+ svn update "$@"; \
+ elif [ -d "$MR_REPO"/.git ]; then \
+ git pull origin master "$@"; \
else \
error "unknown repo type"; \
fi
status = \
- if [ -d .svn ]; then \
- svn status; \
- elif [ -d .git ]; then \
- git status || true; \
+ if [ -d "$MR_REPO"/.svn ]; then \
+ svn status "$@"; \
+ elif [ -d "$MR_REPO"/.git ]; then \
+ git status "$@" || true; \
else \
error "unknown repo type"; \
fi
commit = \
- if [ -d .svn ]; then \
+ if [ -d "$MR_REPO"/.svn ]; then \
svn commit "$@"; \
- elif [ -d .git ]; then \
+ elif [ -d "$MR_REPO"/.git ]; then \
git commit -a "$@" && git push --all; \
else \
error "unknown repo type"; \
fi
+diff = \
+ if [ -d "$MR_REPO"/.svn ]; then \
+ svn diff "$@"; \
+ elif [ -d "$MR_REPO"/.git ]; then \
+ git diff "$@"; \
+ else \
+ error "unknown repo type"; \
+ fi
+list = true
+help = \
+ if [ ! -e "$MR_PATH" ]; then \
+ error "cannot find program path";\
+ fi; \
+ (pod2man -c mr "$MR_PATH" | man -l -) || \
+ error "pod2man or man failed"