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] config section [parameter=[value] ...]
23 B<mr> [options] action [params ...]
27 B<mr> is a Multiple Repository management tool. It allows you to register a
28 set of repositories in a .mrconfig file, and then checkout, update, or
29 perform other actions on the repositories as if they were one big
32 Any mix of revision control systems can be used with B<mr>, and you can
33 define arbitrary actions for commands like "update", "checkout", or "commit".
35 B<mr> cds into and operates on all registered repsitories at or below your
36 working directory. Or, if you are in a subdirectory of a repository that
37 contains no other registered repositories, it will stay in that directory,
38 and work on only that repository,
40 The predefined commands should be fairly familiar to users of any revision
45 =item checkout (or co)
47 Checks out any repositories that are not already checked out.
51 Updates each repository from its configured remote repository.
53 If a repository isn't checked out yet, it will first check it out.
57 Displays a status report for each repository, showing what
58 uncommitted changes are present in the repository.
62 Commits changes to each repository. (By default, changes are pushed to the
63 remote repository too, when using distributed systems like git.)
65 The optional -m parameter allows specifying a commit message.
69 Show a diff of uncommitted changes.
77 List the repositories that mr will act on.
81 Modifies the mrconfig file. The next parameter is the name of the section
82 to add or modify, and it is followed by one or more instances of
83 "parameter=value". Use "parameter=" to remove a parameter. For example,
84 to register a new svn repository in src/foo:
86 mr config src/foo checkout="svn co svn://example.com/foo/trunk"
94 Actions can be abbreviated to any unambiguous subsctring, so
95 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
98 Additional parameters can be passed to other commands than "commit", they
99 will be passed on unchanged to the underlying revision control system.
100 This is mostly useful if the repositories mr will act on all use the same
101 revision control system.
109 Specifies the topmost directory that B<mr> should work in. The default is
110 the current working directory.
114 Use the specified mrconfig file, instead of looking for one in your home
125 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
126 file in your home directory, and this can in turn chain load .mrconfig files
129 Here is an example .mrconfig file:
132 checkout = svn co svn://svn.example.com/src/trunk src
136 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
138 The .mrconfig file uses a variant of the INI file format. Lines starting with
139 "#" are comments. Lines ending with "\" are continued on to the next line.
141 The "DEFAULT" section allows setting default values for the sections that
144 The "ALIAS" section allows adding aliases for actions. Each parameter
145 is an alias, and its value is the action to use.
147 All other sections add repositories. The section header specifies the
148 directory where the repository is located. This is relative to the directory
149 that contains the mrconfig file, but you can also choose to use absolute
152 Within a section, each parameter defines a shell command to run to handle a
153 given action. mr contains default handlers for the "update", "status", and
154 "commit" actions, so normally you only need to specify what to do for
157 Note that these shell commands are run in a "set -e" shell
158 environment, where any additional parameters you pass are available in
159 "$@". The "checkout" command is run in the parent of the repository
160 directory, since the repository isn't checked out yet. All other commands
161 are run inside the repository, though not necessarily at the top of it.
162 The "MR_REPO" environment variable is set to the path to the top of the
165 A few parameters have special meanings:
171 If the "skip" parameter is set and its command returns nonzero, then B<mr>
172 will skip acting on that repository.
176 If the "chain" parameter is set and its command returns nonzero, then B<mr>
177 will try to load a .mrconfig file from the root of the repository. (You
178 should avoid chaining from repositories with untrusted committers.)
182 If the "deleted" parameter is set and its command returns nonzero, then
183 B<mr> will treat the repository as deleted. It won't ever actually delete
184 the repository, but it will warn if it sees the repsoitory's directory.
185 This is useful when one mrconfig file is shared amoung multiple machines,
186 to keep track of and remember to delete old repositories.
190 The "lib" parameter can specify some shell code that will be run before each
191 command, this can be a useful way to define shell functions for other commands
198 Copyright 2007 Joey Hess <joey@kitenet.net>
200 Licensed under the GNU GPL version 2 or higher.
202 http://kitenet.net/~joey/code/mr/
209 use Cwd qw(getcwd abs_path);
211 my $directory=getcwd();
212 my $config="$ENV{HOME}/.mrconfig";
218 Getopt::Long::Configure("no_permute");
219 my $result=GetOptions(
220 "d|directory=s" => sub { $directory=abs_path($_[1]) },
221 "c|config=s" => \$config,
222 "verbose" => \$verbose,
224 if (! $result || @ARGV < 1) {
225 die("Usage: mr [-d directory] action [params ...]\n".
226 "(Use mr help for man page.)\n");
233 #print Dumper(\%config);
236 use FindBin qw($Bin $Script);
237 $ENV{MR_PATH}=$Bin."/".$Script;
240 # alias expansion and command stemming
241 my $action=shift @ARGV;
242 if (exists $alias{$action}) {
243 $action=$alias{$action};
245 if (! exists $knownactions{$action}) {
246 my @matches = grep { /^\Q$action\E/ }
247 keys %knownactions, keys %alias;
251 elsif (@matches == 0) {
252 die "mr: unknown action \"$action\" (known actions: ".
253 join(", ", sort keys %knownactions).")\n";
256 die "mr: ambiguous action \"$action\" (matches: ".
257 join(", ", @matches).")\n";
261 if ($action eq 'help') {
262 exec($config{''}{DEFAULT}{help});
264 elsif ($action eq 'config') {
266 die "mr config: not enough parameters\n";
271 if (/^([^=]+)=(.*)$/) {
275 die "mr config: expected parameter=value, not \"$_\"\n";
278 modifyconfig($config, $section, %fields);
282 # work out what repos to act on
285 foreach my $topdir (sort keys %config) {
286 foreach my $subdir (sort keys %{$config{$topdir}}) {
287 next if $subdir eq 'DEFAULT';
288 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
290 $dir.="/" unless $dir=~/\/$/;
291 $d.="/" unless $d=~/\/$/;
292 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
293 push @repos, [$dir, $topdir, $subdir];
297 # fallback to find a leaf repo
298 LEAF: foreach my $topdir (reverse sort keys %config) {
299 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
300 next if $subdir eq 'DEFAULT';
301 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
303 $dir.="/" unless $dir=~/\/$/;
304 $d.="/" unless $d=~/\/$/;
305 if ($d=~/^\Q$dir\E/) {
306 push @repos, [$dir, $topdir, $subdir];
314 my (@failed, @successful, @skipped);
315 foreach my $repo (@repos) {
316 action($action, @$repo);
320 my ($action, $dir, $topdir, $subdir) = @_;
322 my $lib= exists $config{$topdir}{$subdir}{lib} ?
323 $config{$topdir}{$subdir}{lib}."\n" : "";
325 if (exists $config{$topdir}{$subdir}{deleted}) {
330 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
331 print "mr $action: running deleted test >>$test<<\n" if $verbose;
332 my $ret=system($test);
333 if ($ret >> 8 == 0) {
334 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
341 if ($action eq 'checkout') {
343 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
347 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
349 elsif ($action eq 'update') {
351 return action("checkout", $dir, $topdir, $subdir);
357 if (exists $config{$topdir}{$subdir}{skip}) {
358 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
359 print "mr $action: running skip test >>$test<<\n" if $verbose;
360 my $ret=system($test);
361 if ($ret >> 8 == 0) {
362 print "mr $action: $dir skipped per config file\n" if $verbose;
368 if (! $nochdir && ! chdir($dir)) {
369 print STDERR "mr $action: failed to chdir to $dir: $!\n";
372 elsif (! exists $config{$topdir}{$subdir}{$action}) {
373 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
378 print "mr $action: $topdir$subdir\n";
381 print "mr $action: $topdir$subdir (in subdir $directory)\n";
383 my $command="set -e; ".$lib.
384 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
385 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
386 print STDERR "mr $action: running >>$command<<\n" if $verbose;
387 my $ret=system($command);
389 print STDERR "mr $action: failed ($ret)\n" if $verbose;
391 if ($ret >> 8 != 0) {
392 print STDERR "mr $action: command failed\n";
395 print STDERR "mr $action: command died ($ret)\n";
399 push @successful, $dir;
411 return "$count ".($count > 1 ? $plural : $singular);
415 if (! @successful && ! @failed && ! @skipped) {
416 die "mr $action: no repositories found to work on\n";
418 print "mr $action: finished (".join("; ",
419 showstat($#successful+1, "successful", "successful"),
420 showstat($#failed+1, "failed", "failed"),
421 showstat($#skipped+1, "skipped", "skipped"),
426 elsif (! @successful && @skipped) {
439 if (ref $f eq 'GLOB') {
444 # $f might be a symlink
445 my $absf=abs_path($f);
446 if ($loaded{$absf}) {
451 print "mr: loading config $f\n" if $verbose;
452 open($in, "<", $f) || die "mr: open $f: $!\n";
453 ($dir)=$f=~/^(.*\/)[^\/]+$/;
454 if (! defined $dir) {
457 $dir=abs_path($dir)."/";
459 # copy in defaults from first parent
461 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
462 if (exists $config{$parent} &&
463 exists $config{$parent}{DEFAULT}) {
464 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
473 next if /^\s*\#/ || /^\s*$/;
474 if (/^\s*\[([^\]]*)\]\s*$/) {
477 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
482 while ($value=~/(.*)\\$/s) {
483 $value=$1."\n".<$in>;
487 if (! defined $section) {
488 die "$f line $.: parameter ($parameter) not in section\n";
490 if ($section ne 'ALIAS' &&
491 ! exists $config{$dir}{$section} &&
492 exists $config{$dir}{DEFAULT}) {
494 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
496 if ($section eq 'ALIAS') {
497 $alias{$parameter}=$value;
499 elsif ($parameter eq 'lib') {
500 $config{$dir}{$section}{lib}.=$value."\n";
503 $config{$dir}{$section}{$parameter}=$value;
504 $knownactions{$parameter}=1;
505 if ($parameter eq 'chain' &&
506 length $dir && $section ne "DEFAULT" &&
507 -e $dir.$section."/.mrconfig" &&
508 system($value) >> 8 == 0) {
509 push @toload, $dir.$section."/.mrconfig";
514 die "$f line $.: parse error\n";
526 # the section to modify or add
527 my $targetsection=shift;
528 # fields to change in the section
529 # To remove a field, set its value to "".
534 open(my $in, "<", $f) || die "mr: open $f: $!\n";
544 if (/^\s*\#/ || /^\s*$/) {
547 elsif (/^\s*\[([^\]]*)\]\s*$/) {
548 if (defined $section &&
549 $section eq $targetsection) {
551 while ($out[$#out] =~ /^\s*$/) {
552 unshift @blanks, pop @out;
554 foreach my $field (sort keys %changefields) {
555 if (length $changefields{$field}) {
556 push @out, "$field = $changefields{$field}\n";
566 elsif (/^\s*(\w+)\s*=\s(.*)/) {
571 while ($value=~/(.*\\)$/s) {
572 $value=$1."\n".shift(@lines);
576 if ($section eq $targetsection) {
577 if (exists $changefields{$parameter}) {
578 if (length $changefields{$parameter}) {
579 $value=$changefields{$parameter};
581 delete $changefields{$parameter};
585 push @out, "$parameter = $value\n";
590 push @out, "\n[$targetsection]\n";
591 foreach my $field (sort keys %changefields) {
592 if (length $changefields{$field}) {
593 push @out, "$field = $changefields{$field}\n";
598 open(my $out, ">", $f) || die "mr: write $f: $!\n";
603 # Finally, some useful actions that mr knows about by default.
604 # These can be overridden in ~/.mrconfig.
619 if [ -d "$MR_REPO"/.svn ]; then \
621 elif [ -d "$MR_REPO"/.git ]; then \
622 git pull origin master "$@" \
623 elif [ -d "$MR_REPO"/.bzr ]; then \
625 elif [ -d "$MR_REPO"/CVS ]; then \
628 error "unknown repo type" \
631 if [ -d "$MR_REPO"/.svn ]; then \
633 elif [ -d "$MR_REPO"/.git ]; then \
634 git status "$@" || true \
635 elif [ -d "$MR_REPO"/.bzr ]; then \
637 elif [ -d "$MR_REPO"/CVS ]; then \
640 error "unknown repo type" \
643 if [ -d "$MR_REPO"/.svn ]; then \
645 elif [ -d "$MR_REPO"/.git ]; then \
646 git commit -a "$@" && git push --all \
647 elif [ -d "$MR_REPO"/.bzr ]; then \
648 bzr commit "$@" && bzr push \
649 elif [ -d "$MR_REPO"/CVS ]; then \
652 error "unknown repo type" \
655 if [ -d "$MR_REPO"/.svn ]; then \
657 elif [ -d "$MR_REPO"/.git ]; then \
659 elif [ -d "$MR_REPO"/.bzr ]; then \
661 elif [ -d "$MR_REPO"/CVS ]; then \
664 error "unknown repo type" \
667 if [ -d "$MR_REPO"/.svn ]; then \
669 elif [ -d "$MR_REPO"/.git ]; then \
671 elif [ -d "$MR_REPO"/.bzr ]; then \
673 elif [ -d "$MR_REPO"/CVS ]; then \
676 error "unknown repo type" \
681 if [ ! -e "$MR_PATH" ]; then \
682 error "cannot find program path" \
684 (pod2man -c mr "$MR_PATH" | man -l -) || \
685 error "pod2man or man failed"
687 ed = echo "A horse is a horse, of course, of course.."
688 T = echo "I pity the fool."