All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
5 mr - a Multiple Repository management tool
9 B<mr> [options] checkout
11 B<mr> [options] update
13 B<mr> [options] status
15 B<mr> [options] commit [-m "message"]
21 B<mr> [options] action [params ...]
25 B<mr> is a Multiple Repository management tool. It allows you to register a
26 set of repositories in a .mrconfig file, and then checkout, update, or
27 perform other actions on the repositories as if they were one big
30 Any mix of revision control systems can be used with B<mr>, and you can
31 define arbitrary actions for commands like "update", "checkout", or "commit".
33 B<mr> cds into and operates on all registered repsitories at or below your
34 working directory. Or, if you are in a subdirectory of a repository that
35 contains no other registered repositories, it will stay in that directory,
36 and work on only that repository,
38 The predefined commands should be fairly familiar to users of any revision
43 =item checkout (or co)
45 Checks out any repositories that are not already checked out.
49 Updates each repository from its configured remote repository.
51 If a repository isn't checked out yet, it will first check it out.
55 Displays a status report for each repository, showing what
56 uncommitted changes are present in the repository.
60 Commits changes to each repository. (By default, changes are pushed to the
61 remote repository too, when using distributed systems like git.)
63 The optional -m parameter allows specifying a commit message.
67 Show a diff of uncommitted changes.
75 List the repositories that mr will act on.
83 Actions can be abbreviated to any unambiguous subsctring, so
84 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
87 Additional parameters can be passed to other commands than "commit", they
88 will be passed on unchanged to the underlying revision control system.
89 This is mostly useful if the repositories mr will act on all use the same
90 revision control system.
98 Specifies the topmost directory that B<mr> should work in. The default is
99 the current working directory.
103 Use the specified mrconfig file, instead of looking for one in your home
114 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
115 file in your home directory, and this can in turn chain load .mrconfig files
118 Here is an example .mrconfig file:
121 checkout = svn co svn://svn.example.com/src/trunk src
125 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
127 The .mrconfig file uses a variant of the INI file format. Lines starting with
128 "#" are comments. Lines ending with "\" are continued on to the next line.
130 The "DEFAULT" section allows setting default values for the sections that
133 The "ALIAS" section allows adding aliases for actions. Each parameter
134 is an alias, and its value is the action to use.
136 All other sections add repositories. The section header specifies the
137 directory where the repository is located. This is relative to the directory
138 that contains the mrconfig file, but you can also choose to use absolute
141 Within a section, each parameter defines a shell command to run to handle a
142 given action. mr contains default handlers for the "update", "status", and
143 "commit" actions, so normally you only need to specify what to do for
146 Note that these shell commands are run in a "set -e" shell
147 environment, where any additional parameters you pass are available in
148 "$@". The "checkout" command is run in the parent of the repository
149 directory, since the repository isn't checked out yet. All other commands
150 are run inside the repository, though not necessarily at the top of it.
151 The "MR_REPO" environment variable is set to the path to the top of the
154 A few parameters have special meanings:
160 If the "skip" parameter is set and its command returns nonzero, then B<mr>
161 will skip acting on that repository.
165 If the "chain" parameter is set and its command returns nonzero, then B<mr>
166 will try to load a .mrconfig file from the root of the repository. (You
167 should avoid chaining from repositories with untrusted committers.)
171 If the "deleted" parameter is set and its command returns nonzero, then
172 B<mr> will treat the repository as deleted. It won't ever actually delete
173 the repository, but it will warn if it sees the repsoitory's directory.
174 This is useful when one mrconfig file is shared amoung multiple machines,
175 to keep track of and remember to delete old repositories.
179 The "lib" parameter can specify some shell code that will be run before each
180 command, this can be a useful way to define shell functions for other commands
187 Copyright 2007 Joey Hess <joey@kitenet.net>
189 Licensed under the GNU GPL version 2 or higher.
191 http://kitenet.net/~joey/code/mr/
198 use Cwd qw(getcwd abs_path);
200 my $directory=getcwd();
201 my $config="$ENV{HOME}/.mrconfig";
207 Getopt::Long::Configure("no_permute");
208 my $result=GetOptions(
209 "d|directory=s" => sub { $directory=abs_path($_[1]) },
210 "c|config=s" => \$config,
211 "verbose" => \$verbose,
213 if (! $result || @ARGV < 1) {
214 die("Usage: mr [-d directory] action [params ...]\n".
215 "(Use mr help for man page.)\n");
222 #print Dumper(\%config);
225 use FindBin qw($Bin $Script);
226 $ENV{MR_PATH}=$Bin."/".$Script;
229 # alias expansion and command stemming
230 my $action=shift @ARGV;
231 if (exists $alias{$action}) {
232 $action=$alias{$action};
234 if (! exists $knownactions{$action}) {
235 my @matches = grep { /^\Q$action\E/ }
236 keys %knownactions, keys %alias;
240 elsif (@matches == 0) {
241 die "mr: unknown action \"$action\" (known actions: ".
242 join(", ", sort keys %knownactions).")\n";
245 die "mr: ambiguous action \"$action\" (matches: ".
246 join(", ", @matches).")\n";
250 if ($action eq 'help') {
251 exec($config{''}{DEFAULT}{help});
254 # work out what repos to act on
257 foreach my $topdir (sort keys %config) {
258 foreach my $subdir (sort keys %{$config{$topdir}}) {
259 next if $subdir eq 'DEFAULT';
260 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
262 $dir.="/" unless $dir=~/\/$/;
263 $d.="/" unless $d=~/\/$/;
264 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
265 push @repos, [$dir, $topdir, $subdir];
269 # fallback to find a leaf repo
270 LEAF: foreach my $topdir (reverse sort keys %config) {
271 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
272 next if $subdir eq 'DEFAULT';
273 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
275 $dir.="/" unless $dir=~/\/$/;
276 $d.="/" unless $d=~/\/$/;
277 if ($d=~/^\Q$dir\E/) {
278 push @repos, [$dir, $topdir, $subdir];
286 my (@failed, @successful, @skipped);
287 foreach my $repo (@repos) {
288 action($action, @$repo);
292 my ($action, $dir, $topdir, $subdir) = @_;
294 my $lib= exists $config{$topdir}{$subdir}{lib} ?
295 $config{$topdir}{$subdir}{lib}."\n" : "";
297 if (exists $config{$topdir}{$subdir}{deleted}) {
302 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
303 print "mr $action: running deleted test >>$test<<\n" if $verbose;
304 my $ret=system($test);
305 if ($ret >> 8 == 0) {
306 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
313 if ($action eq 'checkout') {
315 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
319 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
321 elsif ($action eq 'update') {
323 return action("checkout", $dir, $topdir, $subdir);
329 if (exists $config{$topdir}{$subdir}{skip}) {
330 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
331 print "mr $action: running skip test >>$test<<\n" if $verbose;
332 my $ret=system($test);
333 if ($ret >> 8 == 0) {
334 print "mr $action: $dir skipped per config file\n" if $verbose;
340 if (! $nochdir && ! chdir($dir)) {
341 print STDERR "mr $action: failed to chdir to $dir: $!\n";
344 elsif (! exists $config{$topdir}{$subdir}{$action}) {
345 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
350 print "mr $action: $topdir$subdir\n";
353 print "mr $action: $topdir$subdir (in subdir $directory)\n";
355 my $command="set -e; ".$lib.
356 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
357 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
358 print STDERR "mr $action: running >>$command<<\n" if $verbose;
359 my $ret=system($command);
361 print STDERR "mr $action: failed ($ret)\n" if $verbose;
363 if ($ret >> 8 != 0) {
364 print STDERR "mr $action: command failed\n";
367 print STDERR "mr $action: command died ($ret)\n";
371 push @successful, $dir;
383 return "$count ".($count > 1 ? $plural : $singular);
387 if (! @successful && ! @failed && ! @skipped) {
388 die "mr $action: no repositories found to work on\n";
390 print "mr $action: finished (".join("; ",
391 showstat($#successful+1, "successful", "successful"),
392 showstat($#failed+1, "failed", "failed"),
393 showstat($#skipped+1, "skipped", "skipped"),
398 elsif (! @successful && @skipped) {
411 if (ref $f eq 'GLOB') {
416 # $f might be a symlink
417 my $absf=abs_path($f);
418 if ($loaded{$absf}) {
423 print "mr: loading config $f\n" if $verbose;
424 open($in, "<", $f) || die "mr: open $f: $!\n";
425 ($dir)=$f=~/^(.*\/)[^\/]+$/;
426 if (! defined $dir) {
429 $dir=abs_path($dir)."/";
431 # copy in defaults from first parent
433 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
434 if (exists $config{$parent} &&
435 exists $config{$parent}{DEFAULT}) {
436 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
445 next if /^\s*\#/ || /^\s*$/;
446 if (/^\s*\[([^\]]*)\]\s*$/) {
449 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
454 while ($value=~/(.*)\\$/s) {
455 $value=$1."\n".<$in>;
459 if (! defined $section) {
460 die "$f line $.: parameter ($parameter) not in section\n";
462 if ($section ne 'ALIAS' &&
463 ! exists $config{$dir}{$section} &&
464 exists $config{$dir}{DEFAULT}) {
466 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
468 if ($section eq 'ALIAS') {
469 $alias{$parameter}=$value;
471 elsif ($parameter eq 'lib') {
472 $config{$dir}{$section}{lib}.=$value."\n";
475 $config{$dir}{$section}{$parameter}=$value;
476 $knownactions{$parameter}=1;
477 if ($parameter eq 'chain' &&
478 length $dir && $section ne "DEFAULT" &&
479 -e $dir.$section."/.mrconfig" &&
480 system($value) >> 8 == 0) {
481 push @toload, $dir.$section."/.mrconfig";
486 die "$f line $.: parse error\n";
496 # Finally, some useful actions that mr knows about by default.
497 # These can be overridden in ~/.mrconfig.
512 if [ -d "$MR_REPO"/.svn ]; then \
514 elif [ -d "$MR_REPO"/.git ]; then \
515 git pull origin master "$@" \
516 elif [ -d "$MR_REPO"/.bzr ]; then \
518 elif [ -d "$MR_REPO"/CVS ]; then \
521 error "unknown repo type" \
524 if [ -d "$MR_REPO"/.svn ]; then \
526 elif [ -d "$MR_REPO"/.git ]; then \
527 git status "$@" || true \
528 elif [ -d "$MR_REPO"/.bzr ]; then \
530 elif [ -d "$MR_REPO"/CVS ]; then \
533 error "unknown repo type" \
536 if [ -d "$MR_REPO"/.svn ]; then \
538 elif [ -d "$MR_REPO"/.git ]; then \
539 git commit -a "$@" && git push --all \
540 elif [ -d "$MR_REPO"/.bzr ]; then \
541 bzr commit "$@" && bzr push \
542 elif [ -d "$MR_REPO"/CVS ]; then \
545 error "unknown repo type" \
548 if [ -d "$MR_REPO"/.svn ]; then \
550 elif [ -d "$MR_REPO"/.git ]; then \
552 elif [ -d "$MR_REPO"/.bzr ]; then \
554 elif [ -d "$MR_REPO"/CVS ]; then \
557 error "unknown repo type" \
560 if [ -d "$MR_REPO"/.svn ]; then \
562 elif [ -d "$MR_REPO"/.git ]; then \
564 elif [ -d "$MR_REPO"/.bzr ]; then \
566 elif [ -d "$MR_REPO"/CVS ]; then \
569 error "unknown repo type" \
574 if [ ! -e "$MR_PATH" ]; then \
575 error "cannot find program path" \
577 (pod2man -c mr "$MR_PATH" | man -l -) || \
578 error "pod2man or man failed"
580 ed = echo "A horse is a horse, of course, of course.."
581 T = echo "I pity the fool."