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] register [repository]
23 B<mr> [options] config section [parameter=[value] ...]
25 B<mr> [options] action [params ...]
29 B<mr> is a Multiple Repository management tool. It allows you to register a
30 set of repositories in a .mrconfig file, and then checkout, update, or
31 perform other actions on the repositories as if they were one big
34 Any mix of revision control systems can be used with B<mr>, and you can
35 define arbitrary actions for commands like "update", "checkout", or "commit".
37 B<mr> cds into and operates on all registered repsitories 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 The 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 List the repositories that mr will act on.
83 Register an existing repository in the mrconfig file. By default, the
84 epository in the current directory is registered, or you can specify a
85 directory to register.
89 Modifies the mrconfig file. The next parameter is the name of the section
90 to add or modify, and it is followed by one or more instances of
91 "parameter=value". Use "parameter=" to remove a parameter.
93 For example, to add (or edit) a repository in src/foo:
95 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
103 Actions can be abbreviated to any unambiguous subsctring, so
104 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
107 Additional parameters can be passed to most commands, and are passed on
108 unchanged to the underlying revision control system. This is mostly useful
109 if the repositories mr will act on all use the same revision control
118 Specifies the topmost directory that B<mr> should work in. The default is
119 the current working directory.
123 Use the specified mrconfig file, instead of looking for one in your home
134 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
135 file in your home directory, and this can in turn chain load .mrconfig files
138 Here is an example .mrconfig file:
141 checkout = svn co svn://svn.example.com/src/trunk src
145 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
147 The .mrconfig file uses a variant of the INI file format. Lines starting with
148 "#" are comments. Lines ending with "\" are continued on to the next line.
150 The "DEFAULT" section allows setting default values for the sections that
153 The "ALIAS" section allows adding aliases for actions. Each parameter
154 is an alias, and its value is the action to use.
156 All other sections add repositories. The section header specifies the
157 directory where the repository is located. This is relative to the directory
158 that contains the mrconfig file, but you can also choose to use absolute
161 Within a section, each parameter defines a shell command to run to handle a
162 given action. mr contains default handlers for the "update", "status", and
163 "commit" actions, so normally you only need to specify what to do for
166 Note that these shell commands are run in a "set -e" shell
167 environment, where any additional parameters you pass are available in
168 "$@". The "checkout" command is run in the parent of the repository
169 directory, since the repository isn't checked out yet. All other commands
170 are run inside the repository, though not necessarily at the top of it.
171 The "MR_REPO" environment variable is set to the path to the top of the
174 A few parameters have special meanings:
180 If the "skip" parameter is set and its command returns nonzero, then B<mr>
181 will skip acting on that repository.
185 If the "chain" parameter is set and its command returns nonzero, then B<mr>
186 will try to load a .mrconfig file from the root of the repository. (You
187 should avoid chaining from repositories with untrusted committers.)
191 If the "deleted" parameter is set and its command returns nonzero, then
192 B<mr> will treat the repository as deleted. It won't ever actually delete
193 the repository, but it will warn if it sees the repsoitory's directory.
194 This is useful when one mrconfig file is shared amoung multiple machines,
195 to keep track of and remember to delete old repositories.
199 The "lib" parameter can specify some shell code that will be run before each
200 command, this can be a useful way to define shell functions for other commands
207 Copyright 2007 Joey Hess <joey@kitenet.net>
209 Licensed under the GNU GPL version 2 or higher.
211 http://kitenet.net/~joey/code/mr/
218 use Cwd qw(getcwd abs_path);
220 my $directory=getcwd();
221 my $config="$ENV{HOME}/.mrconfig";
227 Getopt::Long::Configure("no_permute");
228 my $result=GetOptions(
229 "d|directory=s" => sub { $directory=abs_path($_[1]) },
230 "c|config=s" => \$config,
231 "verbose" => \$verbose,
233 if (! $result || @ARGV < 1) {
234 die("Usage: mr [-d directory] action [params ...]\n".
235 "(Use mr help for man page.)\n");
242 #print Dumper(\%config);
245 use FindBin qw($Bin $Script);
246 $ENV{MR_PATH}=$Bin."/".$Script;
249 # alias expansion and command stemming
250 my $action=shift @ARGV;
251 if (exists $alias{$action}) {
252 $action=$alias{$action};
254 if (! exists $knownactions{$action}) {
255 my @matches = grep { /^\Q$action\E/ }
256 keys %knownactions, keys %alias;
260 elsif (@matches == 0) {
261 die "mr: unknown action \"$action\" (known actions: ".
262 join(", ", sort keys %knownactions).")\n";
265 die "mr: ambiguous action \"$action\" (matches: ".
266 join(", ", @matches).")\n";
270 if ($action eq 'help') {
271 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
273 elsif ($action eq 'config') {
275 die "mr config: not enough parameters\n";
278 if ($section=~/^\//) {
279 # try to convert to a path relative to $config's dir
280 my ($dir)=$config=~/^(.*\/)[^\/]+$/;
281 if ($section=~/^\Q$dir\E(.*)/) {
287 if (/^([^=]+)=(.*)$/) {
291 die "mr config: expected parameter=value, not \"$_\"\n";
294 modifyconfig($config, $section, %fields);
297 elsif ($action eq 'register') {
298 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
299 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
300 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
301 print STDERR "mr $action: running >>$command<<\n" if $verbose;
302 exec($command) || die "exec: $!";
305 # work out what repos to act on
308 foreach my $topdir (sort keys %config) {
309 foreach my $subdir (sort keys %{$config{$topdir}}) {
310 next if $subdir eq 'DEFAULT';
311 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
313 $dir.="/" unless $dir=~/\/$/;
314 $d.="/" unless $d=~/\/$/;
315 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
316 push @repos, [$dir, $topdir, $subdir];
320 # fallback to find a leaf repo
321 LEAF: foreach my $topdir (reverse sort keys %config) {
322 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
323 next if $subdir eq 'DEFAULT';
324 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
326 $dir.="/" unless $dir=~/\/$/;
327 $d.="/" unless $d=~/\/$/;
328 if ($d=~/^\Q$dir\E/) {
329 push @repos, [$dir, $topdir, $subdir];
337 my (@failed, @successful, @skipped);
338 foreach my $repo (@repos) {
339 action($action, @$repo);
343 my ($action, $dir, $topdir, $subdir) = @_;
345 my $lib= exists $config{$topdir}{$subdir}{lib} ?
346 $config{$topdir}{$subdir}{lib}."\n" : "";
348 if (exists $config{$topdir}{$subdir}{deleted}) {
353 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
354 print "mr $action: running deleted test >>$test<<\n" if $verbose;
355 my $ret=system($test);
356 if ($ret >> 8 == 0) {
357 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
364 if ($action eq 'checkout') {
366 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
370 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
372 elsif ($action eq 'update') {
374 return action("checkout", $dir, $topdir, $subdir);
380 if (exists $config{$topdir}{$subdir}{skip}) {
381 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
382 print "mr $action: running skip test >>$test<<\n" if $verbose;
383 my $ret=system($test);
384 if ($ret >> 8 == 0) {
385 print "mr $action: $dir skipped per config file\n" if $verbose;
391 if (! $nochdir && ! chdir($dir)) {
392 print STDERR "mr $action: failed to chdir to $dir: $!\n";
395 elsif (! exists $config{$topdir}{$subdir}{$action}) {
396 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
401 print "mr $action: $topdir$subdir\n";
404 print "mr $action: $topdir$subdir (in subdir $directory)\n";
406 my $command="set -e; ".$lib.
407 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
408 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
409 print STDERR "mr $action: running >>$command<<\n" if $verbose;
410 my $ret=system($command);
412 print STDERR "mr $action: failed ($ret)\n" if $verbose;
414 if ($ret >> 8 != 0) {
415 print STDERR "mr $action: command failed\n";
418 print STDERR "mr $action: command died ($ret)\n";
422 push @successful, $dir;
434 return "$count ".($count > 1 ? $plural : $singular);
438 if (! @successful && ! @failed && ! @skipped) {
439 die "mr $action: no repositories found to work on\n";
441 print "mr $action: finished (".join("; ",
442 showstat($#successful+1, "successful", "successful"),
443 showstat($#failed+1, "failed", "failed"),
444 showstat($#skipped+1, "skipped", "skipped"),
449 elsif (! @successful && @skipped) {
462 if (ref $f eq 'GLOB') {
471 my $absf=abs_path($f);
472 if ($loaded{$absf}) {
477 print "mr: loading config $f\n" if $verbose;
478 open($in, "<", $f) || die "mr: open $f: $!\n";
479 ($dir)=$f=~/^(.*\/)[^\/]+$/;
480 if (! defined $dir) {
483 $dir=abs_path($dir)."/";
485 # copy in defaults from first parent
487 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
488 if (exists $config{$parent} &&
489 exists $config{$parent}{DEFAULT}) {
490 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
499 next if /^\s*\#/ || /^\s*$/;
500 if (/^\s*\[([^\]]*)\]\s*$/) {
503 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
508 while ($value=~/(.*)\\$/s) {
509 $value=$1."\n".<$in>;
513 if (! defined $section) {
514 die "$f line $.: parameter ($parameter) not in section\n";
516 if ($section ne 'ALIAS' &&
517 ! exists $config{$dir}{$section} &&
518 exists $config{$dir}{DEFAULT}) {
520 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
522 if ($section eq 'ALIAS') {
523 $alias{$parameter}=$value;
525 elsif ($parameter eq 'lib') {
526 $config{$dir}{$section}{lib}.=$value."\n";
529 $config{$dir}{$section}{$parameter}=$value;
530 $knownactions{$parameter}=1;
531 if ($parameter eq 'chain' &&
532 length $dir && $section ne "DEFAULT" &&
533 -e $dir.$section."/.mrconfig" &&
534 system($value) >> 8 == 0) {
535 push @toload, $dir.$section."/.mrconfig";
540 die "$f line $.: parse error\n";
552 # the section to modify or add
553 my $targetsection=shift;
554 # fields to change in the section
555 # To remove a field, set its value to "".
562 open(my $in, "<", $f) || die "mr: open $f: $!\n";
569 while ($out[$#out] =~ /^\s*$/) {
570 unshift @blanks, pop @out;
572 foreach my $field (sort keys %changefields) {
573 if (length $changefields{$field}) {
574 push @out, "$field = $changefields{$field}\n";
584 if (/^\s*\#/ || /^\s*$/) {
587 elsif (/^\s*\[([^\]]*)\]\s*$/) {
588 if (defined $section &&
589 $section eq $targetsection) {
597 elsif (/^\s*(\w+)\s*=\s(.*)/) {
602 while ($value=~/(.*\\)$/s) {
603 $value=$1."\n".shift(@lines);
607 if ($section eq $targetsection) {
608 if (exists $changefields{$parameter}) {
609 if (length $changefields{$parameter}) {
610 $value=$changefields{$parameter};
612 delete $changefields{$parameter};
616 push @out, "$parameter = $value\n";
620 if (defined $section &&
621 $section eq $targetsection) {
624 elsif (%changefields) {
625 push @out, "\n[$targetsection]\n";
626 foreach my $field (sort keys %changefields) {
627 if (length $changefields{$field}) {
628 push @out, "$field = $changefields{$field}\n";
633 open(my $out, ">", $f) || die "mr: write $f: $!\n";
638 # Finally, some useful actions that mr knows about by default.
639 # These can be overridden in ~/.mrconfig.
654 if [ -d "$MR_REPO"/.svn ]; then \
656 elif [ -d "$MR_REPO"/.git ]; then \
657 git pull origin master "$@" \
658 elif [ -d "$MR_REPO"/.bzr ]; then \
660 elif [ -d "$MR_REPO"/CVS ]; then \
663 error "unknown repo type" \
666 if [ -d "$MR_REPO"/.svn ]; then \
668 elif [ -d "$MR_REPO"/.git ]; then \
669 git status "$@" || true \
670 elif [ -d "$MR_REPO"/.bzr ]; then \
672 elif [ -d "$MR_REPO"/CVS ]; then \
675 error "unknown repo type" \
678 if [ -d "$MR_REPO"/.svn ]; then \
680 elif [ -d "$MR_REPO"/.git ]; then \
681 git commit -a "$@" && git push --all \
682 elif [ -d "$MR_REPO"/.bzr ]; then \
683 bzr commit "$@" && bzr push \
684 elif [ -d "$MR_REPO"/CVS ]; then \
687 error "unknown repo type" \
690 if [ -d "$MR_REPO"/.svn ]; then \
692 elif [ -d "$MR_REPO"/.git ]; then \
694 elif [ -d "$MR_REPO"/.bzr ]; then \
696 elif [ -d "$MR_REPO"/CVS ]; then \
699 error "unknown repo type" \
702 if [ -d "$MR_REPO"/.svn ]; then \
704 elif [ -d "$MR_REPO"/.git ]; then \
706 elif [ -d "$MR_REPO"/.bzr ]; then \
708 elif [ -d "$MR_REPO"/CVS ]; then \
711 error "unknown repo type" \
714 if [ -n "$1" ]; then \
717 basedir="$(basename $(pwd))" \
718 if [ -d .svn ]; then \
719 url=$(LANG=C svn info . | \
720 grep -i ^URL: | cut -d ' ' -f 2) \
721 if [ -z "$url" ]; then \
722 error "cannot determine svn url" \
724 echo "Registering svn url: $url" \
725 mr config "$(pwd)" checkout="svn co $url $basedir" \
726 elif [ -d .git ]; then \
727 url=$(LANG=C git-config --get remote.origin.url) \
728 if [ -z "$url" ]; then \
729 error "cannot determine git url" \
731 echo "Registering git url: $url" \
732 mr config "$(pwd)" checkout="git clone $url $basedir" \
733 elif [ -d .bzr ]; then \
734 url=$(cat .bzr/branch/parent) \
735 if [ -z "$url" ]; then \
736 error "cannot determine bzr url" \
738 echo "Registering bzr url: $url" \
739 mr config "$(pwd)" checkout="bzr clone $url $basedir" \
741 error "unable to register this repo type" \
746 if [ ! -e "$MR_PATH" ]; then \
747 error "cannot find program path" \
749 (pod2man -c mr "$MR_PATH" | man -l -) || \
750 error "pod2man or man failed"
752 ed = echo "A horse is a horse, of course, of course.."
753 T = echo "I pity the fool."