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 Adds, modifies, removes, or prints a value from the mrconfig file. The next
90 parameter is the name of the section the value is in. To add or modify
91 values, use one or more instances of "parameter=value". Use "parameter=" to
92 remove a parameter. Use just "parameter" to get the value of a parameter.
94 For example, to add (or edit) a repository in src/foo:
96 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
98 To show the command that mr uses to update the repository in src/foo:
100 mr config src/foo update
108 Actions can be abbreviated to any unambiguous subsctring, so
109 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
112 Additional parameters can be passed to most commands, and are passed on
113 unchanged to the underlying revision control system. This is mostly useful
114 if the repositories mr will act on all use the same revision control
123 Specifies the topmost directory that B<mr> should work in. The default is
124 the current working directory.
128 Use the specified mrconfig file, instead of looking for one in your home
139 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
140 file in your home directory, and this can in turn chain load .mrconfig files
143 Here is an example .mrconfig file:
146 checkout = svn co svn://svn.example.com/src/trunk src
150 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
152 The .mrconfig file uses a variant of the INI file format. Lines starting with
153 "#" are comments. Lines ending with "\" are continued on to the next line.
155 The "DEFAULT" section allows setting default values for the sections that
158 The "ALIAS" section allows adding aliases for actions. Each parameter
159 is an alias, and its value is the action to use.
161 All other sections add repositories. The section header specifies the
162 directory where the repository is located. This is relative to the directory
163 that contains the mrconfig file, but you can also choose to use absolute
166 Within a section, each parameter defines a shell command to run to handle a
167 given action. mr contains default handlers for the "update", "status", and
168 "commit" actions, so normally you only need to specify what to do for
171 Note that these shell commands are run in a "set -e" shell
172 environment, where any additional parameters you pass are available in
173 "$@". The "checkout" command is run in the parent of the repository
174 directory, since the repository isn't checked out yet. All other commands
175 are run inside the repository, though not necessarily at the top of it.
176 The "MR_REPO" environment variable is set to the path to the top of the
179 A few parameters have special meanings:
185 If the "skip" parameter is set and its command returns nonzero, then B<mr>
186 will skip acting on that repository. The command is passed the action
189 Here are two examples. The first skips the repo unless
190 mr is run by joey. The second uses the hours_since function
191 (included in mr's built-in library) to skip updating the repo unless it's
192 been at least 12 hours since the last update.
194 skip = test $(whoami) != joey
195 skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
199 If the "chain" parameter is set and its command returns nonzero, then B<mr>
200 will try to load a .mrconfig file from the root of the repository. (You
201 should avoid chaining from repositories with untrusted committers.)
205 If the "deleted" parameter is set and its command returns nonzero, then
206 B<mr> will treat the repository as deleted. It won't ever actually delete
207 the repository, but it will warn if it sees the repsoitory's directory.
208 This is useful when one mrconfig file is shared amoung multiple machines,
209 to keep track of and remember to delete old repositories.
213 The "lib" parameter can specify some shell code that will be run before each
214 command, this can be a useful way to define shell functions for other commands
221 Copyright 2007 Joey Hess <joey@kitenet.net>
223 Licensed under the GNU GPL version 2 or higher.
225 http://kitenet.net/~joey/code/mr/
232 use Cwd qw(getcwd abs_path);
234 my $directory=getcwd();
235 my $config="$ENV{HOME}/.mrconfig";
241 Getopt::Long::Configure("no_permute");
242 my $result=GetOptions(
243 "d|directory=s" => sub { $directory=abs_path($_[1]) },
244 "c|config=s" => \$config,
245 "verbose" => \$verbose,
247 if (! $result || @ARGV < 1) {
248 die("Usage: mr [-d directory] action [params ...]\n".
249 "(Use mr help for man page.)\n");
256 #print Dumper(\%config);
259 use FindBin qw($Bin $Script);
260 $ENV{MR_PATH}=$Bin."/".$Script;
263 # alias expansion and command stemming
264 my $action=shift @ARGV;
265 if (exists $alias{$action}) {
266 $action=$alias{$action};
268 if (! exists $knownactions{$action}) {
269 my @matches = grep { /^\Q$action\E/ }
270 keys %knownactions, keys %alias;
274 elsif (@matches == 0) {
275 die "mr: unknown action \"$action\" (known actions: ".
276 join(", ", sort keys %knownactions).")\n";
279 die "mr: ambiguous action \"$action\" (matches: ".
280 join(", ", @matches).")\n";
284 if ($action eq 'help') {
285 exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
287 elsif ($action eq 'config') {
289 die "mr config: not enough parameters\n";
292 if ($section=~/^\//) {
293 # try to convert to a path relative to $config's dir
294 my ($dir)=$config=~/^(.*\/)[^\/]+$/;
295 if ($section=~/^\Q$dir\E(.*)/) {
301 if (/^([^=]+)=(.*)$/) {
302 $changefields{$1}=$2;
306 foreach my $topdir (sort keys %config) {
307 if (exists $config{$topdir}{$section} &&
308 exists $config{$topdir}{$section}{$_}) {
309 print $config{$topdir}{$section}{$_}."\n";
314 die "mr $action: $section $_ not set\n";
318 modifyconfig($config, $section, %changefields) if %changefields;
321 elsif ($action eq 'register') {
322 my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
323 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
324 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
325 print STDERR "mr $action: running >>$command<<\n" if $verbose;
326 exec($command) || die "exec: $!";
329 # work out what repos to act on
332 foreach my $topdir (sort keys %config) {
333 foreach my $subdir (sort keys %{$config{$topdir}}) {
334 next if $subdir eq 'DEFAULT';
335 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
337 $dir.="/" unless $dir=~/\/$/;
338 $d.="/" unless $d=~/\/$/;
339 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
340 push @repos, [$dir, $topdir, $subdir];
344 # fallback to find a leaf repo
345 LEAF: foreach my $topdir (reverse sort keys %config) {
346 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
347 next if $subdir eq 'DEFAULT';
348 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
350 $dir.="/" unless $dir=~/\/$/;
351 $d.="/" unless $d=~/\/$/;
352 if ($d=~/^\Q$dir\E/) {
353 push @repos, [$dir, $topdir, $subdir];
361 my (@failed, @successful, @skipped);
362 foreach my $repo (@repos) {
363 action($action, @$repo);
367 my ($action, $dir, $topdir, $subdir) = @_;
369 my $lib= exists $config{$topdir}{$subdir}{lib} ?
370 $config{$topdir}{$subdir}{lib}."\n" : "";
372 if (exists $config{$topdir}{$subdir}{deleted}) {
377 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
378 print "mr $action: running deleted test >>$test<<\n" if $verbose;
379 my $ret=system($test);
380 if ($ret >> 8 == 0) {
381 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
388 if ($action eq 'checkout') {
390 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
394 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
396 elsif ($action eq 'update') {
398 return action("checkout", $dir, $topdir, $subdir);
404 if (exists $config{$topdir}{$subdir}{skip}) {
405 my $test="set -e;".$lib.
406 "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
407 print "mr $action: running skip test >>$test<<\n" if $verbose;
408 my $ret=system($test);
409 if ($ret >> 8 == 0) {
410 print "mr $action: $dir skipped per config file\n" if $verbose;
416 if (! $nochdir && ! chdir($dir)) {
417 print STDERR "mr $action: failed to chdir to $dir: $!\n";
420 elsif (! exists $config{$topdir}{$subdir}{$action}) {
421 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
426 print "mr $action: $topdir$subdir\n";
429 print "mr $action: $topdir$subdir (in subdir $directory)\n";
431 my $command="set -e; ".$lib.
432 "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
433 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
434 print STDERR "mr $action: running >>$command<<\n" if $verbose;
435 my $ret=system($command);
437 print STDERR "mr $action: failed ($ret)\n" if $verbose;
439 if ($ret >> 8 != 0) {
440 print STDERR "mr $action: command failed\n";
443 print STDERR "mr $action: command died ($ret)\n";
447 push @successful, $dir;
459 return "$count ".($count > 1 ? $plural : $singular);
463 if (! @successful && ! @failed && ! @skipped) {
464 die "mr $action: no repositories found to work on\n";
466 print "mr $action: finished (".join("; ",
467 showstat($#successful+1, "successful", "successful"),
468 showstat($#failed+1, "failed", "failed"),
469 showstat($#skipped+1, "skipped", "skipped"),
474 elsif (! @successful && @skipped) {
487 if (ref $f eq 'GLOB') {
496 my $absf=abs_path($f);
497 if ($loaded{$absf}) {
502 print "mr: loading config $f\n" if $verbose;
503 open($in, "<", $f) || die "mr: open $f: $!\n";
504 ($dir)=$f=~/^(.*\/)[^\/]+$/;
505 if (! defined $dir) {
508 $dir=abs_path($dir)."/";
510 # copy in defaults from first parent
512 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
513 if (exists $config{$parent} &&
514 exists $config{$parent}{DEFAULT}) {
515 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
524 next if /^\s*\#/ || /^\s*$/;
525 if (/^\s*\[([^\]]*)\]\s*$/) {
528 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
533 while ($value=~/(.*)\\$/s) {
534 $value=$1."\n".<$in>;
538 if (! defined $section) {
539 die "$f line $.: parameter ($parameter) not in section\n";
541 if ($section ne 'ALIAS' &&
542 ! exists $config{$dir}{$section} &&
543 exists $config{$dir}{DEFAULT}) {
545 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
547 if ($section eq 'ALIAS') {
548 $alias{$parameter}=$value;
550 elsif ($parameter eq 'lib') {
551 $config{$dir}{$section}{lib}.=$value."\n";
554 $config{$dir}{$section}{$parameter}=$value;
555 $knownactions{$parameter}=1;
556 if ($parameter eq 'chain' &&
557 length $dir && $section ne "DEFAULT" &&
558 -e $dir.$section."/.mrconfig" &&
559 system($value) >> 8 == 0) {
560 push @toload, $dir.$section."/.mrconfig";
565 die "$f line $.: parse error\n";
577 # the section to modify or add
578 my $targetsection=shift;
579 # fields to change in the section
580 # To remove a field, set its value to "".
587 open(my $in, "<", $f) || die "mr: open $f: $!\n";
594 while ($out[$#out] =~ /^\s*$/) {
595 unshift @blanks, pop @out;
597 foreach my $field (sort keys %changefields) {
598 if (length $changefields{$field}) {
599 push @out, "$field = $changefields{$field}\n";
609 if (/^\s*\#/ || /^\s*$/) {
612 elsif (/^\s*\[([^\]]*)\]\s*$/) {
613 if (defined $section &&
614 $section eq $targetsection) {
622 elsif (/^\s*(\w+)\s*=\s(.*)/) {
627 while ($value=~/(.*\\)$/s) {
628 $value=$1."\n".shift(@lines);
632 if ($section eq $targetsection) {
633 if (exists $changefields{$parameter}) {
634 if (length $changefields{$parameter}) {
635 $value=$changefields{$parameter};
637 delete $changefields{$parameter};
641 push @out, "$parameter = $value\n";
645 if (defined $section &&
646 $section eq $targetsection) {
649 elsif (%changefields) {
650 push @out, "\n[$targetsection]\n";
651 foreach my $field (sort keys %changefields) {
652 if (length $changefields{$field}) {
653 push @out, "$field = $changefields{$field}\n";
658 open(my $out, ">", $f) || die "mr: write $f: $!\n";
663 # Finally, some useful actions that mr knows about by default.
664 # These can be overridden in ~/.mrconfig.
678 for dir in .git .svn .bzr CVS; do \
679 if [ -e "$MR_REPO/$dir" ]; then \
680 flagfile="$MR_REPO/$dir/.mr_last$1" \
684 if [ -z "$flagfile" ]; then \
685 error "cannot determine flag filename" \
687 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile" \
692 if [ -d "$MR_REPO"/.svn ]; then \
694 elif [ -d "$MR_REPO"/.git ]; then \
695 git pull origin master "$@" \
696 elif [ -d "$MR_REPO"/.bzr ]; then \
698 elif [ -d "$MR_REPO"/CVS ]; then \
701 error "unknown repo type" \
704 if [ -d "$MR_REPO"/.svn ]; then \
706 elif [ -d "$MR_REPO"/.git ]; then \
707 git status "$@" || true \
708 elif [ -d "$MR_REPO"/.bzr ]; then \
710 elif [ -d "$MR_REPO"/CVS ]; then \
713 error "unknown repo type" \
716 if [ -d "$MR_REPO"/.svn ]; then \
718 elif [ -d "$MR_REPO"/.git ]; then \
719 git commit -a "$@" && git push --all \
720 elif [ -d "$MR_REPO"/.bzr ]; then \
721 bzr commit "$@" && bzr push \
722 elif [ -d "$MR_REPO"/CVS ]; then \
725 error "unknown repo type" \
728 if [ -d "$MR_REPO"/.svn ]; then \
730 elif [ -d "$MR_REPO"/.git ]; then \
732 elif [ -d "$MR_REPO"/.bzr ]; then \
734 elif [ -d "$MR_REPO"/CVS ]; then \
737 error "unknown repo type" \
740 if [ -d "$MR_REPO"/.svn ]; then \
742 elif [ -d "$MR_REPO"/.git ]; then \
744 elif [ -d "$MR_REPO"/.bzr ]; then \
746 elif [ -d "$MR_REPO"/CVS ]; then \
749 error "unknown repo type" \
752 if [ -n "$1" ]; then \
755 basedir="$(basename $(pwd))" \
756 if [ -d .svn ]; then \
757 url=$(LANG=C svn info . | \
758 grep -i ^URL: | cut -d ' ' -f 2) \
759 if [ -z "$url" ]; then \
760 error "cannot determine svn url" \
762 echo "Registering svn url: $url" \
763 mr config "$(pwd)" checkout="svn co $url $basedir" \
764 elif [ -d .git ]; then \
765 url=$(LANG=C git-config --get remote.origin.url) \
766 if [ -z "$url" ]; then \
767 error "cannot determine git url" \
769 echo "Registering git url: $url" \
770 mr config "$(pwd)" checkout="git clone $url $basedir" \
771 elif [ -d .bzr ]; then \
772 url=$(cat .bzr/branch/parent) \
773 if [ -z "$url" ]; then \
774 error "cannot determine bzr url" \
776 echo "Registering bzr url: $url" \
777 mr config "$(pwd)" checkout="bzr clone $url $basedir" \
779 error "unable to register this repo type" \
784 if [ ! -e "$MR_PATH" ]; then \
785 error "cannot find program path" \
787 (pod2man -c mr "$MR_PATH" | man -l -) || \
788 error "pod2man or man failed"
790 ed = echo "A horse is a horse, of course, of course.."
791 T = echo "I pity the fool."