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 a mrconfig file. By default, the
90 repository in the current directory is registered, or you can specify a
91 directory to register.
93 The mrconfig file that is modified is chosen by either the -c option, or by
94 looking for the closest known one at or below the current directory.
98 Adds, modifies, removes, or prints a value from a 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
111 The ~/.mrconfig file is used by default. To use a different config file,
120 Actions can be abbreviated to any unambiguous subsctring, so
121 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
124 Additional parameters can be passed to most commands, and are passed on
125 unchanged to the underlying revision control system. This is mostly useful
126 if the repositories mr will act on all use the same revision control
135 Specifies the topmost directory that B<mr> should work in. The default is
136 the current working directory.
140 Use the specified mrconfig file. The default is B<~/.mrconfig>
148 Expand the statistics line displayed at the end to include information
149 about exactly which repositories failed and were skipped, if any.
153 Just operate on the repository for the current directory, do not
154 recurse into deeper repositories.
160 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
161 file in your home directory, and this can in turn chain load .mrconfig files
164 Here is an example .mrconfig file:
167 checkout = svn co svn://svn.example.com/src/trunk src
171 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
173 git checkout -b mybranch origin/master
175 The .mrconfig file uses a variant of the INI file format. Lines starting with
176 "#" are comments. Values can be continued to the following line by
177 indenting the line with whitespace.
179 The "DEFAULT" section allows setting default values for the sections that
182 The "ALIAS" section allows adding aliases for actions. Each parameter
183 is an alias, and its value is the action to use.
185 All other sections add repositories. The section header specifies the
186 directory where the repository is located. This is relative to the directory
187 that contains the mrconfig file, but you can also choose to use absolute
190 Within a section, each parameter defines a shell command to run to handle a
191 given action. mr contains default handlers for the "update", "status", and
192 "commit" actions, so normally you only need to specify what to do for
195 Note that these shell commands are run in a "set -e" shell
196 environment, where any additional parameters you pass are available in
197 "$@". The "checkout" command is run in the parent of the repository
198 directory, since the repository isn't checked out yet. All other commands
199 are run inside the repository, though not necessarily at the top of it.
201 The "MR_REPO" environment variable is set to the path to the top of the
202 repository. The "MR_CONFIG" environment variable is set to the .mrconfig file
203 that defines the repo being acted on, or, if the repo is not yet in a config
204 file, the .mrconfig file that should be modified to register the repo.
206 A few parameters have special meanings:
212 If the "skip" parameter is set and its command returns true, then B<mr>
213 will skip acting on that repository. The command is passed the action
216 Here are two examples. The first skips the repo unless
217 mr is run by joey. The second uses the hours_since function
218 (included in mr's built-in library) to skip updating the repo unless it's
219 been at least 12 hours since the last update.
221 skip = test $(whoami) != joey
222 skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
226 If the "chain" parameter is set and its command returns true, then B<mr>
227 will try to load a .mrconfig file from the root of the repository. (You
228 should avoid chaining from repositories with untrusted committers.)
232 If the "deleted" parameter is set and its command returns true, then
233 B<mr> will treat the repository as deleted. It won't ever actually delete
234 the repository, but it will warn if it sees the repository's directory.
235 This is useful when one mrconfig file is shared amoung multiple machines,
236 to keep track of and remember to delete old repositories.
240 The "lib" parameter can specify some shell code that will be run before each
241 command, this can be a useful way to define shell functions for other commands
248 Copyright 2007 Joey Hess <joey@kitenet.net>
250 Licensed under the GNU GPL version 2 or higher.
252 http://kitenet.net/~joey/code/mr/
261 use Cwd qw(getcwd abs_path);
264 print STDERR "mr: interrupted\n";
268 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
269 my $config_overridden=0;
270 my $directory=getcwd();
279 Getopt::Long::Configure("no_permute");
280 my $result=GetOptions(
281 "d|directory=s" => sub { $directory=abs_path($_[1]) },
282 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
283 "v|verbose" => \$verbose,
284 "s|stats" => \$stats,
285 "n|no-recurse" => \$no_recurse,
287 if (! $result || @ARGV < 1) {
288 die("Usage: mr [-d directory] action [params ...]\n".
289 "(Use mr help for man page.)\n");
293 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
294 # the config file might be a symlink to elsewhere, and the directory it's
296 if ($ENV{MR_CONFIG} !~ /^\//) {
297 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
301 loadconfig($ENV{MR_CONFIG});
303 #print Dumper(\%config);
306 use FindBin qw($Bin $Script);
307 $ENV{MR_PATH}=$Bin."/".$Script;
310 # alias expansion and command stemming
311 my $action=shift @ARGV;
312 if (exists $alias{$action}) {
313 $action=$alias{$action};
315 if (! exists $knownactions{$action}) {
316 my @matches = grep { /^\Q$action\E/ }
317 keys %knownactions, keys %alias;
321 elsif (@matches == 0) {
322 die "mr: unknown action \"$action\" (known actions: ".
323 join(", ", sort keys %knownactions).")\n";
326 die "mr: ambiguous action \"$action\" (matches: ".
327 join(", ", @matches).")\n";
331 # commands that do not operate on all repos
332 if ($action eq 'help') {
333 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
335 elsif ($action eq 'config') {
337 die "mr config: not enough parameters\n";
340 if ($section=~/^\//) {
341 # try to convert to a path relative to the config file
342 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
344 $dir.="/" unless $dir=~/\/$/;
345 if ($section=~/^\Q$dir\E(.*)/) {
351 if (/^([^=]+)=(.*)$/) {
352 $changefields{$1}=$2;
356 foreach my $topdir (sort keys %config) {
357 if (exists $config{$topdir}{$section} &&
358 exists $config{$topdir}{$section}{$_}) {
359 print $config{$topdir}{$section}{$_}."\n";
364 die "mr $action: $section $_ not set\n";
368 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
371 elsif ($action eq 'register') {
372 if (! $config_overridden) {
373 # Find the closest known mrconfig file to the current
375 $directory.="/" unless $directory=~/\/$/;
376 foreach my $topdir (reverse sort keys %config) {
377 next unless length $topdir;
378 if ($directory=~/^\Q$topdir\E/) {
379 $ENV{MR_CONFIG}=$configfiles{$topdir};
384 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
385 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
386 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
387 print STDERR "mr $action: running >>$command<<\n" if $verbose;
388 exec($command) || die "exec: $!";
391 # work out what repos to act on
394 foreach my $topdir (sort keys %config) {
395 foreach my $subdir (sort keys %{$config{$topdir}}) {
396 next if $subdir eq 'DEFAULT';
397 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
399 $dir.="/" unless $dir=~/\/$/;
400 $d.="/" unless $d=~/\/$/;
401 next if $no_recurse && $d ne $dir;
402 next if $dir ne $d && $dir !~ /^\Q$d\E/;
403 push @repos, [$dir, $topdir, $subdir];
407 # fallback to find a leaf repo
408 LEAF: foreach my $topdir (reverse sort keys %config) {
409 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
410 next if $subdir eq 'DEFAULT';
411 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
413 $dir.="/" unless $dir=~/\/$/;
414 $d.="/" unless $d=~/\/$/;
415 if ($d=~/^\Q$dir\E/) {
416 push @repos, [$dir, $topdir, $subdir];
424 my (@failed, @ok, @skipped);
425 foreach my $repo (@repos) {
426 action($action, @$repo);
430 my ($action, $dir, $topdir, $subdir) = @_;
432 $ENV{MR_CONFIG}=$configfiles{$topdir};
433 my $lib=exists $config{$topdir}{$subdir}{lib} ?
434 $config{$topdir}{$subdir}{lib}."\n" : "";
436 if (exists $config{$topdir}{$subdir}{deleted}) {
437 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
438 print "mr $action: running deleted test >>$test<<\n" if $verbose;
439 my $ret=system($test);
441 if (($? & 127) == 2) {
442 print STDERR "mr $action: interrupted\n";
446 print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
449 if ($ret >> 8 == 0) {
451 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
456 print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
463 if ($action eq 'checkout') {
465 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
470 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
473 print "mr $action: creating parent directory $dir\n" if $verbose;
474 my $ret=system("mkdir", "-p", $dir);
477 elsif ($action eq 'update') {
479 return action("checkout", $dir, $topdir, $subdir);
485 if (exists $config{$topdir}{$subdir}{skip}) {
486 my $test="set -e;".$lib.
487 "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
488 print "mr $action: running skip test >>$test<<\n" if $verbose;
489 my $ret=system($test);
491 if (($? & 127) == 2) {
492 print STDERR "mr $action: interrupted\n";
496 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
500 if ($ret >> 8 == 0) {
501 print "mr $action: $dir skipped per config file\n" if $verbose;
507 if (! $nochdir && ! chdir($dir)) {
508 print STDERR "mr $action: failed to chdir to $dir: $!\n";
511 elsif (! exists $config{$topdir}{$subdir}{$action}) {
512 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
517 print "mr $action: $topdir$subdir\n";
520 print "mr $action: $topdir$subdir (in subdir $directory)\n";
522 my $command="set -e; ".$lib.
523 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
524 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
525 print STDERR "mr $action: running >>$command<<\n" if $verbose;
526 my $ret=system($command);
528 if (($? & 127) == 2) {
529 print STDERR "mr $action: interrupted\n";
533 print STDERR "mr $action: received signal ".($? & 127)."\n";
535 print STDERR "mr $action: failed ($ret)\n" if $verbose;
537 if ($ret >> 8 != 0) {
538 print STDERR "mr $action: command failed\n";
541 print STDERR "mr $action: command died ($ret)\n";
545 if ($action eq 'checkout' && ! -d $dir) {
546 print STDERR "mr $action: $dir missing after checkout\n";;
563 return "$count ".($count > 1 ? $plural : $singular);
567 if (! @ok && ! @failed && ! @skipped) {
568 die "mr $action: no repositories found to work on\n";
570 print "mr $action: finished (".join("; ",
571 showstat($#ok+1, "ok", "ok"),
572 showstat($#failed+1, "failed", "failed"),
573 showstat($#skipped+1, "skipped", "skipped"),
577 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
580 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
586 elsif (! @ok && @skipped) {
592 sub loadconfig { #{{{
599 if (ref $f eq 'GLOB') {
608 my $absf=abs_path($f);
609 if ($loaded{$absf}) {
614 ($dir)=$f=~/^(.*\/)[^\/]+$/;
615 if (! defined $dir) {
618 $dir=abs_path($dir)."/";
620 if (! exists $configfiles{$dir}) {
621 $configfiles{$dir}=$f;
624 # copy in defaults from first parent
626 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
627 if ($parent eq '/') {
630 if (exists $config{$parent} &&
631 exists $config{$parent}{DEFAULT}) {
632 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
637 print "mr: loading config $f\n" if $verbose;
638 open($in, "<", $f) || die "mr: open $f: $!\n";
649 next if /^\s*\#/ || /^\s*$/;
650 if (/^\[([^\]]*)\]\s*$/) {
653 elsif (/^(\w+)\s*=\s*(.*)/) {
658 while (@lines && $lines[0]=~/^\s(.+)/) {
665 if (! defined $section) {
666 die "$f line $.: parameter ($parameter) not in section\n";
668 if ($section ne 'ALIAS' &&
669 ! exists $config{$dir}{$section} &&
670 exists $config{$dir}{DEFAULT}) {
672 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
674 if ($section eq 'ALIAS') {
675 $alias{$parameter}=$value;
677 elsif ($parameter eq 'lib') {
678 $config{$dir}{$section}{lib}.=$value."\n";
681 $config{$dir}{$section}{$parameter}=$value;
682 $knownactions{$parameter}=1;
683 if ($parameter eq 'chain' &&
684 length $dir && $section ne "DEFAULT" &&
685 -e $dir.$section."/.mrconfig") {
686 my $ret=system($value);
688 if (($? & 127) == 2) {
689 print STDERR "mr $action: chain test interrupted\n";
693 print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
697 push @toload, $dir.$section."/.mrconfig";
703 die "$f line $line: parse error\n";
712 sub modifyconfig { #{{{
714 # the section to modify or add
715 my $targetsection=shift;
716 # fields to change in the section
717 # To remove a field, set its value to "".
724 open(my $in, "<", $f) || die "mr: open $f: $!\n";
729 my $formatfield=sub {
731 my @value=split(/\n/, shift);
733 return "$field = ".shift(@value)."\n".
734 join("", map { "\t$_\n" } @value);
738 while ($out[$#out] =~ /^\s*$/) {
739 unshift @blanks, pop @out;
741 foreach my $field (sort keys %changefields) {
742 if (length $changefields{$field}) {
743 push @out, "$field = $changefields{$field}\n";
744 delete $changefields{$field};
754 if (/^\s*\#/ || /^\s*$/) {
757 elsif (/^\[([^\]]*)\]\s*$/) {
758 if (defined $section &&
759 $section eq $targetsection) {
767 elsif (/^(\w+)\s*=\s(.*)/) {
772 while (@lines && $lines[0]=~/^\s(.+)/) {
778 if ($section eq $targetsection) {
779 if (exists $changefields{$parameter}) {
780 if (length $changefields{$parameter}) {
781 $value=$changefields{$parameter};
783 delete $changefields{$parameter};
787 push @out, $formatfield->($parameter, $value);
791 if (defined $section &&
792 $section eq $targetsection) {
795 elsif (%changefields) {
796 push @out, "\n[$targetsection]\n";
797 foreach my $field (sort keys %changefields) {
798 if (length $changefields{$field}) {
799 push @out, $formatfield->($field, $changefields{$field});
804 open(my $out, ">", $f) || die "mr: write $f: $!\n";
809 # Finally, some useful actions that mr knows about by default.
810 # These can be overridden in ~/.mrconfig.
825 for dir in .git .svn .bzr CVS; do
826 if [ -e "$MR_REPO/$dir" ]; then
827 flagfile="$MR_REPO/$dir/.mr_last$1"
831 if [ -z "$flagfile" ]; then
832 error "cannot determine flag filename"
834 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
839 if [ -d "$MR_REPO"/.svn ]; then
841 elif [ -d "$MR_REPO"/.git ]; then
842 git pull origin master "$@"
843 elif [ -d "$MR_REPO"/.bzr ]; then
845 elif [ -d "$MR_REPO"/CVS ]; then
848 error "unknown repo type"
851 if [ -d "$MR_REPO"/.svn ]; then
853 elif [ -d "$MR_REPO"/.git ]; then
854 git status "$@" || true
855 elif [ -d "$MR_REPO"/.bzr ]; then
857 elif [ -d "$MR_REPO"/CVS ]; then
860 error "unknown repo type"
863 if [ -d "$MR_REPO"/.svn ]; then
865 elif [ -d "$MR_REPO"/.git ]; then
866 git commit -a "$@" && git push --all
867 elif [ -d "$MR_REPO"/.bzr ]; then
868 bzr commit "$@" && bzr push
869 elif [ -d "$MR_REPO"/CVS ]; then
872 error "unknown repo type"
875 if [ -d "$MR_REPO"/.svn ]; then
877 elif [ -d "$MR_REPO"/.git ]; then
879 elif [ -d "$MR_REPO"/.bzr ]; then
881 elif [ -d "$MR_REPO"/CVS ]; then
884 error "unknown repo type"
887 if [ -d "$MR_REPO"/.svn ]; then
889 elif [ -d "$MR_REPO"/.git ]; then
891 elif [ -d "$MR_REPO"/.bzr ]; then
893 elif [ -d "$MR_REPO"/CVS ]; then
896 error "unknown repo type"
902 basedir="$(basename $(pwd))"
904 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
905 if [ -z "$url" ]; then
906 error "cannot determine svn url"
908 echo "Registering svn url: $url in $MR_CONFIG"
909 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
910 elif [ -d .git ]; then
911 url=$(LANG=C git-config --get remote.origin.url)
912 if [ -z "$url" ]; then
913 error "cannot determine git url"
915 echo "Registering git url: $url in $MR_CONFIG"
916 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
917 elif [ -d .bzr ]; then
918 url=$(cat .bzr/branch/parent)
919 if [ -z "$url" ]; then
920 error "cannot determine bzr url"
922 echo "Registering bzr url: $url in $MR_CONFIG"
923 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
924 elif [ -d CVS ]; then
925 repo=$(cat CVS/Repository)
927 if [ -z "$root" ]; then
928 error "cannot determine cvs root"
930 echo "Registering cvs repository $repo at root $root"
931 mr -c "$MR_CONFIG" config "$(pwd)" \
932 checkout="cvs -d '$root' co -d $basedir $repo"
934 error "unable to register this repo type"
937 if [ ! -e "$MR_PATH" ]; then
938 error "cannot find program path"
940 (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
944 ed = echo "A horse is a horse, of course, of course.."
945 T = echo "I pity the fool."
946 right = echo "Not found."