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
30 can checkout, update, or perform other actions on
31 a set of repositories as if they were one combined respository. It
32 supports any combination of subversion, git, cvs, and bzr repositories,
33 and support for other revision control systems can easily be added.
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 Register an existing repository in the mrconfig file. By default, the
82 epository in the current directory is registered, or you can specify a
83 directory to register.
87 Adds, modifies, removes, or prints a value from the mrconfig file. The next
88 parameter is the name of the section the value is in. To add or modify
89 values, use one or more instances of "parameter=value". Use "parameter=" to
90 remove a parameter. Use just "parameter" to get the value of a parameter.
92 For example, to add (or edit) a repository in src/foo:
94 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
96 To show the command that mr uses to update the repository in src/foo:
98 mr config src/foo update
106 Actions can be abbreviated to any unambiguous subsctring, so
107 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
110 Additional parameters can be passed to most commands, and are passed on
111 unchanged to the underlying revision control system. This is mostly useful
112 if the repositories mr will act on all use the same revision control
121 Specifies the topmost directory that B<mr> should work in. The default is
122 the current working directory.
126 Use the specified mrconfig file, instead of looking for one in your home
137 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
138 file in your home directory, and this can in turn chain load .mrconfig files
141 Here is an example .mrconfig file:
144 checkout = svn co svn://svn.example.com/src/trunk src
148 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
150 git checkout -b mybranch origin/master
152 The .mrconfig file uses a variant of the INI file format. Lines starting with
153 "#" are comments. Values can be continued to the following line by
154 indenting the line with whitespace.
156 The "DEFAULT" section allows setting default values for the sections that
159 The "ALIAS" section allows adding aliases for actions. Each parameter
160 is an alias, and its value is the action to use.
162 All other sections add repositories. The section header specifies the
163 directory where the repository is located. This is relative to the directory
164 that contains the mrconfig file, but you can also choose to use absolute
167 Within a section, each parameter defines a shell command to run to handle a
168 given action. mr contains default handlers for the "update", "status", and
169 "commit" actions, so normally you only need to specify what to do for
172 Note that these shell commands are run in a "set -e" shell
173 environment, where any additional parameters you pass are available in
174 "$@". The "checkout" command is run in the parent of the repository
175 directory, since the repository isn't checked out yet. All other commands
176 are run inside the repository, though not necessarily at the top of it.
177 The "MR_REPO" environment variable is set to the path to the top of the
180 A few parameters have special meanings:
186 If the "skip" parameter is set and its command returns nonzero, then B<mr>
187 will skip acting on that repository. The command is passed the action
190 Here are two examples. The first skips the repo unless
191 mr is run by joey. The second uses the hours_since function
192 (included in mr's built-in library) to skip updating the repo unless it's
193 been at least 12 hours since the last update.
195 skip = test $(whoami) != joey
196 skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
200 If the "chain" parameter is set and its command returns nonzero, then B<mr>
201 will try to load a .mrconfig file from the root of the repository. (You
202 should avoid chaining from repositories with untrusted committers.)
206 If the "deleted" parameter is set and its command returns nonzero, then
207 B<mr> will treat the repository as deleted. It won't ever actually delete
208 the repository, but it will warn if it sees the repsoitory's directory.
209 This is useful when one mrconfig file is shared amoung multiple machines,
210 to keep track of and remember to delete old repositories.
214 The "lib" parameter can specify some shell code that will be run before each
215 command, this can be a useful way to define shell functions for other commands
222 Copyright 2007 Joey Hess <joey@kitenet.net>
224 Licensed under the GNU GPL version 2 or higher.
226 http://kitenet.net/~joey/code/mr/
233 use Cwd qw(getcwd abs_path);
235 my $directory=getcwd();
236 my $config="$ENV{HOME}/.mrconfig";
242 Getopt::Long::Configure("no_permute");
243 my $result=GetOptions(
244 "d|directory=s" => sub { $directory=abs_path($_[1]) },
245 "c|config=s" => \$config,
246 "verbose" => \$verbose,
248 if (! $result || @ARGV < 1) {
249 die("Usage: mr [-d directory] action [params ...]\n".
250 "(Use mr help for man page.)\n");
257 #print Dumper(\%config);
260 use FindBin qw($Bin $Script);
261 $ENV{MR_PATH}=$Bin."/".$Script;
264 # alias expansion and command stemming
265 my $action=shift @ARGV;
266 if (exists $alias{$action}) {
267 $action=$alias{$action};
269 if (! exists $knownactions{$action}) {
270 my @matches = grep { /^\Q$action\E/ }
271 keys %knownactions, keys %alias;
275 elsif (@matches == 0) {
276 die "mr: unknown action \"$action\" (known actions: ".
277 join(", ", sort keys %knownactions).")\n";
280 die "mr: ambiguous action \"$action\" (matches: ".
281 join(", ", @matches).")\n";
285 if ($action eq 'help') {
286 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
288 elsif ($action eq 'config') {
290 die "mr config: not enough parameters\n";
293 if ($section=~/^\//) {
294 # try to convert to a path relative to $config's dir
295 my ($dir)=$config=~/^(.*\/)[^\/]+$/;
296 if ($section=~/^\Q$dir\E(.*)/) {
302 if (/^([^=]+)=(.*)$/) {
303 $changefields{$1}=$2;
307 foreach my $topdir (sort keys %config) {
308 if (exists $config{$topdir}{$section} &&
309 exists $config{$topdir}{$section}{$_}) {
310 print $config{$topdir}{$section}{$_}."\n";
315 die "mr $action: $section $_ not set\n";
319 modifyconfig($config, $section, %changefields) if %changefields;
322 elsif ($action eq 'register') {
323 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
324 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
325 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
326 print STDERR "mr $action: running >>$command<<\n" if $verbose;
327 exec($command) || die "exec: $!";
330 # work out what repos to act on
333 foreach my $topdir (sort keys %config) {
334 foreach my $subdir (sort keys %{$config{$topdir}}) {
335 next if $subdir eq 'DEFAULT';
336 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
338 $dir.="/" unless $dir=~/\/$/;
339 $d.="/" unless $d=~/\/$/;
340 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
341 push @repos, [$dir, $topdir, $subdir];
345 # fallback to find a leaf repo
346 LEAF: foreach my $topdir (reverse sort keys %config) {
347 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
348 next if $subdir eq 'DEFAULT';
349 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
351 $dir.="/" unless $dir=~/\/$/;
352 $d.="/" unless $d=~/\/$/;
353 if ($d=~/^\Q$dir\E/) {
354 push @repos, [$dir, $topdir, $subdir];
362 my (@failed, @successful, @skipped);
363 foreach my $repo (@repos) {
364 action($action, @$repo);
368 my ($action, $dir, $topdir, $subdir) = @_;
370 my $lib= exists $config{$topdir}{$subdir}{lib} ?
371 $config{$topdir}{$subdir}{lib}."\n" : "";
373 if (exists $config{$topdir}{$subdir}{deleted}) {
378 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
379 print "mr $action: running deleted test >>$test<<\n" if $verbose;
380 my $ret=system($test);
381 if ($ret >> 8 == 0) {
382 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
389 if ($action eq 'checkout') {
391 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
395 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
397 elsif ($action eq 'update') {
399 return action("checkout", $dir, $topdir, $subdir);
405 if (exists $config{$topdir}{$subdir}{skip}) {
406 my $test="set -e;".$lib.
407 "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
408 print "mr $action: running skip test >>$test<<\n" if $verbose;
409 my $ret=system($test);
410 if ($ret >> 8 == 0) {
411 print "mr $action: $dir skipped per config file\n" if $verbose;
417 if (! $nochdir && ! chdir($dir)) {
418 print STDERR "mr $action: failed to chdir to $dir: $!\n";
421 elsif (! exists $config{$topdir}{$subdir}{$action}) {
422 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
427 print "mr $action: $topdir$subdir\n";
430 print "mr $action: $topdir$subdir (in subdir $directory)\n";
432 my $command="set -e; ".$lib.
433 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
434 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
435 print STDERR "mr $action: running >>$command<<\n" if $verbose;
436 my $ret=system($command);
438 print STDERR "mr $action: failed ($ret)\n" if $verbose;
440 if ($ret >> 8 != 0) {
441 print STDERR "mr $action: command failed\n";
444 print STDERR "mr $action: command died ($ret)\n";
448 push @successful, $dir;
460 return "$count ".($count > 1 ? $plural : $singular);
464 if (! @successful && ! @failed && ! @skipped) {
465 die "mr $action: no repositories found to work on\n";
467 print "mr $action: finished (".join("; ",
468 showstat($#successful+1, "successful", "successful"),
469 showstat($#failed+1, "failed", "failed"),
470 showstat($#skipped+1, "skipped", "skipped"),
475 elsif (! @successful && @skipped) {
488 if (ref $f eq 'GLOB') {
497 my $absf=abs_path($f);
498 if ($loaded{$absf}) {
503 ($dir)=$f=~/^(.*\/)[^\/]+$/;
504 if (! defined $dir) {
507 $dir=abs_path($dir)."/";
509 # copy in defaults from first parent
511 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
512 if (exists $config{$parent} &&
513 exists $config{$parent}{DEFAULT}) {
514 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
519 print "mr: loading config $f\n" if $verbose;
520 open($in, "<", $f) || die "mr: open $f: $!\n";
529 next if /^\s*\#/ || /^\s*$/;
530 if (/^\[([^\]]*)\]\s*$/) {
533 elsif (/^(\w+)\s*=\s*(.*)/) {
538 while (@lines && $lines[0]=~/^\s(.+)/) {
544 if (! defined $section) {
545 die "$f line $.: parameter ($parameter) not in section\n";
547 if ($section ne 'ALIAS' &&
548 ! exists $config{$dir}{$section} &&
549 exists $config{$dir}{DEFAULT}) {
551 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
553 if ($section eq 'ALIAS') {
554 $alias{$parameter}=$value;
556 elsif ($parameter eq 'lib') {
557 $config{$dir}{$section}{lib}.=$value."\n";
560 $config{$dir}{$section}{$parameter}=$value;
561 $knownactions{$parameter}=1;
562 if ($parameter eq 'chain' &&
563 length $dir && $section ne "DEFAULT" &&
564 -e $dir.$section."/.mrconfig" &&
565 system($value) >> 8 == 0) {
566 push @toload, $dir.$section."/.mrconfig";
571 die "$f line $.: parse error\n";
582 # the section to modify or add
583 my $targetsection=shift;
584 # fields to change in the section
585 # To remove a field, set its value to "".
592 open(my $in, "<", $f) || die "mr: open $f: $!\n";
597 my $formatfield=sub {
599 my @value=split(/\n/, shift);
601 return "$field = ".shift(@value)."\n".
602 join("", map { "\t$_\n" } @value);
606 while ($out[$#out] =~ /^\s*$/) {
607 unshift @blanks, pop @out;
609 foreach my $field (sort keys %changefields) {
610 if (length $changefields{$field}) {
611 push @out, "$field = $changefields{$field}\n";
612 delete $changefields{$field};
622 if (/^\s*\#/ || /^\s*$/) {
625 elsif (/^\[([^\]]*)\]\s*$/) {
626 if (defined $section &&
627 $section eq $targetsection) {
635 elsif (/^(\w+)\s*=\s(.*)/) {
640 while (@lines && $lines[0]=~/^\s(.+)/) {
646 if ($section eq $targetsection) {
647 if (exists $changefields{$parameter}) {
648 if (length $changefields{$parameter}) {
649 $value=$changefields{$parameter};
651 delete $changefields{$parameter};
655 push @out, $formatfield->($parameter, $value);
659 if (defined $section &&
660 $section eq $targetsection) {
663 elsif (%changefields) {
664 push @out, "\n[$targetsection]\n";
665 foreach my $field (sort keys %changefields) {
666 if (length $changefields{$field}) {
667 push @out, $formatfield->($field, $changefields{$field});
672 open(my $out, ">", $f) || die "mr: write $f: $!\n";
677 # Finally, some useful actions that mr knows about by default.
678 # These can be overridden in ~/.mrconfig.
692 for dir in .git .svn .bzr CVS; do
693 if [ -e "$MR_REPO/$dir" ]; then
694 flagfile="$MR_REPO/$dir/.mr_last$1"
698 if [ -z "$flagfile" ]; then
699 error "cannot determine flag filename"
701 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
706 if [ -d "$MR_REPO"/.svn ]; then
708 elif [ -d "$MR_REPO"/.git ]; then
709 git pull origin master "$@"
710 elif [ -d "$MR_REPO"/.bzr ]; then
712 elif [ -d "$MR_REPO"/CVS ]; then
715 error "unknown repo type"
718 if [ -d "$MR_REPO"/.svn ]; then
720 elif [ -d "$MR_REPO"/.git ]; then
721 git status "$@" || true
722 elif [ -d "$MR_REPO"/.bzr ]; then
724 elif [ -d "$MR_REPO"/CVS ]; then
727 error "unknown repo type"
730 if [ -d "$MR_REPO"/.svn ]; then
732 elif [ -d "$MR_REPO"/.git ]; then
733 git commit -a "$@" && git push --all
734 elif [ -d "$MR_REPO"/.bzr ]; then
735 bzr commit "$@" && bzr push
736 elif [ -d "$MR_REPO"/CVS ]; then
739 error "unknown repo type"
742 if [ -d "$MR_REPO"/.svn ]; then
744 elif [ -d "$MR_REPO"/.git ]; then
746 elif [ -d "$MR_REPO"/.bzr ]; then
748 elif [ -d "$MR_REPO"/CVS ]; then
751 error "unknown repo type"
754 if [ -d "$MR_REPO"/.svn ]; then
756 elif [ -d "$MR_REPO"/.git ]; then
758 elif [ -d "$MR_REPO"/.bzr ]; then
760 elif [ -d "$MR_REPO"/CVS ]; then
763 error "unknown repo type"
769 basedir="$(basename $(pwd))"
771 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
772 if [ -z "$url" ]; then
773 error "cannot determine svn url"
775 echo "Registering svn url: $url"
776 mr config "$(pwd)" checkout="svn co $url $basedir"
777 elif [ -d .git ]; then
778 url=$(LANG=C git-config --get remote.origin.url)
779 if [ -z "$url" ]; then
780 error "cannot determine git url"
782 echo "Registering git url: $url"
783 mr config "$(pwd)" checkout="git clone $url $basedir"
784 elif [ -d .bzr ]; then
785 url=$(cat .bzr/branch/parent)
786 if [ -z "$url" ]; then
787 error "cannot determine bzr url"
789 echo "Registering bzr url: $url"
790 mr config "$(pwd)" checkout="bzr clone $url $basedir"
792 error "unable to register this repo type"
795 if [ ! -e "$MR_PATH" ]; then
796 error "cannot find program path"
798 (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
802 ed = echo "A horse is a horse, of course, of course.."
803 T = echo "I pity the fool."