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.
7 mr - a Multiple Repository management tool
11 B<mr> [options] checkout
13 B<mr> [options] update
15 B<mr> [options] status
17 B<mr> [options] commit [-m "message"]
23 B<mr> [options] register [repository]
25 B<mr> [options] config section [parameter=[value] ...]
27 B<mr> [options] action [params ...]
31 B<mr> is a Multiple Repository management tool. It
32 can checkout, update, or perform other actions on
33 a set of repositories as if they were one combined respository. It
34 supports any combination of subversion, git, cvs, and bzr repositories,
35 and support for other revision control systems can easily be added.
37 B<mr> cds into and operates on all registered repositories at or below your
38 working directory. Or, if you are in a subdirectory of a repository that
39 contains no other registered repositories, it will stay in that directory,
40 and work on only that repository,
42 These predefined commands should be fairly familiar to users of any revision
47 =item checkout (or co)
49 Checks out any repositories that are not already checked out.
53 Updates each repository from its configured remote repository.
55 If a repository isn't checked out yet, it will first check it out.
59 Displays a status report for each repository, showing what
60 uncommitted changes are present in the repository.
64 Commits changes to each repository. (By default, changes are pushed to the
65 remote repository too, when using distributed systems like git.)
67 The optional -m parameter allows specifying a commit message.
71 Show a diff of uncommitted changes.
79 These commands are also available:
85 List the repositories that mr will act on.
89 Register an existing repository in the mrconfig file. By default, the
90 repository in the current directory is registered, or you can specify a
91 directory to register.
93 By default it registers it to the ~/.mrconfig file. To make it write to a
94 different file, use the -c option.
98 Adds, modifies, removes, or prints a value from the mrconfig file. The next
99 parameter is the name of the section the value is in. To add or modify
100 values, use one or more instances of "parameter=value". Use "parameter=" to
101 remove a parameter. Use just "parameter" to get the value of a parameter.
103 For example, to add (or edit) a repository in src/foo:
105 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
107 To show the command that mr uses to update the repository in src/foo:
109 mr config src/foo update
117 Actions can be abbreviated to any unambiguous subsctring, so
118 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
121 Additional parameters can be passed to most commands, and are passed on
122 unchanged to the underlying revision control system. This is mostly useful
123 if the repositories mr will act on all use the same revision control
132 Specifies the topmost directory that B<mr> should work in. The default is
133 the current working directory.
137 Use the specified mrconfig file, instead of looking for one in your home
146 Expand the statistics line displayed at the end to include information
147 about exactly which repositories failed and were skipped, if any.
151 Just operate on the repository for the current directory, do not
152 recurse into deeper repositories.
158 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
159 file in your home directory, and this can in turn chain load .mrconfig files
162 Here is an example .mrconfig file:
165 checkout = svn co svn://svn.example.com/src/trunk src
169 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
171 git checkout -b mybranch origin/master
173 The .mrconfig file uses a variant of the INI file format. Lines starting with
174 "#" are comments. Values can be continued to the following line by
175 indenting the line with whitespace.
177 The "DEFAULT" section allows setting default values for the sections that
180 The "ALIAS" section allows adding aliases for actions. Each parameter
181 is an alias, and its value is the action to use.
183 All other sections add repositories. The section header specifies the
184 directory where the repository is located. This is relative to the directory
185 that contains the mrconfig file, but you can also choose to use absolute
188 Within a section, each parameter defines a shell command to run to handle a
189 given action. mr contains default handlers for the "update", "status", and
190 "commit" actions, so normally you only need to specify what to do for
193 Note that these shell commands are run in a "set -e" shell
194 environment, where any additional parameters you pass are available in
195 "$@". The "checkout" command is run in the parent of the repository
196 directory, since the repository isn't checked out yet. All other commands
197 are run inside the repository, though not necessarily at the top of it.
199 The "MR_REPO" environment variable is set to the path to the top of the
200 repository, and "MR_CONFIG" is set to the .mrconfig file that defines the
201 repo being acted on, or, if the repo is not yet in a config file, the
202 .mrconfig file that mr thinks it should be added to.
204 A few parameters have special meanings:
210 If the "skip" parameter is set and its command returns true, then B<mr>
211 will skip acting on that repository. The command is passed the action
214 Here are two examples. The first skips the repo unless
215 mr is run by joey. The second uses the hours_since function
216 (included in mr's built-in library) to skip updating the repo unless it's
217 been at least 12 hours since the last update.
219 skip = test $(whoami) != joey
220 skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
224 If the "chain" parameter is set and its command returns true, then B<mr>
225 will try to load a .mrconfig file from the root of the repository. (You
226 should avoid chaining from repositories with untrusted committers.)
230 If the "deleted" parameter is set and its command returns true, then
231 B<mr> will treat the repository as deleted. It won't ever actually delete
232 the repository, but it will warn if it sees the repository's directory.
233 This is useful when one mrconfig file is shared amoung multiple machines,
234 to keep track of and remember to delete old repositories.
238 The "lib" parameter can specify some shell code that will be run before each
239 command, this can be a useful way to define shell functions for other commands
246 Copyright 2007 Joey Hess <joey@kitenet.net>
248 Licensed under the GNU GPL version 2 or higher.
250 http://kitenet.net/~joey/code/mr/
259 use Cwd qw(getcwd abs_path);
262 print STDERR "mr: interrupted\n";
266 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
267 my $directory=getcwd();
276 Getopt::Long::Configure("no_permute");
277 my $result=GetOptions(
278 "d|directory=s" => sub { $directory=abs_path($_[1]) },
279 "c|config=s" => \$ENV{MR_CONFIG},
280 "v|verbose" => \$verbose,
281 "s|stats" => \$stats,
282 "n|no-recurse" => \$no_recurse,
284 if (! $result || @ARGV < 1) {
285 die("Usage: mr [-d directory] action [params ...]\n".
286 "(Use mr help for man page.)\n");
290 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
291 # the config file might be a symlink to elsewhere, and the directory it's
293 if ($ENV{MR_CONFIG} !~ /^\//) {
294 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
298 loadconfig($ENV{MR_CONFIG});
300 #print Dumper(\%config);
303 use FindBin qw($Bin $Script);
304 $ENV{MR_PATH}=$Bin."/".$Script;
307 # alias expansion and command stemming
308 my $action=shift @ARGV;
309 if (exists $alias{$action}) {
310 $action=$alias{$action};
312 if (! exists $knownactions{$action}) {
313 my @matches = grep { /^\Q$action\E/ }
314 keys %knownactions, keys %alias;
318 elsif (@matches == 0) {
319 die "mr: unknown action \"$action\" (known actions: ".
320 join(", ", sort keys %knownactions).")\n";
323 die "mr: ambiguous action \"$action\" (matches: ".
324 join(", ", @matches).")\n";
328 if ($action eq 'help') {
329 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
331 elsif ($action eq 'config') {
333 die "mr config: not enough parameters\n";
336 if ($section=~/^\//) {
337 # try to convert to a path relative to the config file
338 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
340 $dir.="/" unless $dir=~/\/$/;
341 if ($section=~/^\Q$dir\E(.*)/) {
347 if (/^([^=]+)=(.*)$/) {
348 $changefields{$1}=$2;
352 foreach my $topdir (sort keys %config) {
353 if (exists $config{$topdir}{$section} &&
354 exists $config{$topdir}{$section}{$_}) {
355 print $config{$topdir}{$section}{$_}."\n";
360 die "mr $action: $section $_ not set\n";
364 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
367 elsif ($action eq 'register') {
368 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
369 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
370 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
371 print STDERR "mr $action: running >>$command<<\n" if $verbose;
372 exec($command) || die "exec: $!";
375 # work out what repos to act on
378 foreach my $topdir (sort keys %config) {
379 foreach my $subdir (sort keys %{$config{$topdir}}) {
380 next if $subdir eq 'DEFAULT';
381 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
383 $dir.="/" unless $dir=~/\/$/;
384 $d.="/" unless $d=~/\/$/;
385 next if $no_recurse && $d ne $dir;
386 next if $dir ne $d && $dir !~ /^\Q$d\E/;
387 push @repos, [$dir, $topdir, $subdir];
391 # fallback to find a leaf repo
392 LEAF: foreach my $topdir (reverse sort keys %config) {
393 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
394 next if $subdir eq 'DEFAULT';
395 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
397 $dir.="/" unless $dir=~/\/$/;
398 $d.="/" unless $d=~/\/$/;
399 if ($d=~/^\Q$dir\E/) {
400 push @repos, [$dir, $topdir, $subdir];
408 my (@failed, @ok, @skipped);
409 foreach my $repo (@repos) {
410 action($action, @$repo);
414 my ($action, $dir, $topdir, $subdir) = @_;
416 $ENV{MR_CONFIG}=$configfiles{$topdir};
417 my $lib=exists $config{$topdir}{$subdir}{lib} ?
418 $config{$topdir}{$subdir}{lib}."\n" : "";
420 if (exists $config{$topdir}{$subdir}{deleted}) {
421 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
422 print "mr $action: running deleted test >>$test<<\n" if $verbose;
423 my $ret=system($test);
425 if (($? & 127) == 2) {
426 print STDERR "mr $action: interrupted\n";
430 print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
433 if ($ret >> 8 == 0) {
435 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
440 print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
447 if ($action eq 'checkout') {
449 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
454 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
457 print "mr $action: creating parent directory $dir\n" if $verbose;
458 my $ret=system("mkdir", "-p", $dir);
461 elsif ($action eq 'update') {
463 return action("checkout", $dir, $topdir, $subdir);
469 if (exists $config{$topdir}{$subdir}{skip}) {
470 my $test="set -e;".$lib.
471 "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
472 print "mr $action: running skip test >>$test<<\n" if $verbose;
473 my $ret=system($test);
475 if (($? & 127) == 2) {
476 print STDERR "mr $action: interrupted\n";
480 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
484 if ($ret >> 8 == 0) {
485 print "mr $action: $dir skipped per config file\n" if $verbose;
491 if (! $nochdir && ! chdir($dir)) {
492 print STDERR "mr $action: failed to chdir to $dir: $!\n";
495 elsif (! exists $config{$topdir}{$subdir}{$action}) {
496 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
501 print "mr $action: $topdir$subdir\n";
504 print "mr $action: $topdir$subdir (in subdir $directory)\n";
506 my $command="set -e; ".$lib.
507 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
508 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
509 print STDERR "mr $action: running >>$command<<\n" if $verbose;
510 my $ret=system($command);
512 if (($? & 127) == 2) {
513 print STDERR "mr $action: interrupted\n";
517 print STDERR "mr $action: received signal ".($? & 127)."\n";
519 print STDERR "mr $action: failed ($ret)\n" if $verbose;
521 if ($ret >> 8 != 0) {
522 print STDERR "mr $action: command failed\n";
525 print STDERR "mr $action: command died ($ret)\n";
529 if ($action eq 'checkout' && ! -d $dir) {
530 print STDERR "mr $action: $dir missing after checkout\n";;
547 return "$count ".($count > 1 ? $plural : $singular);
551 if (! @ok && ! @failed && ! @skipped) {
552 die "mr $action: no repositories found to work on\n";
554 print "mr $action: finished (".join("; ",
555 showstat($#ok+1, "ok", "ok"),
556 showstat($#failed+1, "failed", "failed"),
557 showstat($#skipped+1, "skipped", "skipped"),
561 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
564 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
570 elsif (! @ok && @skipped) {
576 sub loadconfig { #{{{
583 if (ref $f eq 'GLOB') {
592 my $absf=abs_path($f);
593 if ($loaded{$absf}) {
598 ($dir)=$f=~/^(.*\/)[^\/]+$/;
599 if (! defined $dir) {
602 $dir=abs_path($dir)."/";
604 # copy in defaults from first parent
606 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
607 if ($parent eq '/') {
610 if (exists $config{$parent} &&
611 exists $config{$parent}{DEFAULT}) {
612 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
617 print "mr: loading config $f\n" if $verbose;
618 open($in, "<", $f) || die "mr: open $f: $!\n";
629 next if /^\s*\#/ || /^\s*$/;
630 if (/^\[([^\]]*)\]\s*$/) {
633 elsif (/^(\w+)\s*=\s*(.*)/) {
638 while (@lines && $lines[0]=~/^\s(.+)/) {
645 if (! defined $section) {
646 die "$f line $.: parameter ($parameter) not in section\n";
648 if ($section ne 'ALIAS' &&
649 ! exists $config{$dir}{$section} &&
650 exists $config{$dir}{DEFAULT}) {
652 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
654 if ($section eq 'ALIAS') {
655 $alias{$parameter}=$value;
657 elsif ($parameter eq 'lib') {
658 $config{$dir}{$section}{lib}.=$value."\n";
661 $config{$dir}{$section}{$parameter}=$value;
662 $knownactions{$parameter}=1;
663 if (! exists $configfiles{$dir}) {
664 $configfiles{$dir}=abs_path($f);
666 if ($parameter eq 'chain' &&
667 length $dir && $section ne "DEFAULT" &&
668 -e $dir.$section."/.mrconfig") {
669 my $ret=system($value);
671 if (($? & 127) == 2) {
672 print STDERR "mr $action: chain test interrupted\n";
676 print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
680 push @toload, $dir.$section."/.mrconfig";
686 die "$f line $line: parse error\n";
695 sub modifyconfig { #{{{
697 # the section to modify or add
698 my $targetsection=shift;
699 # fields to change in the section
700 # To remove a field, set its value to "".
707 open(my $in, "<", $f) || die "mr: open $f: $!\n";
712 my $formatfield=sub {
714 my @value=split(/\n/, shift);
716 return "$field = ".shift(@value)."\n".
717 join("", map { "\t$_\n" } @value);
721 while ($out[$#out] =~ /^\s*$/) {
722 unshift @blanks, pop @out;
724 foreach my $field (sort keys %changefields) {
725 if (length $changefields{$field}) {
726 push @out, "$field = $changefields{$field}\n";
727 delete $changefields{$field};
737 if (/^\s*\#/ || /^\s*$/) {
740 elsif (/^\[([^\]]*)\]\s*$/) {
741 if (defined $section &&
742 $section eq $targetsection) {
750 elsif (/^(\w+)\s*=\s(.*)/) {
755 while (@lines && $lines[0]=~/^\s(.+)/) {
761 if ($section eq $targetsection) {
762 if (exists $changefields{$parameter}) {
763 if (length $changefields{$parameter}) {
764 $value=$changefields{$parameter};
766 delete $changefields{$parameter};
770 push @out, $formatfield->($parameter, $value);
774 if (defined $section &&
775 $section eq $targetsection) {
778 elsif (%changefields) {
779 push @out, "\n[$targetsection]\n";
780 foreach my $field (sort keys %changefields) {
781 if (length $changefields{$field}) {
782 push @out, $formatfield->($field, $changefields{$field});
787 open(my $out, ">", $f) || die "mr: write $f: $!\n";
792 # Finally, some useful actions that mr knows about by default.
793 # These can be overridden in ~/.mrconfig.
808 for dir in .git .svn .bzr CVS; do
809 if [ -e "$MR_REPO/$dir" ]; then
810 flagfile="$MR_REPO/$dir/.mr_last$1"
814 if [ -z "$flagfile" ]; then
815 error "cannot determine flag filename"
817 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
822 if [ -d "$MR_REPO"/.svn ]; then
824 elif [ -d "$MR_REPO"/.git ]; then
825 git pull origin master "$@"
826 elif [ -d "$MR_REPO"/.bzr ]; then
828 elif [ -d "$MR_REPO"/CVS ]; then
831 error "unknown repo type"
834 if [ -d "$MR_REPO"/.svn ]; then
836 elif [ -d "$MR_REPO"/.git ]; then
837 git status "$@" || true
838 elif [ -d "$MR_REPO"/.bzr ]; then
840 elif [ -d "$MR_REPO"/CVS ]; then
843 error "unknown repo type"
846 if [ -d "$MR_REPO"/.svn ]; then
848 elif [ -d "$MR_REPO"/.git ]; then
849 git commit -a "$@" && git push --all
850 elif [ -d "$MR_REPO"/.bzr ]; then
851 bzr commit "$@" && bzr push
852 elif [ -d "$MR_REPO"/CVS ]; then
855 error "unknown repo type"
858 if [ -d "$MR_REPO"/.svn ]; then
860 elif [ -d "$MR_REPO"/.git ]; then
862 elif [ -d "$MR_REPO"/.bzr ]; then
864 elif [ -d "$MR_REPO"/CVS ]; then
867 error "unknown repo type"
870 if [ -d "$MR_REPO"/.svn ]; then
872 elif [ -d "$MR_REPO"/.git ]; then
874 elif [ -d "$MR_REPO"/.bzr ]; then
876 elif [ -d "$MR_REPO"/CVS ]; then
879 error "unknown repo type"
885 basedir="$(basename $(pwd))"
887 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
888 if [ -z "$url" ]; then
889 error "cannot determine svn url"
891 echo "Registering svn url: $url in $MR_CONFIG"
892 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
893 elif [ -d .git ]; then
894 url=$(LANG=C git-config --get remote.origin.url)
895 if [ -z "$url" ]; then
896 error "cannot determine git url"
898 echo "Registering git url: $url in $MR_CONFIG"
899 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
900 elif [ -d .bzr ]; then
901 url=$(cat .bzr/branch/parent)
902 if [ -z "$url" ]; then
903 error "cannot determine bzr url"
905 echo "Registering bzr url: $url in $MR_CONFIG"
906 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
907 elif [ -d CVS ]; then
908 repo=$(cat CVS/Repository)
910 if [ -z "$root" ]; then
911 error "cannot determine cvs root"
913 echo "Registering cvs repository $repo at root $root"
914 mr -c "$MR_CONFIG" config "$(pwd)" \
915 checkout="cvs -d '$root' co -d $basedir $repo"
917 error "unable to register this repo type"
920 if [ ! -e "$MR_PATH" ]; then
921 error "cannot find program path"
923 (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
927 ed = echo "A horse is a horse, of course, of course.."
928 T = echo "I pity the fool."
929 right = echo "Not found."