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"]
17 B<mr> [options] record [-m "message"]
25 B<mr> [options] bootstrap url [directory]
27 B<mr> [options] register [repository]
29 B<mr> [options] config section ["parameter=[value]" ...]
31 B<mr> [options] action [params ...]
33 B<mr> [options] [online|offline]
35 B<mr> [options] remember action [params ...]
39 B<mr> is a Multiple Repository management tool. It can checkout, update, or
40 perform other actions on a set of repositories as if they were one combined
41 repository. It supports any combination of subversion, git, cvs, mercurial,
42 bzr, darcs and fossil repositories, and support for other revision
43 control systems can easily be added.
45 B<mr> cds into and operates on all registered repositories at or below your
46 working directory. Or, if you are in a subdirectory of a repository that
47 contains no other registered repositories, it will stay in that directory,
48 and work on only that repository,
50 B<mr> is configured by .mrconfig files, which list the repositories. It
51 starts by reading the .mrconfig file in your home directory, and this can
52 in turn chain load .mrconfig files from repositories. It also automatically
53 looks for a .mrconfig file in the current directory, or in one of its
56 These predefined commands should be fairly familiar to users of any revision
61 =item checkout (or co)
63 Checks out any repositories that are not already checked out.
67 Updates each repository from its configured remote repository.
69 If a repository isn't checked out yet, it will first check it out.
73 Displays a status report for each repository, showing what
74 uncommitted changes are present in the repository.
78 Commits changes to each repository. (By default, changes are pushed to the
79 remote repository too, when using distributed systems like git. If you
80 don't like this default, you can change it in your .mrconfig, or use record
83 The optional -m parameter allows specifying a commit message.
87 Records changes to the local repository, but does not push them to the
88 remote repository. Only supported for distributed revision control systems.
90 The optional -m parameter allows specifying a commit message.
94 Pushes committed local changes to the remote repository. A no-op for
95 centralized revision control systems.
99 Show a diff of uncommitted changes.
107 These commands are also available:
111 =item bootstrap url [directory]
113 Causes mr to download the url, and use it as a .mrconfig file
114 to checkout the repositories listed in it, into the specified directory.
116 The directory will be created if it does not exist. If no directory is
117 specified, the current directory will be used.
119 If the .mrconfig file includes a repository named ".", that
120 is checked out into the top of the specified directory.
124 List the repositories that mr will act on.
128 Register an existing repository in a mrconfig file. By default, the
129 repository in the current directory is registered, or you can specify a
130 directory to register.
132 The mrconfig file that is modified is chosen by either the -c option, or by
133 looking for the closest known one at or in a parent of the current directory.
137 Adds, modifies, removes, or prints a value from a mrconfig file. The next
138 parameter is the name of the section the value is in. To add or modify
139 values, use one or more instances of "parameter=value". Use "parameter=" to
140 remove a parameter. Use just "parameter" to get the value of a parameter.
142 For example, to add (or edit) a repository in src/foo:
144 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
146 To show the command that mr uses to update the repository in src/foo:
148 mr config src/foo update
150 To see the built-in library of shell functions contained in mr:
152 mr config DEFAULT lib
154 The mrconfig file that is used is chosen by either the -c option, or by
155 looking for the closest known one at or in a parent of the current directory.
159 Advises mr that it is in offline mode. Any commands that fail in
160 offline mode will be remembered, and retried when mr is told it's online.
164 Advices mr that it is in online mode again. Commands that failed while in
165 offline mode will be re-run.
169 Remember a command, to be run later when mr re-enters online mode. This
170 implicitly puts mr into offline mode. The command can be any regular mr
171 command. This is useful when you know that a command will fail due to being
172 offline, and so don't want to run it right now at all, but just remember
173 to run it when you go back online.
181 Actions can be abbreviated to any unambiguous substring, so
182 "mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
185 Additional parameters can be passed to most commands, and are passed on
186 unchanged to the underlying revision control system. This is mostly useful
187 if the repositories mr will act on all use the same revision control
196 =item --directory directory
198 Specifies the topmost directory that B<mr> should work in. The default is
199 the current working directory.
203 =item --config mrconfig
205 Use the specified mrconfig file. The default is to use both B<~/.mrconfig>
206 as well as look for a .mrconfig file in the current directory, or in one
207 of its parent directories.
225 Accept untrusted SSL certificates when bootstrapping.
231 Expand the statistics line displayed at the end to include information
232 about exactly which repositories failed and were skipped, if any.
238 Interactive mode. If a repository fails to be processed, a subshell will be
239 started which you can use to resolve or investigate the problem. Exit the
240 subshell to continue the mr run.
244 =item --no-recurse [number]
246 If no number if specified, just operate on the repository for the current
247 directory, do not recurse into deeper repositories.
249 If a number is specified, will recurse into repositories at most that many
250 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
251 but not ./src/packages/bar.
255 =item --jobs [number]
257 Run the specified number of jobs in parallel, or an unlimited number of jobs
258 with no number specified. This can greatly speed up operations such as updates.
259 It is not recommended for interactive operations.
261 Note that running more than 10 jobs at a time is likely to run afoul of
262 ssh connection limits. Running between 3 and 5 jobs at a time will yield
263 a good speedup in updates without loading the machine too much.
269 Trust all mrconfig files even if they are not listed in ~/.mrtrust.
276 This obsolete flag is ignored.
280 =head1 MRCONFIG FILES
282 Here is an example .mrconfig file:
285 checkout = svn co svn://svn.example.com/src/trunk src
289 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
291 git checkout -b mybranch origin/master
293 The .mrconfig file uses a variant of the INI file format. Lines starting with
294 "#" are comments. Values can be continued to the following line by
295 indenting the line with whitespace.
297 The "DEFAULT" section allows setting default values for the sections that
300 The "ALIAS" section allows adding aliases for actions. Each parameter
301 is an alias, and its value is the action to use.
303 All other sections add repositories. The section header specifies the
304 directory where the repository is located. This is relative to the directory
305 that contains the mrconfig file, but you can also choose to use absolute
306 paths. (Note that you can use environment variables in section names; they
307 will be passed through the shell for expansion. For example,
308 "[$HOSTNAME]", or "[${HOSTNAME}foo]")
310 Within a section, each parameter defines a shell command to run to handle a
311 given action. mr contains default handlers for "update", "status",
312 "commit", and other standard actions. Normally you only need to specify what
313 to do for "checkout".
315 Note that these shell commands are run in a "set -e" shell
316 environment, where any additional parameters you pass are available in
317 "$@". The "checkout" command is run in the parent of the repository
318 directory, since the repository isn't checked out yet. All other commands
319 are run inside the repository, though not necessarily at the top of it.
321 The "MR_REPO" environment variable is set to the path to the top of the
322 repository. (For the "register" action, "MR_REPO" is instead set to the
323 basename of the directory that should be created when checking the
326 The "MR_CONFIG" environment variable is set to the .mrconfig file
327 that defines the repo being acted on, or, if the repo is not yet in a config
328 file, the .mrconfig file that should be modified to register the repo.
330 A few parameters have special meanings:
336 If the "skip" parameter is set and its command returns true, then B<mr>
337 will skip acting on that repository. The command is passed the action
340 Here are two examples. The first skips the repo unless
341 mr is run by joey. The second uses the hours_since function
342 (included in mr's built-in library) to skip updating the repo unless it's
343 been at least 12 hours since the last update.
345 skip = test `whoami` != joey
346 skip = [ "$1" = update ] && ! hours_since "$1" 12
350 The "order" parameter can be used to override the default ordering of
351 repositories. The default order value is 10. Use smaller values to make
352 repositories be processed earlier, and larger values to make repositories
355 Note that if a repository is located in a subdirectory of another
356 repository, ordering it to be processed earlier is not recommended.
360 If the "chain" parameter is set and its command returns true, then B<mr>
361 will try to load a .mrconfig file from the root of the repository.
365 If the "include" parameter is set, its command is ran, and should output
366 additional mrconfig file content. The content is included as if it were
367 part of the including file.
369 Unlike all other parameters, this parameter does not need to be placed
374 The "lib" parameter can specify some shell code that will be run before each
375 command, this can be a useful way to define shell functions for other commands
380 If the "fixups" parameter is set, its command is run whenever a repository
381 is checked out, or updated. This provides an easy way to do things
382 like permissions fixups, or other tweaks to the repository content,
383 whenever the repository is changed.
387 If a "pre_action" parameter is set, its command is run before mr performs the
388 specified action. Similarly, "post_action" parameters are run after mr
389 successfully performs the specified action. For example, "pre_commit" is
390 run before committing; "post_update" is run after updating.
394 When looking for a command to run for a given action, mr first looks for
395 a parameter with the same name as the action. If that is not found, it
396 looks for a parameter named "rcs_action" (substituting in the name of the
397 revision control system and the action). The name of the revision control
398 system is itself determined by running each defined "rcs_test" action,
401 Internally, mr has settings for "git_update", "svn_update", etc. To change
402 the action that is performed for a given revision control system, you can
403 override these rcs specific actions. To add a new revision control system,
404 you can just add rcs specific actions for it.
406 =head1 UNTRUSTED MRCONFIG FILES
408 Since mrconfig files can contain arbitrary shell commands, they can do
409 anything. This flexibility is good, but it also allows a malicious mrconfig
410 file to delete your whole home directory. Such a file might be contained
411 inside a repository that your main ~/.mrconfig checks out. To
412 avoid worries about evil commands in a mrconfig file, mr defaults to
413 reading all mrconfig files other than the main ~/.mrconfig in untrusted
414 mode. In untrusted mode, mrconfig files are limited to running only known
415 safe commands (like "git clone") in a carefully checked manner.
417 To configure mr to trust other mrconfig files, list them in ~/.mrtrust.
418 One mrconfig file should be listed per line. Either the full pathname
419 should be listed, or the pathname can start with "~/" to specify a file
420 relative to your home directory.
422 =head1 OFFLINE LOG FILE
424 The ~/.mrlog file contains commands that mr has remembered to run later,
425 due to being offline. You can delete or edit this file to remove commands,
426 or even to add other commands for 'mr online' to run. If the file is
427 present, mr assumes it is in offline mode.
431 mr can be extended to support things such as unison and git-svn. Some
432 files providing such extensions are available in /usr/share/mr/. See
433 the documentation in the files for details about using them.
437 mr returns nonzero if a command failed in any of the repositories.
441 Copyright 2007-2011 Joey Hess <joey@kitenet.net>
443 Licensed under the GNU GPL version 2 or higher.
445 http://kitenet.net/~joey/code/mr/
452 use Cwd qw(getcwd abs_path);
454 # things that can happen when mr runs a command
463 my $config_overridden=0;
473 my $directory=getcwd();
475 $ENV{MR_CONFIG}=find_mrconfig();
482 my (@ok, @failed, @skipped);
488 my ($action, $dir, $topdir, $subdir) = @_;
490 if (exists $rcs{$dir}) {
495 foreach my $rcs_test (
497 length $a <=> length $b
500 } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
501 my ($rcs)=$rcs_test=~/(.*)_test/;
502 $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test;
503 $test.="if my_$rcs_test; then echo $rcs; fi\n";
505 $test=$config{$topdir}{$subdir}{lib}."\n".$test
506 if exists $config{$topdir}{$subdir}{lib};
508 print "mr $action: running rcs test >>$test<<\n" if $verbose;
513 print STDERR "mr $action: found multiple possible repository types ($rcs) for ".fulldir($topdir, $subdir)."\n";
517 return $rcs{$dir}=undef;
520 return $rcs{$dir}=$rcs;
525 my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
527 if (exists $config{$topdir}{$subdir}{$action}) {
528 return $config{$topdir}{$subdir}{$action};
535 my $rcs=rcs_test(@_);
538 exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
539 return $config{$topdir}{$subdir}{$rcs."_".$action};
547 my ($topdir, $subdir) = @_;
548 return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
552 my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
553 my $fulldir=fulldir($topdir, $subdir);
555 $ENV{MR_CONFIG}=$configfiles{$topdir};
556 my $lib=exists $config{$topdir}{$subdir}{lib} ?
557 $config{$topdir}{$subdir}{lib}."\n" : "";
558 my $is_checkout=($action eq 'checkout');
559 my $is_update=($action =~ /update/);
564 if (! $force_checkout) {
566 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
570 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
575 return action("checkout", $dir, $topdir, $subdir);
579 my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
580 my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
582 if (defined $skiptest) {
583 my $test="set -e;".$lib.
584 "my_action(){ $skiptest\n }; my_action '$action'";
585 print "mr $action: running skip test >>$test<<\n" if $verbose;
586 my $ret=system($test);
588 if (($? & 127) == 2) {
589 print STDERR "mr $action: interrupted\n";
593 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
597 if ($ret >> 8 == 0) {
598 print "mr $action: $dir skipped per config file\n" if $verbose;
603 if ($is_checkout && ! -d $dir) {
604 print "mr $action: creating parent directory $dir\n" if $verbose;
605 system("mkdir", "-p", $dir);
608 if (! $no_chdir && ! chdir($dir)) {
609 print STDERR "mr $action: failed to chdir to $dir: $!\n";
612 elsif (! defined $command) {
613 my $rcs=rcs_test(@_);
614 if (! defined $rcs) {
615 print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
619 print STDERR "mr $action: no defined action for $rcs repository $fulldir, skipping\n";
625 print "mr $action: $fulldir\n" unless $quiet;
629 $s=~s/^\Q$fulldir\E\/?//;
630 print "mr $action: $fulldir (in subdir $s)\n" unless $quiet;
633 my $hookret=hook("pre_$action", $topdir, $subdir);
634 return $hookret if $hookret != OK;
636 $command="set -e; ".$lib.
637 "my_action(){ $command\n }; my_action ".
638 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
639 print "mr $action: running >>$command<<\n" if $verbose;
640 my $ret=system($command);
642 if (($? & 127) == 2) {
643 print STDERR "mr $action: interrupted\n";
647 print STDERR "mr $action: received signal ".($? & 127)."\n";
650 print STDERR "mr $action: failed ($ret)\n" if $verbose;
651 if ($ret >> 8 != 0) {
652 print STDERR "mr $action: command failed\n";
653 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
654 # recreate original command line to
655 # remember, and avoid recursing
657 @ARGV=('-n', $action, @orig);
658 action("remember", $dir, $topdir, $subdir);
663 print STDERR "mr $action: command died ($ret)\n";
668 if ($is_checkout && ! -d $dir) {
669 print STDERR "mr $action: $dir missing after checkout\n";;
673 my $ret=hook("post_$action", $topdir, $subdir);
674 return $ret if $ret != OK;
676 if (($is_checkout || $is_update)) {
677 my $ret=hook("fixups", $topdir, $subdir);
678 return $ret if $ret != OK;
687 my ($hook, $topdir, $subdir) = @_;
689 my $command=$config{$topdir}{$subdir}{$hook};
690 return OK unless defined $command;
691 my $lib=exists $config{$topdir}{$subdir}{lib} ?
692 $config{$topdir}{$subdir}{lib}."\n" : "";
693 my $shell="set -e;".$lib.
694 "my_hook(){ $command\n }; my_hook";
695 print "mr $hook: running >>$shell<<\n" if $verbose;
696 my $ret=system($shell);
698 if (($? & 127) == 2) {
699 print STDERR "mr $hook: interrupted\n";
703 print STDERR "mr $hook: received signal ".($? & 127)."\n";
711 # run actions on multiple repos, in parallel
721 while (@fhs or @repos) {
722 while ((!$jobs || $running < $jobs) && @repos) {
724 my $repo = shift @repos;
725 pipe(my $outfh, CHILD_STDOUT);
726 pipe(my $errfh, CHILD_STDERR);
728 unless ($pid = fork) {
729 die "mr $action: cannot fork: $!" unless defined $pid;
730 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
731 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
736 exit action($action, @$repo);
740 push @active, [$pid, $repo];
741 push @fhs, [$outfh, $errfh];
744 my ($rin, $rout) = ('','');
746 foreach my $fh (@fhs) {
747 next unless defined $fh;
748 vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
749 vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
751 $nfound = select($rout=$rin, undef, undef, 1);
752 foreach my $channel (0, 1) {
753 foreach my $i (0..$#fhs) {
754 next unless defined $fhs[$i];
755 my $fh = $fhs[$i][$channel];
756 next unless defined $fh;
757 if (vec($rout, fileno($fh), 1) == 1) {
759 if (sysread($fh, $r, 1024) == 0) {
761 $fhs[$i][$channel] = undef;
762 if (! defined $fhs[$i][0] &&
763 ! defined $fhs[$i][1]) {
764 waitpid($active[$i][0], 0);
765 print STDOUT $out[$i][0];
766 print STDERR $out[$i][1];
767 record($active[$i][1], $? >> 8);
769 splice(@active, $i, 1);
774 $out[$i][$channel] .= $r;
782 my $dir=shift()->[0];
787 print "\n" unless $quiet;
789 elsif ($ret == FAILED) {
791 chdir($dir) unless $no_chdir;
792 print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
793 system((getpwuid($<))[8], "-i");
796 print "\n" unless $quiet;
798 elsif ($ret == SKIPPED) {
801 elsif ($ret == ABORT) {
805 die "unknown exit status $ret";
811 if (! @ok && ! @failed && ! @skipped) {
812 die "mr $action: no repositories found to work on\n";
814 print "mr $action: finished (".join("; ",
815 showstat($#ok+1, "ok", "ok"),
816 showstat($#failed+1, "failed", "failed"),
817 showstat($#skipped+1, "skipped", "skipped"),
818 ).")\n" unless $quiet;
821 print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
824 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
834 return "$count ".($count > 1 ? $plural : $singular);
839 # an ordered list of repos
842 foreach my $topdir (sort keys %config) {
843 foreach my $subdir (sort keys %{$config{$topdir}}) {
847 order => $config{$topdir}{$subdir}{order},
852 $a->{order} <=> $b->{order}
854 $a->{topdir} cmp $b->{topdir}
856 $a->{subdir} cmp $b->{subdir}
862 my $topdir=$repo->{topdir};
863 my $subdir=$repo->{subdir};
864 my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
869 # figure out which repos to act on
872 foreach my $repo (repolist()) {
873 my $topdir=$repo->{topdir};
874 my $subdir=$repo->{subdir};
876 next if $subdir eq 'DEFAULT';
877 my $dir=repodir($repo);
879 $dir.="/" unless $dir=~/\/$/;
880 $d.="/" unless $d=~/\/$/;
881 next if $dir ne $d && $dir !~ /^\Q$d\E/;
882 if (defined $max_depth) {
883 my @a=split('/', $dir);
884 my @b=split('/', $d);
885 do { } while (@a && @b && shift(@a) eq shift(@b));
886 next if @a > $max_depth || @b > $max_depth;
888 push @repos, [$dir, $topdir, $subdir];
891 # fallback to find a leaf repo
892 foreach my $repo (reverse repolist()) {
893 my $topdir=$repo->{topdir};
894 my $subdir=$repo->{subdir};
896 next if $subdir eq 'DEFAULT';
897 my $dir=repodir($repo);
899 $dir.="/" unless $dir=~/\/$/;
900 $d.="/" unless $d=~/\/$/;
901 if ($d=~/^\Q$dir\E/) {
902 push @repos, [$dir, $topdir, $subdir];
924 sub is_trusted_config {
925 my $config=shift; # must be abs_pathed already
927 # We always trust ~/.mrconfig.
928 return 1 if $config eq abs_path("$ENV{HOME}/.mrconfig");
930 return 1 if $trust_all;
932 my $trustfile=$ENV{HOME}."/.mrtrust";
935 $trusted{"$ENV{HOME}/.mrconfig"}=1;
936 if (open (TRUST, "<", $trustfile)) {
939 s/^~\//$ENV{HOME}\//;
940 $trusted{abs_path($_)}=1;
946 return $trusted{$config};
950 sub is_trusted_repo {
953 # Tightly limit what is allowed in a repo name.
954 # No ../, no absolute paths, and no unusual filenames
955 # that might try to escape to the shell.
956 return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
957 $repo !~ /\.\./ && $repo !~ /^\//;
960 sub is_trusted_checkout {
963 # To determine if the command is safe, compare it with the
964 # *_trusted_checkout config settings. Those settings are
965 # templates for allowed commands, so make sure that each word
966 # of the command matches the corresponding word of the template.
969 foreach my $word (split(' ', $command)) {
971 if ($word=~/^'(.*)'$/) {
974 elsif ($word=~/^"(.*)"$/) {
981 foreach my $key (grep { /_trusted_checkout$/ }
982 keys %{$config{''}{DEFAULT}}) {
983 my @twords=split(' ', $config{''}{DEFAULT}{$key});
984 next if @words > @twords;
988 for (my $c=0; $c < @twords && $match; $c++) {
989 if ($twords[$c] eq '$url') {
990 # Match all the typical characters found in
991 # urls, plus @ which svn can use. Note
992 # that the "url" might also be a local
995 defined $words[$c] &&
996 $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
1000 elsif ($twords[$c] eq '$repo') {
1001 # If a repo is not specified, assume it
1002 # will be the last path component of the
1003 # url, or something derived from it, and
1005 if (! defined $words[$c] && defined $url) {
1006 ($words[$c])=$url=~/\/([^\/]+)\/?$/;
1010 defined $words[$c] &&
1011 is_trusted_repo($words[$c])
1014 elsif (defined $words[$c] && $twords[$c] eq $words[$c]) {
1029 "(To trust this file, list it in ~/.mrtrust.)\n";
1041 if (ref $f eq 'GLOB') {
1051 my $absf=abs_path($f);
1052 if ($loaded{$absf}) {
1057 $trusted=is_trusted_config($absf);
1059 if (! defined $dir) {
1060 ($dir)=$f=~/^(.*\/)[^\/]+$/;
1061 if (! defined $dir) {
1066 $dir=abs_path($dir)."/";
1068 if (! exists $configfiles{$dir}) {
1069 $configfiles{$dir}=$f;
1072 # copy in defaults from first parent
1074 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
1075 if ($parent eq '/') {
1078 if (exists $config{$parent} &&
1079 exists $config{$parent}{DEFAULT}) {
1080 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
1085 print "mr: loading config $f\n" if $verbose;
1086 open($in, "<", $f) || die "mr: open $f: $!\n";
1089 close $in unless ref $f eq 'GLOB';
1097 next if /^\s*\#/ || /^\s*$/;
1098 if (/^\[([^\]]*)\]\s*$/) {
1102 if (! is_trusted_repo($section) ||
1103 $section eq 'ALIAS' ||
1104 $section eq 'DEFAULT') {
1105 trusterror "mr: illegal section \"[$section]\" in untrusted $f line $line";
1108 $section=expandenv($section) if $trusted;
1109 if ($section ne 'ALIAS' &&
1110 ! exists $config{$dir}{$section} &&
1111 exists $config{$dir}{DEFAULT}) {
1113 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1116 elsif (/^(\w+)\s*=\s*(.*)/) {
1121 while (@lines && $lines[0]=~/^\s(.+)/) {
1129 # Untrusted files can only contain checkout
1131 if ($parameter ne 'checkout') {
1132 trusterror "mr: illegal setting \"$parameter=$value\" in untrusted $f line $line";
1134 if (! is_trusted_checkout($value)) {
1135 trusterror "mr: illegal checkout command \"$value\" in untrusted $f line $line";
1139 if ($parameter eq "include") {
1140 print "mr: including output of \"$value\"\n" if $verbose;
1141 unshift @lines, `$value`;
1143 print STDERR "mr: include command exited nonzero ($?)\n";
1148 if (! defined $section) {
1149 die "$f line $.: parameter ($parameter) not in section\n";
1151 if ($section eq 'ALIAS') {
1152 $alias{$parameter}=$value;
1154 elsif ($parameter eq 'lib') {
1155 $config{$dir}{$section}{lib}.=$value."\n";
1158 $config{$dir}{$section}{$parameter}=$value;
1159 if ($parameter =~ /.*_(.*)/) {
1160 $knownactions{$1}=1;
1163 $knownactions{$parameter}=1;
1165 if ($parameter eq 'chain' &&
1166 length $dir && $section ne "DEFAULT" &&
1167 -e $dir.$section."/.mrconfig") {
1168 my $ret=system($value);
1170 if (($? & 127) == 2) {
1171 print STDERR "mr: chain test interrupted\n";
1175 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1179 push @toload, $dir.$section."/.mrconfig";
1185 die "$f line $line: parse error\n";
1194 sub startingconfig {
1195 %alias=%config=%configfiles=%knownactions=%loaded=();
1196 my $datapos=tell(DATA);
1198 seek(DATA,$datapos,0); # rewind
1203 # the section to modify or add
1204 my $targetsection=shift;
1205 # fields to change in the section
1206 # To remove a field, set its value to "".
1207 my %changefields=@_;
1213 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1218 my $formatfield=sub {
1220 my @value=split(/\n/, shift);
1222 return "$field = ".shift(@value)."\n".
1223 join("", map { "\t$_\n" } @value);
1227 while ($out[$#out] =~ /^\s*$/) {
1228 unshift @blanks, pop @out;
1230 foreach my $field (sort keys %changefields) {
1231 if (length $changefields{$field}) {
1232 push @out, "$field = $changefields{$field}\n";
1233 delete $changefields{$field};
1243 if (/^\s*\#/ || /^\s*$/) {
1246 elsif (/^\[([^\]]*)\]\s*$/) {
1247 if (defined $section &&
1248 $section eq $targetsection) {
1252 $section=expandenv($1);
1256 elsif (/^(\w+)\s*=\s(.*)/) {
1261 while (@lines && $lines[0]=~/^\s(.+)/) {
1267 if ($section eq $targetsection) {
1268 if (exists $changefields{$parameter}) {
1269 if (length $changefields{$parameter}) {
1270 $value=$changefields{$parameter};
1272 delete $changefields{$parameter};
1276 push @out, $formatfield->($parameter, $value);
1280 if (defined $section &&
1281 $section eq $targetsection) {
1284 elsif (%changefields) {
1285 push @out, "\n[$targetsection]\n";
1286 foreach my $field (sort keys %changefields) {
1287 if (length $changefields{$field}) {
1288 push @out, $formatfield->($field, $changefields{$field});
1293 open(my $out, ">", $f) || die "mr: write $f: $!\n";
1301 # actions that do not operate on all repos
1302 if ($action eq 'help') {
1305 elsif ($action eq 'config') {
1308 elsif ($action eq 'register') {
1311 elsif ($action eq 'bootstrap') {
1314 elsif ($action eq 'remember' ||
1315 $action eq 'offline' ||
1316 $action eq 'online') {
1317 my @repos=selectrepos;
1318 action($action, @{$repos[0]}) if @repos;
1322 if (!$jobs || $jobs > 1) {
1323 mrs($action, selectrepos());
1326 foreach my $repo (selectrepos()) {
1327 record($repo, action($action, @$repo));
1333 exec($config{''}{DEFAULT}{help}) || die "exec: $!";
1338 die "mr config: not enough parameters\n";
1341 if ($section=~/^\//) {
1342 # try to convert to a path relative to the config file
1343 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1344 $dir=abs_path($dir);
1345 $dir.="/" unless $dir=~/\/$/;
1346 if ($section=~/^\Q$dir\E(.*)/) {
1352 if (/^([^=]+)=(.*)$/) {
1353 $changefields{$1}=$2;
1357 foreach my $topdir (sort keys %config) {
1358 if (exists $config{$topdir}{$section} &&
1359 exists $config{$topdir}{$section}{$_}) {
1360 print $config{$topdir}{$section}{$_}."\n";
1362 last if $section eq 'DEFAULT';
1366 die "mr config: $section $_ not set\n";
1370 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1375 if ($config_overridden) {
1376 # Find the directory that the specified config file is
1378 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1381 # Find the closest known mrconfig file to the current
1383 $directory.="/" unless $directory=~/\/$/;
1385 foreach my $topdir (reverse sort keys %config) {
1386 next unless length $topdir;
1387 if ($directory=~/^\Q$topdir\E/) {
1388 $ENV{MR_CONFIG}=$configfiles{$topdir};
1394 if (! $foundconfig) {
1395 $directory=""; # no config file, use builtin
1399 my $subdir=shift @ARGV;
1400 if (! chdir($subdir)) {
1401 print STDERR "mr register: failed to chdir to $subdir: $!\n";
1405 $ENV{MR_REPO}=getcwd();
1406 my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1407 if (! defined $command) {
1408 die "mr register: unknown repository type\n";
1411 $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1412 $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1413 "my_action(){ $command\n }; my_action ".
1414 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1415 print "mr register: running >>$command<<\n" if $verbose;
1416 exec($command) || die "exec: $!";
1420 my $url=shift @ARGV;
1421 my $dir=shift @ARGV || ".";
1423 if (! defined $url || ! length $url) {
1424 die "mr: bootstrap requires url\n";
1427 # Download the config file to a temporary location.
1428 eval q{use File::Temp};
1430 my $tmpconfig=File::Temp->new();
1431 my @curlargs = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
1432 push(@curlargs, "-k") if $insecure;
1433 my $curlstatus = system(@curlargs);
1434 die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n" if $curlstatus >> 8 == 60;
1435 die "mr bootstrap: download of $url failed\n" if $curlstatus != 0;
1438 system("mkdir", "-p", $dir);
1440 chdir($dir) || die "chdir $dir: $!";
1442 # Special case to handle checkout of the "." repo, which
1443 # would normally be skipped.
1444 my $topdir=abs_path(".")."/";
1445 my @repo=($topdir, $topdir, ".");
1446 loadconfig($tmpconfig, $topdir);
1447 record(\@repo, action("checkout", @repo, 1))
1448 if exists $config{$topdir}{"."}{"checkout"};
1450 if (-e ".mrconfig") {
1451 print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n";
1454 eval q{use File::Copy};
1456 move($tmpconfig, ".mrconfig") || die "rename: $!";
1459 # Reload the config file (in case we got a different version)
1460 # and checkout everything else.
1462 loadconfig(".mrconfig");
1463 dispatch("checkout");
1464 @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped;
1465 showstats("bootstrap");
1469 # alias expansion and command stemming
1472 if (exists $alias{$action}) {
1473 $action=$alias{$action};
1475 if (! exists $knownactions{$action}) {
1476 my @matches = grep { /^\Q$action\E/ }
1477 keys %knownactions, keys %alias;
1478 if (@matches == 1) {
1479 $action=$matches[0];
1481 elsif (@matches == 0) {
1482 die "mr: unknown action \"$action\" (known actions: ".
1483 join(", ", sort keys %knownactions).")\n";
1486 die "mr: ambiguous action \"$action\" (matches: ".
1487 join(", ", @matches).")\n";
1495 while (length $dir) {
1496 if (-e "$dir/.mrconfig") {
1497 return "$dir/.mrconfig";
1499 $dir=~s/\/[^\/]*$//;
1501 return "$ENV{HOME}/.mrconfig";
1506 Getopt::Long::Configure("bundling", "no_permute");
1507 my $result=GetOptions(
1508 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1509 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1510 "p|path" => sub { }, # now default, ignore
1511 "v|verbose" => \$verbose,
1512 "q|quiet" => \$quiet,
1513 "s|stats" => \$stats,
1514 "k|insecure" => \$insecure,
1515 "i|interactive" => \$interactive,
1516 "n|no-recurse:i" => \$max_depth,
1517 "j|jobs:i" => \$jobs,
1518 "t|trust-all" => \$trust_all,
1520 if (! $result || @ARGV < 1) {
1521 die("Usage: mr [options] action [params ...]\n".
1522 "(Use mr help for man page.)\n");
1525 $ENV{MR_SWITCHES}="";
1526 foreach my $option (@saved) {
1527 last if $option eq $ARGV[0];
1528 $ENV{MR_SWITCHES}.="$option ";
1534 print STDERR "mr: interrupted\n";
1538 # This can happen if it's run in a directory that was removed
1539 # or other strangeness.
1540 if (! defined $directory) {
1541 die("mr: failed to determine working directory\n");
1543 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1544 # the config file might be a symlink to elsewhere, and the directory it's
1545 # in is significant.
1546 if ($ENV{MR_CONFIG} !~ /^\//) {
1547 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1549 # Try to set MR_PATH to the path to the program.
1551 use FindBin qw($Bin $Script);
1552 $ENV{MR_PATH}=$Bin."/".$Script;
1570 loadconfig("$ENV{HOME}/.mrconfig");
1571 loadconfig($ENV{MR_CONFIG});
1572 #use Data::Dumper; print Dumper(\%config);
1574 my $action=expandaction(shift @ARGV);
1581 # Finally, some useful actions that mr knows about by default.
1582 # These can be overridden in ~/.mrconfig.
1597 echo "mr (warning): $@" >&2
1603 if [ -z "$1" ] || [ -z "$2" ]; then
1604 error "mr: usage: hours_since action num"
1606 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
1607 if [ -e "$MR_REPO/$dir" ]; then
1608 flagfile="$MR_REPO/$dir/.mr_last$1"
1612 if [ -z "$flagfile" ]; then
1613 error "cannot determine flag filename"
1615 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1616 if [ "$delta" -lt "$2" ]; then
1624 svn_test = test -d "$MR_REPO"/.svn
1625 git_test = test -d "$MR_REPO"/.git
1626 bzr_test = test -d "$MR_REPO"/.bzr
1627 cvs_test = test -d "$MR_REPO"/CVS
1628 hg_test = test -d "$MR_REPO"/.hg
1629 darcs_test = test -d "$MR_REPO"/_darcs
1630 fossil_test = test -f "$MR_REPO"/_FOSSIL_
1632 test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
1633 test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
1634 test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
1636 svn_update = svn update "$@"
1637 git_update = git pull "$@"
1638 bzr_update = bzr merge --pull "$@"
1639 cvs_update = cvs update "$@"
1640 hg_update = hg pull "$@" && hg update "$@"
1641 darcs_update = darcs pull -a "$@"
1642 fossil_update = fossil pull "$@"
1644 svn_status = svn status "$@"
1645 git_status = git status -s "$@" || true
1646 bzr_status = bzr status --short "$@"
1647 cvs_status = cvs status "$@"
1648 hg_status = hg status "$@"
1649 darcs_status = darcs whatsnew -ls "$@" || true
1650 fossil_status = fossil changes "$@"
1652 svn_commit = svn commit "$@"
1653 git_commit = git commit -a "$@" && git push --all
1654 bzr_commit = bzr commit "$@" && bzr push
1655 cvs_commit = cvs commit "$@"
1656 hg_commit = hg commit -m "$@" && hg push
1657 darcs_commit = darcs record -a -m "$@" && darcs push -a
1658 fossil_commit = fossil commit "$@"
1660 git_record = git commit -a "$@"
1661 bzr_record = bzr commit "$@"
1662 hg_record = hg commit -m "$@"
1663 darcs_record = darcs record -a -m "$@"
1664 fossil_record = fossil commit "$@"
1667 git_push = git push "$@"
1668 bzr_push = bzr push "$@"
1670 hg_push = hg push "$@"
1671 darcs_push = darcs push -a "$@"
1672 fossil_push = fossil push "$@"
1674 svn_diff = svn diff "$@"
1675 git_diff = git diff "$@"
1676 bzr_diff = bzr diff "$@"
1677 cvs_diff = cvs diff "$@"
1678 hg_diff = hg diff "$@"
1679 darcs_diff = darcs diff -u "$@"
1680 fossil_diff = fossil diff "$@"
1682 svn_log = svn log "$@"
1683 git_log = git log "$@"
1684 bzr_log = bzr log "$@"
1685 cvs_log = cvs log "$@"
1686 hg_log = hg log "$@"
1687 darcs_log = darcs changes "$@"
1688 git_bare_log = git log "$@"
1689 fossil_log = fossil timeline "$@"
1692 url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
1693 if [ -z "$url" ]; then
1694 error "cannot determine svn url"
1696 echo "Registering svn url: $url in $MR_CONFIG"
1697 mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
1699 url="`LC_ALL=C git config --get remote.origin.url`" || true
1700 if [ -z "$url" ]; then
1701 error "cannot determine git url"
1703 echo "Registering git url: $url in $MR_CONFIG"
1704 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
1706 url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
1707 if [ -z "$url" ]; then
1708 error "cannot determine bzr url"
1710 echo "Registering bzr url: $url in $MR_CONFIG"
1711 mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
1713 repo=`cat CVS/Repository`
1715 if [ -z "$root" ]; then
1716 error "cannot determine cvs root"
1718 echo "Registering cvs repository $repo at root $root"
1719 mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
1721 url=`hg showconfig paths.default`
1722 echo "Registering mercurial repo url: $url in $MR_CONFIG"
1723 mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
1725 url=`cat _darcs/prefs/defaultrepo`
1726 echo "Registering darcs repository $url in $MR_CONFIG"
1727 mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
1729 url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
1730 if [ -z "$url" ]; then
1731 error "cannot determine git url"
1733 echo "Registering git url: $url in $MR_CONFIG"
1734 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
1736 url=`fossil remote-url`
1737 repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
1738 echo "Registering fossil repository $url in $MR_CONFIG"
1739 mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
1741 svn_trusted_checkout = svn co $url $repo
1742 svn_alt_trusted_checkout = svn checkout $url $repo
1743 git_trusted_checkout = git clone $url $repo
1744 bzr_trusted_checkout = bzr clone $url $repo
1746 hg_trusted_checkout = hg clone $url $repo
1747 darcs_trusted_checkout = darcs get $url $repo
1748 git_bare_trusted_checkout = git clone --bare $url $repo
1749 # fossil: messy to do
1755 SHOWMANFILE="man -f"
1761 SHOWMANFILE="man -l"
1764 if [ ! -e "$MR_PATH" ]; then
1765 error "cannot find program path"
1767 tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1768 trap "rm -f $tmp" exit
1769 pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1770 $SHOWMANFILE "$tmp" || error "man failed"
1776 if [ -s ~/.mrlog ]; then
1777 info "running offline commands"
1778 mv -f ~/.mrlog ~/.mrlog.old
1779 if ! sh -e ~/.mrlog.old; then
1780 error "offline command failed; left in ~/.mrlog.old"
1784 info "no offline commands to run"
1789 info "offline mode enabled"
1791 info "remembering command: 'mr $@'"
1792 command="mr -d '$(pwd)' $MR_SWITCHES"
1794 command="$command '$w'"
1796 if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
1797 echo "$command" >> ~/.mrlog
1800 ed = echo "A horse is a horse, of course, of course.."
1801 T = echo "I pity the fool."
1802 right = echo "Not found."
1804 # vim:sw=8:sts=0:ts=8:noet