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" => sub { $ENV{MR_CONFIG}=abs_path($_[1]) },
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");
291 loadconfig($ENV{MR_CONFIG});
293 #print Dumper(\%config);
296 use FindBin qw($Bin $Script);
297 $ENV{MR_PATH}=$Bin."/".$Script;
300 # alias expansion and command stemming
301 my $action=shift @ARGV;
302 if (exists $alias{$action}) {
303 $action=$alias{$action};
305 if (! exists $knownactions{$action}) {
306 my @matches = grep { /^\Q$action\E/ }
307 keys %knownactions, keys %alias;
311 elsif (@matches == 0) {
312 die "mr: unknown action \"$action\" (known actions: ".
313 join(", ", sort keys %knownactions).")\n";
316 die "mr: ambiguous action \"$action\" (matches: ".
317 join(", ", @matches).")\n";
321 if ($action eq 'help') {
322 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
324 elsif ($action eq 'config') {
326 die "mr config: not enough parameters\n";
329 if ($section=~/^\//) {
330 # try to convert to a path relative to the config file
331 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
332 if ($section=~/^\Q$dir\E(.*)/) {
338 if (/^([^=]+)=(.*)$/) {
339 $changefields{$1}=$2;
343 foreach my $topdir (sort keys %config) {
344 if (exists $config{$topdir}{$section} &&
345 exists $config{$topdir}{$section}{$_}) {
346 print $config{$topdir}{$section}{$_}."\n";
351 die "mr $action: $section $_ not set\n";
355 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
358 elsif ($action eq 'register') {
359 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
360 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
361 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
362 print STDERR "mr $action: running >>$command<<\n" if $verbose;
363 exec($command) || die "exec: $!";
366 # work out what repos to act on
369 foreach my $topdir (sort keys %config) {
370 foreach my $subdir (sort keys %{$config{$topdir}}) {
371 next if $subdir eq 'DEFAULT';
372 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
374 $dir.="/" unless $dir=~/\/$/;
375 $d.="/" unless $d=~/\/$/;
376 next if $no_recurse && $d ne $dir;
377 next if $dir ne $d && $dir !~ /^\Q$d\E/;
378 push @repos, [$dir, $topdir, $subdir];
382 # fallback to find a leaf repo
383 LEAF: foreach my $topdir (reverse sort keys %config) {
384 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
385 next if $subdir eq 'DEFAULT';
386 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
388 $dir.="/" unless $dir=~/\/$/;
389 $d.="/" unless $d=~/\/$/;
390 if ($d=~/^\Q$dir\E/) {
391 push @repos, [$dir, $topdir, $subdir];
399 my (@failed, @ok, @skipped);
400 foreach my $repo (@repos) {
401 action($action, @$repo);
405 my ($action, $dir, $topdir, $subdir) = @_;
407 $ENV{MR_CONFIG}=$configfiles{$topdir};
408 my $lib=exists $config{$topdir}{$subdir}{lib} ?
409 $config{$topdir}{$subdir}{lib}."\n" : "";
411 if (exists $config{$topdir}{$subdir}{deleted}) {
412 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
413 print "mr $action: running deleted test >>$test<<\n" if $verbose;
414 my $ret=system($test);
416 if (($? & 127) == 2) {
417 print STDERR "mr $action: interrupted\n";
421 print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
424 if ($ret >> 8 == 0) {
426 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
431 print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
438 if ($action eq 'checkout') {
440 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
445 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
448 print "mr $action: creating parent directory $dir\n" if $verbose;
449 my $ret=system("mkdir", "-p", $dir);
452 elsif ($action eq 'update') {
454 return action("checkout", $dir, $topdir, $subdir);
460 if (exists $config{$topdir}{$subdir}{skip}) {
461 my $test="set -e;".$lib.
462 "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
463 print "mr $action: running skip test >>$test<<\n" if $verbose;
464 my $ret=system($test);
466 if (($? & 127) == 2) {
467 print STDERR "mr $action: interrupted\n";
471 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
475 if ($ret >> 8 == 0) {
476 print "mr $action: $dir skipped per config file\n" if $verbose;
482 if (! $nochdir && ! chdir($dir)) {
483 print STDERR "mr $action: failed to chdir to $dir: $!\n";
486 elsif (! exists $config{$topdir}{$subdir}{$action}) {
487 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
492 print "mr $action: $topdir$subdir\n";
495 print "mr $action: $topdir$subdir (in subdir $directory)\n";
497 my $command="set -e; ".$lib.
498 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
499 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
500 print STDERR "mr $action: running >>$command<<\n" if $verbose;
501 my $ret=system($command);
503 if (($? & 127) == 2) {
504 print STDERR "mr $action: interrupted\n";
508 print STDERR "mr $action: received signal ".($? & 127)."\n";
510 print STDERR "mr $action: failed ($ret)\n" if $verbose;
512 if ($ret >> 8 != 0) {
513 print STDERR "mr $action: command failed\n";
516 print STDERR "mr $action: command died ($ret)\n";
520 if ($action eq 'checkout' && ! -d $dir) {
521 print STDERR "mr $action: $dir missing after checkout\n";;
538 return "$count ".($count > 1 ? $plural : $singular);
542 if (! @ok && ! @failed && ! @skipped) {
543 die "mr $action: no repositories found to work on\n";
545 print "mr $action: finished (".join("; ",
546 showstat($#ok+1, "ok", "ok"),
547 showstat($#failed+1, "failed", "failed"),
548 showstat($#skipped+1, "skipped", "skipped"),
552 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
555 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
561 elsif (! @ok && @skipped) {
567 sub loadconfig { #{{{
574 if (ref $f eq 'GLOB') {
583 my $absf=abs_path($f);
584 if ($loaded{$absf}) {
589 ($dir)=$f=~/^(.*\/)[^\/]+$/;
590 if (! defined $dir) {
593 $dir=abs_path($dir)."/";
595 # copy in defaults from first parent
597 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
598 if ($parent eq '/') {
601 if (exists $config{$parent} &&
602 exists $config{$parent}{DEFAULT}) {
603 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
608 print "mr: loading config $f\n" if $verbose;
609 open($in, "<", $f) || die "mr: open $f: $!\n";
620 next if /^\s*\#/ || /^\s*$/;
621 if (/^\[([^\]]*)\]\s*$/) {
624 elsif (/^(\w+)\s*=\s*(.*)/) {
629 while (@lines && $lines[0]=~/^\s(.+)/) {
636 if (! defined $section) {
637 die "$f line $.: parameter ($parameter) not in section\n";
639 if ($section ne 'ALIAS' &&
640 ! exists $config{$dir}{$section} &&
641 exists $config{$dir}{DEFAULT}) {
643 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
645 if ($section eq 'ALIAS') {
646 $alias{$parameter}=$value;
648 elsif ($parameter eq 'lib') {
649 $config{$dir}{$section}{lib}.=$value."\n";
652 $config{$dir}{$section}{$parameter}=$value;
653 $knownactions{$parameter}=1;
654 if (! exists $configfiles{$dir}) {
655 $configfiles{$dir}=abs_path($f);
657 if ($parameter eq 'chain' &&
658 length $dir && $section ne "DEFAULT" &&
659 -e $dir.$section."/.mrconfig") {
660 my $ret=system($value);
662 if (($? & 127) == 2) {
663 print STDERR "mr $action: chain test interrupted\n";
667 print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
671 push @toload, $dir.$section."/.mrconfig";
677 die "$f line $line: parse error\n";
686 sub modifyconfig { #{{{
688 # the section to modify or add
689 my $targetsection=shift;
690 # fields to change in the section
691 # To remove a field, set its value to "".
698 open(my $in, "<", $f) || die "mr: open $f: $!\n";
703 my $formatfield=sub {
705 my @value=split(/\n/, shift);
707 return "$field = ".shift(@value)."\n".
708 join("", map { "\t$_\n" } @value);
712 while ($out[$#out] =~ /^\s*$/) {
713 unshift @blanks, pop @out;
715 foreach my $field (sort keys %changefields) {
716 if (length $changefields{$field}) {
717 push @out, "$field = $changefields{$field}\n";
718 delete $changefields{$field};
728 if (/^\s*\#/ || /^\s*$/) {
731 elsif (/^\[([^\]]*)\]\s*$/) {
732 if (defined $section &&
733 $section eq $targetsection) {
741 elsif (/^(\w+)\s*=\s(.*)/) {
746 while (@lines && $lines[0]=~/^\s(.+)/) {
752 if ($section eq $targetsection) {
753 if (exists $changefields{$parameter}) {
754 if (length $changefields{$parameter}) {
755 $value=$changefields{$parameter};
757 delete $changefields{$parameter};
761 push @out, $formatfield->($parameter, $value);
765 if (defined $section &&
766 $section eq $targetsection) {
769 elsif (%changefields) {
770 push @out, "\n[$targetsection]\n";
771 foreach my $field (sort keys %changefields) {
772 if (length $changefields{$field}) {
773 push @out, $formatfield->($field, $changefields{$field});
778 open(my $out, ">", $f) || die "mr: write $f: $!\n";
783 # Finally, some useful actions that mr knows about by default.
784 # These can be overridden in ~/.mrconfig.
799 for dir in .git .svn .bzr CVS; do
800 if [ -e "$MR_REPO/$dir" ]; then
801 flagfile="$MR_REPO/$dir/.mr_last$1"
805 if [ -z "$flagfile" ]; then
806 error "cannot determine flag filename"
808 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
813 if [ -d "$MR_REPO"/.svn ]; then
815 elif [ -d "$MR_REPO"/.git ]; then
816 git pull origin master "$@"
817 elif [ -d "$MR_REPO"/.bzr ]; then
819 elif [ -d "$MR_REPO"/CVS ]; then
822 error "unknown repo type"
825 if [ -d "$MR_REPO"/.svn ]; then
827 elif [ -d "$MR_REPO"/.git ]; then
828 git status "$@" || true
829 elif [ -d "$MR_REPO"/.bzr ]; then
831 elif [ -d "$MR_REPO"/CVS ]; then
834 error "unknown repo type"
837 if [ -d "$MR_REPO"/.svn ]; then
839 elif [ -d "$MR_REPO"/.git ]; then
840 git commit -a "$@" && git push --all
841 elif [ -d "$MR_REPO"/.bzr ]; then
842 bzr commit "$@" && bzr push
843 elif [ -d "$MR_REPO"/CVS ]; then
846 error "unknown repo type"
849 if [ -d "$MR_REPO"/.svn ]; then
851 elif [ -d "$MR_REPO"/.git ]; then
853 elif [ -d "$MR_REPO"/.bzr ]; then
855 elif [ -d "$MR_REPO"/CVS ]; then
858 error "unknown repo type"
861 if [ -d "$MR_REPO"/.svn ]; then
863 elif [ -d "$MR_REPO"/.git ]; then
865 elif [ -d "$MR_REPO"/.bzr ]; then
867 elif [ -d "$MR_REPO"/CVS ]; then
870 error "unknown repo type"
876 basedir="$(basename $(pwd))"
878 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
879 if [ -z "$url" ]; then
880 error "cannot determine svn url"
882 echo "Registering svn url: $url in $MR_CONFIG"
883 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
884 elif [ -d .git ]; then
885 url=$(LANG=C git-config --get remote.origin.url)
886 if [ -z "$url" ]; then
887 error "cannot determine git url"
889 echo "Registering git url: $url in $MR_CONFIG"
890 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
891 elif [ -d .bzr ]; then
892 url=$(cat .bzr/branch/parent)
893 if [ -z "$url" ]; then
894 error "cannot determine bzr url"
896 echo "Registering bzr url: $url in $MR_CONFIG"
897 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
899 error "unable to register this repo type"
902 if [ ! -e "$MR_PATH" ]; then
903 error "cannot find program path"
905 (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
909 ed = echo "A horse is a horse, of course, of course.."
910 T = echo "I pity the fool."
911 right = echo "Not found."