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"]
23 B<mr> [options] bootstrap url
25 B<mr> [options] register [repository]
27 B<mr> [options] config section ["parameter=[value]" ...]
29 B<mr> [options] action [params ...]
31 B<mr> [options] [online|offline]
33 B<mr> [options] remember action [params ...]
37 B<mr> is a Multiple Repository management tool. It can checkout, update, or
38 perform other actions on a set of repositories as if they were one combined
39 repository. It supports any combination of subversion, git, cvs, mecurial,
40 bzr and darcs repositories, and support for other revision control systems can
43 B<mr> cds into and operates on all registered repositories at or below your
44 working directory. Or, if you are in a subdirectory of a repository that
45 contains no other registered repositories, it will stay in that directory,
46 and work on only that repository,
48 B<mr> is configured by .mrconfig files, which list the repositories. It
49 starts by reading the .mrconfig file in your home directory, and this can
50 in turn chain load .mrconfig files from repositories.
52 These predefined commands should be fairly familiar to users of any revision
57 =item checkout (or co)
59 Checks out any repositories that are not already checked out.
63 Updates each repository from its configured remote repository.
65 If a repository isn't checked out yet, it will first check it out.
69 Displays a status report for each repository, showing what
70 uncommitted changes are present in the repository.
74 Commits changes to each repository. (By default, changes are pushed to the
75 remote repository too, when using distributed systems like git. If you
76 don't like this default, you can change it in your .mrconfig, or use record
79 The optional -m parameter allows specifying a commit message.
83 Records changes to the local repository, but does not push them to the
84 remote repository. Only supported for distributed revision control systems.
86 The optional -m parameter allows specifying a commit message.
90 Pushes committed local changes to the remote repository. A no-op for
91 centralized revision control systems.
95 Show a diff of uncommitted changes.
103 These commands are also available:
109 Causes mr to download the url, save it to a .mrconfig file in the
110 current directory, and then check out all repositories listed in it.
114 List the repositories that mr will act on.
118 Register an existing repository in a mrconfig file. By default, the
119 repository in the current directory is registered, or you can specify a
120 directory to register.
122 The mrconfig file that is modified is chosen by either the -c option, or by
123 looking for the closest known one at or below the current directory.
127 Adds, modifies, removes, or prints a value from a mrconfig file. The next
128 parameter is the name of the section the value is in. To add or modify
129 values, use one or more instances of "parameter=value". Use "parameter=" to
130 remove a parameter. Use just "parameter" to get the value of a parameter.
132 For example, to add (or edit) a repository in src/foo:
134 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
136 To show the command that mr uses to update the repository in src/foo:
138 mr config src/foo update
140 To see the built-in library of shell functions contained in mr:
142 mr config DEFAULT lib
144 The ~/.mrconfig file is used by default. To use a different config file,
149 Advises mr that it is in offline mode. Any commands that fail in
150 offline mode will be remembered, and retried when mr is told it's online.
154 Advices mr that it is in online mode again. Commands that failed while in
155 offline mode will be re-run.
159 Remember a command, to be run later when mr re-enters online mode. This
160 implicitly puts mr into offline mode. The command can be any regular mr
161 command. This is useful when you know that a command will fail due to being
162 offline, and so don't want to run it right now at all, but just remember
163 to run it when you go back online.
171 Actions can be abbreviated to any unambiguous substring, so
172 "mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
175 Additional parameters can be passed to most commands, and are passed on
176 unchanged to the underlying revision control system. This is mostly useful
177 if the repositories mr will act on all use the same revision control
186 Specifies the topmost directory that B<mr> should work in. The default is
187 the current working directory.
191 Use the specified mrconfig file. The default is B<~/.mrconfig>
195 Search in the current directory, and its parent directories and use
196 the first B<.mrconfig> found, instead of the default B<~/.mrconfig>.
208 Expand the statistics line displayed at the end to include information
209 about exactly which repositories failed and were skipped, if any.
213 Interactive mode. If a repository fails to be processed, a subshell will be
214 started which you can use to resolve or investigate the problem. Exit the
215 subshell to continue the mr run.
219 If no number if specified, just operate on the repository for the current
220 directory, do not recurse into deeper repositories.
222 If a number is specified, will recurse into repositories at most that many
223 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
224 but not ./src/packages/bar.
228 Run the specified number of jobs in parallel, or an unlimited number of jobs
229 with no number specified. This can greatly speed up operations such as updates.
230 It is not recommended for interactive operations.
232 Note that running more than 10 jobs at a time is likely to run afoul of
233 ssh connection limits. Running between 3 and 5 jobs at a time will yield
234 a good speedup in updates without loading the machine too much.
238 =head1 "MRCONFIG FILES"
240 Here is an example .mrconfig file:
243 checkout = svn co svn://svn.example.com/src/trunk src
247 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
249 git checkout -b mybranch origin/master
251 The .mrconfig file uses a variant of the INI file format. Lines starting with
252 "#" are comments. Values can be continued to the following line by
253 indenting the line with whitespace.
255 The "DEFAULT" section allows setting default values for the sections that
258 The "ALIAS" section allows adding aliases for actions. Each parameter
259 is an alias, and its value is the action to use.
261 All other sections add repositories. The section header specifies the
262 directory where the repository is located. This is relative to the directory
263 that contains the mrconfig file, but you can also choose to use absolute
264 paths. (Note that you can use environment variables in section names; they
265 will be passed through the shell for expansion. For example,
266 "[$HOSTNAME]", or "[${HOSTNAME}foo]")
268 Within a section, each parameter defines a shell command to run to handle a
269 given action. mr contains default handlers for "update", "status",
270 "commit", and other standard actions. Normally you only need to specify what
271 to do for "checkout".
273 Note that these shell commands are run in a "set -e" shell
274 environment, where any additional parameters you pass are available in
275 "$@". The "checkout" command is run in the parent of the repository
276 directory, since the repository isn't checked out yet. All other commands
277 are run inside the repository, though not necessarily at the top of it.
279 The "MR_REPO" environment variable is set to the path to the top of the
280 repository. (For the "register" action, "MR_REPO" is instead set to the
281 basename of the directory that should be created when checking the
284 The "MR_CONFIG" environment variable is set to the .mrconfig file
285 that defines the repo being acted on, or, if the repo is not yet in a config
286 file, the .mrconfig file that should be modified to register the repo.
288 A few parameters have special meanings:
294 If the "skip" parameter is set and its command returns true, then B<mr>
295 will skip acting on that repository. The command is passed the action
298 Here are two examples. The first skips the repo unless
299 mr is run by joey. The second uses the hours_since function
300 (included in mr's built-in library) to skip updating the repo unless it's
301 been at least 12 hours since the last update.
303 skip = test `whoami` != joey
304 skip = [ "$1" = update ] && ! hours_since "$1" 12
308 The "order" parameter can be used to override the default ordering of
309 repositories. The default order value is 10. Use smaller values to make
310 repositories be processed earlier, and larger values to make repositories
313 Note that if a repository is located in a subdirectory of another
314 repository, ordering it to be processed earlier is not recommended.
318 If the "chain" parameter is set and its command returns true, then B<mr>
319 will try to load a .mrconfig file from the root of the repository.
323 If the "include" parameter is set, its command is ran, and should output
324 additional mrconfig file content. The content is included as if it were
325 part of the including file.
327 Unlike all other parameters, this parameter does not need to be placed
332 The "lib" parameter can specify some shell code that will be run before each
333 command, this can be a useful way to define shell functions for other commands
338 When looking for a command to run for a given action, mr first looks for
339 a parameter with the same name as the action. If that is not found, it
340 looks for a parameter named "rcs_action" (substituting in the name of the
341 revision control system and the action). The name of the revision control
342 system is itself determined by running each defined "rcs_test" action,
345 Internally, mr has settings for "git_update", "svn_update", etc. To change
346 the action that is performed for a given revision control system, you can
347 override these rcs specific actions. To add a new revision control system,
348 you can just add rcs specific actions for it.
350 The ~/.mrlog file contains commands that mr has remembered to run later,
351 due to being offline. You can delete or edit this file to remove commands,
352 or even to add other commands for 'mr online' to run. If the file is
353 present, mr assumes it is in offline mode.
355 =head "UNTRUSTED MRCONFIG FILES"
357 Since mrconfig files can contain arbitrary shell commands, they can do
358 anything. This flexability is good, but it also allows a malicious mrconfig
359 file to delete your whole home directory. Such a file might be contained
360 inside a repository that your main ~/.mrconfig checks out. To avoid worries
361 about a malicious change being committed to such a file, mr has the ability
362 to read mrconfig files in untrusted mode. Such files are limited to running
363 only known safe commands (like "git clone").
365 By default, mr trusts all mrconfig files. (This default will change in a
366 future release!) But if you have a ~/.mrtrust file, mr will only trust
367 mrconfig files that are listed within it. (One file per line.) All other
368 files will be treated as untrusted.
372 mr can be extended to support things such as unison and git-svn. Some
373 files providing such extensions are available in /usr/share/mr/. See
374 the documentation in the files for details about using them.
378 Copyright 2007-2009 Joey Hess <joey@kitenet.net>
380 Licensed under the GNU GPL version 2 or higher.
382 http://kitenet.net/~joey/code/mr/
389 use Cwd qw(getcwd abs_path);
391 # things that can happen when mr runs a command
400 my $config_overridden=0;
408 my $directory=getcwd();
409 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
416 my (@ok, @failed, @skipped);
422 my ($action, $dir, $topdir, $subdir) = @_;
424 if (exists $rcs{$dir}) {
429 foreach my $rcs_test (
431 length $a <=> length $b
434 } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
435 my ($rcs)=$rcs_test=~/(.*)_test/;
436 $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test;
437 $test.="if my_$rcs_test; then echo $rcs; fi\n";
439 $test=$config{$topdir}{$subdir}{lib}."\n".$test
440 if exists $config{$topdir}{$subdir}{lib};
442 print "mr $action: running rcs test >>$test<<\n" if $verbose;
447 print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n";
451 return $rcs{$dir}=undef;
454 return $rcs{$dir}=$rcs;
459 my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
461 if (exists $config{$topdir}{$subdir}{$action}) {
462 return $config{$topdir}{$subdir}{$action};
469 my $rcs=rcs_test(@_);
472 exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
473 return $config{$topdir}{$subdir}{$rcs."_".$action};
481 my ($action, $dir, $topdir, $subdir) = @_;
483 $ENV{MR_CONFIG}=$configfiles{$topdir};
484 my $lib=exists $config{$topdir}{$subdir}{lib} ?
485 $config{$topdir}{$subdir}{lib}."\n" : "";
486 my $is_checkout=($action eq 'checkout');
492 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
496 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
498 elsif ($action =~ /update/) {
500 return action("checkout", $dir, $topdir, $subdir);
504 my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
505 my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
507 if (defined $skiptest) {
508 my $test="set -e;".$lib.
509 "my_action(){ $skiptest\n }; my_action '$action'";
510 print "mr $action: running skip test >>$test<<\n" if $verbose;
511 my $ret=system($test);
513 if (($? & 127) == 2) {
514 print STDERR "mr $action: interrupted\n";
518 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
522 if ($ret >> 8 == 0) {
523 print "mr $action: $dir skipped per config file\n" if $verbose;
528 if ($is_checkout && ! -d $dir) {
529 print "mr $action: creating parent directory $dir\n" if $verbose;
530 system("mkdir", "-p", $dir);
533 if (! $no_chdir && ! chdir($dir)) {
534 print STDERR "mr $action: failed to chdir to $dir: $!\n";
537 elsif (! defined $command) {
538 my $rcs=rcs_test(@_);
539 if (! defined $rcs) {
540 print STDERR "mr $action: unknown repository type and no defined $action command for $topdir$subdir\n";
544 print STDERR "mr $action: no defined action for $rcs repository $topdir$subdir, skipping\n";
550 print "mr $action: $topdir$subdir\n" unless $quiet;
554 $s=~s/^\Q$topdir$subdir\E\/?//;
555 print "mr $action: $topdir$subdir (in subdir $s)\n" unless $quiet;
557 $command="set -e; ".$lib.
558 "my_action(){ $command\n }; my_action ".
559 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
560 print "mr $action: running >>$command<<\n" if $verbose;
561 my $ret=system($command);
563 if (($? & 127) == 2) {
564 print STDERR "mr $action: interrupted\n";
568 print STDERR "mr $action: received signal ".($? & 127)."\n";
571 print STDERR "mr $action: failed ($ret)\n" if $verbose;
572 if ($ret >> 8 != 0) {
573 print STDERR "mr $action: command failed\n";
574 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
575 # recreate original command line to
576 # remember, and avoid recursing
578 @ARGV=('-n', $action, @orig);
579 action("remember", $dir, $topdir, $subdir);
584 print STDERR "mr $action: command died ($ret)\n";
589 if ($action eq 'checkout' && ! -d $dir) {
590 print STDERR "mr $action: $dir missing after checkout\n";;
599 # run actions on multiple repos, in parallel
609 while (@fhs or @repos) {
610 while ((!$jobs || $running < $jobs) && @repos) {
612 my $repo = shift @repos;
613 pipe(my $outfh, CHILD_STDOUT);
614 pipe(my $errfh, CHILD_STDERR);
616 unless ($pid = fork) {
617 die "mr $action: cannot fork: $!" unless defined $pid;
618 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
619 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
624 exit action($action, @$repo);
628 push @active, [$pid, $repo];
629 push @fhs, [$outfh, $errfh];
632 my ($rin, $rout) = ('','');
634 foreach my $fh (@fhs) {
635 next unless defined $fh;
636 vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
637 vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
639 $nfound = select($rout=$rin, undef, undef, 1);
640 foreach my $channel (0, 1) {
641 foreach my $i (0..$#fhs) {
642 next unless defined $fhs[$i];
643 my $fh = $fhs[$i][$channel];
644 next unless defined $fh;
645 if (vec($rout, fileno($fh), 1) == 1) {
647 if (sysread($fh, $r, 1024) == 0) {
649 $fhs[$i][$channel] = undef;
650 if (! defined $fhs[$i][0] &&
651 ! defined $fhs[$i][1]) {
652 waitpid($active[$i][0], 0);
653 print STDOUT $out[$i][0];
654 print STDERR $out[$i][1];
655 record($active[$i][1], $? >> 8);
657 splice(@active, $i, 1);
662 $out[$i][$channel] .= $r;
670 my $dir=shift()->[0];
677 elsif ($ret == FAILED) {
679 chdir($dir) unless $no_chdir;
680 print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
681 system((getpwuid($<))[8]);
686 elsif ($ret == SKIPPED) {
689 elsif ($ret == ABORT) {
693 die "unknown exit status $ret";
699 if (! @ok && ! @failed && ! @skipped) {
700 die "mr $action: no repositories found to work on\n";
702 print "mr $action: finished (".join("; ",
703 showstat($#ok+1, "ok", "ok"),
704 showstat($#failed+1, "failed", "failed"),
705 showstat($#skipped+1, "skipped", "skipped"),
706 ).")\n" unless $quiet;
709 print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
712 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
722 return "$count ".($count > 1 ? $plural : $singular);
727 # an ordered list of repos
730 foreach my $topdir (sort keys %config) {
731 foreach my $subdir (sort keys %{$config{$topdir}}) {
735 order => $config{$topdir}{$subdir}{order},
740 $a->{order} <=> $b->{order}
742 $a->{topdir} cmp $b->{topdir}
744 $a->{subdir} cmp $b->{subdir}
748 # figure out which repos to act on
751 foreach my $repo (repolist()) {
752 my $topdir=$repo->{topdir};
753 my $subdir=$repo->{subdir};
755 next if $subdir eq 'DEFAULT';
756 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
758 $dir.="/" unless $dir=~/\/$/;
759 $d.="/" unless $d=~/\/$/;
760 next if $dir ne $d && $dir !~ /^\Q$d\E/;
761 if (defined $max_depth) {
762 my @a=split('/', $dir);
763 my @b=split('/', $d);
764 do { } while (@a && @b && shift(@a) eq shift(@b));
765 next if @a > $max_depth || @b > $max_depth;
767 push @repos, [$dir, $topdir, $subdir];
770 # fallback to find a leaf repo
771 foreach my $repo (reverse repolist()) {
772 my $topdir=$repo->{topdir};
773 my $subdir=$repo->{subdir};
775 next if $subdir eq 'DEFAULT';
776 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
778 $dir.="/" unless $dir=~/\/$/;
779 $d.="/" unless $d=~/\/$/;
780 if ($d=~/^\Q$dir\E/) {
781 push @repos, [$dir, $topdir, $subdir];
803 sub is_trusted_config {
804 my $config=shift; # must be abs_pathed already
806 # We always trust ~/.mrconfig.
807 return 1 if $config eq abs_path("$ENV{HOME}/.mrconfig");
809 my $trustfile=$ENV{HOME}."/.mrtrust";
811 if (! -e $trustfile) {
812 print "mr: Assuming $config is trusted.\n";
813 print "mr: For better security, you are encouraged to create ~/.mrtrust\n";
814 print "mr: and list all trusted mrconfig files in it.\n";
819 $trusted{"$ENV{HOME}/.mrconfig"}=1;
820 open (TRUST, "<", $trustfile) || die "$trustfile: $!";
823 s/^~\//$ENV{HOME}\//;
824 $trusted{abs_path($_)}=1;
829 return $trusted{$config};
833 sub is_trusted_repo {
836 # Tightly limit what is allowed in a repo name.
837 # No ../, no absolute paths, and no unusual filenames
838 # that might try to escape to the shell.
839 return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
840 $repo !~ /\.\./ && $repo !~ /^\//;
843 sub is_trusted_checkout {
846 # To determine if the command is safe, compare it with the
847 # *_trusted_checkout config settings. Those settings are
848 # templates for allowed commands, so make sure that each word
849 # of the command matches the corresponding word of the template.
852 foreach my $word (split(' ', $command)) {
854 if ($word=~/^'(.*)'$/) {
857 elsif ($word=~/^"(.*)"$/) {
864 foreach my $key (grep { /_trusted_checkout$/ }
865 keys %{$config{''}{DEFAULT}}) {
866 my @twords=split(' ', $config{''}{DEFAULT}{$key});
867 next if @words > @twords;
871 for (my $c=0; $c < @twords && $match; $c++) {
872 if ($twords[$c] eq '$url') {
873 # Match all the typical characters found in
874 # urls, plus @ which svn can use. Note
875 # that the "url" might also be a local
878 defined $words[$c] &&
879 $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
883 elsif ($twords[$c] eq '$repo') {
884 # If a repo is not specified, assume it
885 # will be the last path component of the
886 # url, or something derived from it, and
888 if (! defined $words[$c] && defined $url) {
889 ($words[$c])=$url=~/\/([^\/]+)\/?$/;
893 defined $words[$c] &&
894 is_trusted_repo($words[$c])
897 elsif (defined $words[$c] && $twords[$c] eq $words[$c]) {
919 if (ref $f eq 'GLOB') {
929 my $absf=abs_path($f);
930 if ($loaded{$absf}) {
935 $trusted=is_trusted_config($absf);
937 ($dir)=$f=~/^(.*\/)[^\/]+$/;
938 if (! defined $dir) {
941 $dir=abs_path($dir)."/";
943 if (! exists $configfiles{$dir}) {
944 $configfiles{$dir}=$f;
947 # copy in defaults from first parent
949 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
950 if ($parent eq '/') {
953 if (exists $config{$parent} &&
954 exists $config{$parent}{DEFAULT}) {
955 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
960 print "mr: loading config $f\n" if $verbose;
961 open($in, "<", $f) || die "mr: open $f: $!\n";
972 next if /^\s*\#/ || /^\s*$/;
973 if (/^\[([^\]]*)\]\s*$/) {
977 if (! is_trusted_repo($section) ||
978 $section eq 'ALIAS' ||
979 $section eq 'DEFAULT') {
980 die "mr: illegal section \"[$section]\" in untrusted $f line $line\n";
983 $section=expandenv($section) if $trusted;
985 elsif (/^(\w+)\s*=\s*(.*)/) {
990 while (@lines && $lines[0]=~/^\s(.+)/) {
998 # Untrusted files can only contain checkout
1000 if ($parameter ne 'checkout') {
1001 die "mr: illegal setting \"$parameter=$value\" in untrusted $f line $line\n";
1003 if (! is_trusted_checkout($value)) {
1004 die "mr: illegal checkout command \"$value\" in untrusted $f line $line\n";
1008 if ($parameter eq "include") {
1009 print "mr: including output of \"$value\"\n" if $verbose;
1010 unshift @lines, `$value`;
1012 print STDERR "mr: include command exited nonzero ($?)\n";
1017 if (! defined $section) {
1018 die "$f line $.: parameter ($parameter) not in section\n";
1020 if ($section ne 'ALIAS' &&
1021 ! exists $config{$dir}{$section} &&
1022 exists $config{$dir}{DEFAULT}) {
1024 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1026 if ($section eq 'ALIAS') {
1027 $alias{$parameter}=$value;
1029 elsif ($parameter eq 'lib') {
1030 $config{$dir}{$section}{lib}.=$value."\n";
1033 $config{$dir}{$section}{$parameter}=$value;
1034 if ($parameter =~ /.*_(.*)/) {
1035 $knownactions{$1}=1;
1038 $knownactions{$parameter}=1;
1040 if ($parameter eq 'chain' &&
1041 length $dir && $section ne "DEFAULT" &&
1042 -e $dir.$section."/.mrconfig") {
1043 my $ret=system($value);
1045 if (($? & 127) == 2) {
1046 print STDERR "mr: chain test interrupted\n";
1050 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1054 push @toload, $dir.$section."/.mrconfig";
1060 die "$f line $line: parse error\n";
1071 # the section to modify or add
1072 my $targetsection=shift;
1073 # fields to change in the section
1074 # To remove a field, set its value to "".
1075 my %changefields=@_;
1081 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1086 my $formatfield=sub {
1088 my @value=split(/\n/, shift);
1090 return "$field = ".shift(@value)."\n".
1091 join("", map { "\t$_\n" } @value);
1095 while ($out[$#out] =~ /^\s*$/) {
1096 unshift @blanks, pop @out;
1098 foreach my $field (sort keys %changefields) {
1099 if (length $changefields{$field}) {
1100 push @out, "$field = $changefields{$field}\n";
1101 delete $changefields{$field};
1111 if (/^\s*\#/ || /^\s*$/) {
1114 elsif (/^\[([^\]]*)\]\s*$/) {
1115 if (defined $section &&
1116 $section eq $targetsection) {
1120 $section=expandenv($1);
1124 elsif (/^(\w+)\s*=\s(.*)/) {
1129 while (@lines && $lines[0]=~/^\s(.+)/) {
1135 if ($section eq $targetsection) {
1136 if (exists $changefields{$parameter}) {
1137 if (length $changefields{$parameter}) {
1138 $value=$changefields{$parameter};
1140 delete $changefields{$parameter};
1144 push @out, $formatfield->($parameter, $value);
1148 if (defined $section &&
1149 $section eq $targetsection) {
1152 elsif (%changefields) {
1153 push @out, "\n[$targetsection]\n";
1154 foreach my $field (sort keys %changefields) {
1155 if (length $changefields{$field}) {
1156 push @out, $formatfield->($field, $changefields{$field});
1161 open(my $out, ">", $f) || die "mr: write $f: $!\n";
1169 # actions that do not operate on all repos
1170 if ($action eq 'help') {
1173 elsif ($action eq 'config') {
1176 elsif ($action eq 'register') {
1179 elsif ($action eq 'bootstrap') {
1182 elsif ($action eq 'remember' ||
1183 $action eq 'offline' ||
1184 $action eq 'online') {
1185 my @repos=selectrepos;
1186 action($action, @{$repos[0]}) if @repos;
1190 if (!$jobs || $jobs > 1) {
1191 mrs($action, selectrepos());
1194 foreach my $repo (selectrepos()) {
1195 record($repo, action($action, @$repo));
1201 exec($config{''}{DEFAULT}{help}) || die "exec: $!";
1206 die "mr config: not enough parameters\n";
1209 if ($section=~/^\//) {
1210 # try to convert to a path relative to the config file
1211 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1212 $dir=abs_path($dir);
1213 $dir.="/" unless $dir=~/\/$/;
1214 if ($section=~/^\Q$dir\E(.*)/) {
1220 if (/^([^=]+)=(.*)$/) {
1221 $changefields{$1}=$2;
1225 foreach my $topdir (sort keys %config) {
1226 if (exists $config{$topdir}{$section} &&
1227 exists $config{$topdir}{$section}{$_}) {
1228 print $config{$topdir}{$section}{$_}."\n";
1230 last if $section eq 'DEFAULT';
1234 die "mr config: $section $_ not set\n";
1238 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1243 if ($config_overridden) {
1244 # Find the directory that the specified config file is
1246 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1249 # Find the closest known mrconfig file to the current
1251 $directory.="/" unless $directory=~/\/$/;
1253 foreach my $topdir (reverse sort keys %config) {
1254 next unless length $topdir;
1255 if ($directory=~/^\Q$topdir\E/) {
1256 $ENV{MR_CONFIG}=$configfiles{$topdir};
1262 if (! $foundconfig) {
1263 $directory=""; # no config file, use builtin
1267 my $subdir=shift @ARGV;
1268 if (! chdir($subdir)) {
1269 print STDERR "mr register: failed to chdir to $subdir: $!\n";
1273 $ENV{MR_REPO}=getcwd();
1274 my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1275 if (! defined $command) {
1276 die "mr register: unknown repository type\n";
1279 $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1280 $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1281 "my_action(){ $command\n }; my_action ".
1282 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1283 print "mr register: running >>$command<<\n" if $verbose;
1284 exec($command) || die "exec: $!";
1288 my $url=shift @ARGV;
1290 if (! defined $url || ! length $url) {
1291 die "mr: bootstrap requires url\n";
1294 if (-e ".mrconfig") {
1295 die "mr: .mrconfig file already exists, not overwriting with $url\n";
1298 if (system("curl", "-s", $url, "-o", ".mrconfig") != 0) {
1299 die "mr: download of $url failed\n";
1302 exec("mr $ENV{MR_SWITCHES} -c .mrconfig checkout");
1303 die "failed to run mr checkout";
1306 # alias expansion and command stemming
1309 if (exists $alias{$action}) {
1310 $action=$alias{$action};
1312 if (! exists $knownactions{$action}) {
1313 my @matches = grep { /^\Q$action\E/ }
1314 keys %knownactions, keys %alias;
1315 if (@matches == 1) {
1316 $action=$matches[0];
1318 elsif (@matches == 0) {
1319 die "mr: unknown action \"$action\" (known actions: ".
1320 join(", ", sort keys %knownactions).")\n";
1323 die "mr: ambiguous action \"$action\" (matches: ".
1324 join(", ", @matches).")\n";
1330 sub find_nearest_mrconfig {
1332 while (length $dir) {
1333 if (-e "$dir/.mrconfig") {
1334 return "$dir/.mrconfig";
1336 $dir=~s/\/[^\/]*$//;
1338 die "no .mrconfig found in path\n";
1343 Getopt::Long::Configure("bundling", "no_permute");
1344 my $result=GetOptions(
1345 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1346 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1347 "p|path" => sub { $ENV{MR_CONFIG}=find_nearest_mrconfig(); $config_overridden=1 },
1348 "v|verbose" => \$verbose,
1349 "q|quiet" => \$quiet,
1350 "s|stats" => \$stats,
1351 "i|interactive" => \$interactive,
1352 "n|no-recurse:i" => \$max_depth,
1353 "j|jobs:i" => \$jobs,
1355 if (! $result || @ARGV < 1) {
1356 die("Usage: mr [options] action [params ...]\n".
1357 "(Use mr help for man page.)\n");
1360 $ENV{MR_SWITCHES}="";
1361 foreach my $option (@saved) {
1362 last if $option eq $ARGV[0];
1363 $ENV{MR_SWITCHES}.="$option ";
1369 print STDERR "mr: interrupted\n";
1373 # This can happen if it's run in a directory that was removed
1374 # or other strangeness.
1375 if (! defined $directory) {
1376 die("mr: failed to determine working directory\n");
1378 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1379 # the config file might be a symlink to elsewhere, and the directory it's
1380 # in is significant.
1381 if ($ENV{MR_CONFIG} !~ /^\//) {
1382 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1384 # Try to set MR_PATH to the path to the program.
1386 use FindBin qw($Bin $Script);
1387 $ENV{MR_PATH}=$Bin."/".$Script;
1396 loadconfig($ENV{MR_CONFIG});
1397 #use Data::Dumper; print Dumper(\%config);
1399 my $action=expandaction(shift @ARGV);
1406 elsif (! @ok && @skipped) {
1414 # Finally, some useful actions that mr knows about by default.
1415 # These can be overridden in ~/.mrconfig.
1430 echo "mr (warning): $@" >&2
1436 if [ -z "$1" ] || [ -z "$2" ]; then
1437 error "mr: usage: hours_since action num"
1439 for dir in .git .svn .bzr CVS .hg _darcs; do
1440 if [ -e "$MR_REPO/$dir" ]; then
1441 flagfile="$MR_REPO/$dir/.mr_last$1"
1445 if [ -z "$flagfile" ]; then
1446 error "cannot determine flag filename"
1448 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1449 if [ "$delta" -lt "$2" ]; then
1457 svn_test = test -d "$MR_REPO"/.svn
1458 git_test = test -d "$MR_REPO"/.git
1459 bzr_test = test -d "$MR_REPO"/.bzr
1460 cvs_test = test -d "$MR_REPO"/CVS
1461 hg_test = test -d "$MR_REPO"/.hg
1462 darcs_test = test -d "$MR_REPO"/_darcs
1464 test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
1465 test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
1466 test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
1468 svn_update = svn update "$@"
1469 git_update = git pull "$@"
1470 bzr_update = bzr merge --pull "$@"
1471 cvs_update = cvs update "$@"
1472 hg_update = hg pull "$@" && hg update "$@"
1473 darcs_update = darcs pull -a "$@"
1475 svn_status = svn status "$@"
1476 git_status = git status "$@" || true
1477 bzr_status = bzr status "$@"
1478 cvs_status = cvs status "$@"
1479 hg_status = hg status "$@"
1480 darcs_status = darcs whatsnew -ls "$@" || true
1482 svn_commit = svn commit "$@"
1483 git_commit = git commit -a "$@" && git push --all
1484 bzr_commit = bzr commit "$@" && bzr push
1485 cvs_commit = cvs commit "$@"
1486 hg_commit = hg commit -m "$@" && hg push
1487 darcs_commit = darcs record -a -m "$@" && darcs push -a
1489 git_record = git commit -a "$@"
1490 bzr_record = bzr commit "$@"
1491 hg_record = hg commit -m "$@"
1492 darcs_record = darcs record -a -m "$@"
1495 git_push = git push "$@"
1496 bzr_push = bzr push "$@"
1498 hg_push = hg push "$@"
1499 darcs_push = darcs push -a "$@"
1501 svn_diff = svn diff "$@"
1502 git_diff = git diff "$@"
1503 bzr_diff = bzr diff "$@"
1504 cvs_diff = cvs diff "$@"
1505 hg_diff = hg diff "$@"
1506 darcs_diff = darcs diff -u "$@"
1508 svn_log = svn log "$@"
1509 git_log = git log "$@"
1510 bzr_log = bzr log "$@"
1511 cvs_log = cvs log "$@"
1512 hg_log = hg log "$@"
1513 darcs_log = darcs changes "$@"
1514 git_bare_log = git log "$@"
1517 url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
1518 if [ -z "$url" ]; then
1519 error "cannot determine svn url"
1521 echo "Registering svn url: $url in $MR_CONFIG"
1522 mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
1524 url="`LC_ALL=C git config --get remote.origin.url`" || true
1525 if [ -z "$url" ]; then
1526 error "cannot determine git url"
1528 echo "Registering git url: $url in $MR_CONFIG"
1529 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
1531 url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
1532 if [ -z "$url" ]; then
1533 error "cannot determine bzr url"
1535 echo "Registering bzr url: $url in $MR_CONFIG"
1536 mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
1538 repo=`cat CVS/Repository`
1540 if [ -z "$root" ]; then
1541 error "cannot determine cvs root"
1543 echo "Registering cvs repository $repo at root $root"
1544 mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
1546 url=`hg showconfig paths.default`
1547 echo "Registering mercurial repo url: $url in $MR_CONFIG"
1548 mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
1550 url=`cat _darcs/prefs/defaultrepo`
1551 echo "Registering darcs repository $url in $MR_CONFIG"
1552 mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
1554 url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
1555 if [ -z "$url" ]; then
1556 error "cannot determine git url"
1558 echo "Registering git url: $url in $MR_CONFIG"
1559 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
1561 svn_trusted_checkout = svn co $url $repo
1562 svn_alt_trusted_checkout = svn checkout $url $repo
1563 git_trusted_checkout = git clone $url $repo
1564 bzr_trusted_checkout = bzr clone $url $repo
1566 hg_trusted_checkout = hg clone $url $repo
1567 darcs_trusted_checkout = darcs get $url $repo
1568 git_bare_trusted_checkout = git clone --bare $url $repo
1571 if [ ! -e "$MR_PATH" ]; then
1572 error "cannot find program path"
1574 tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1575 trap "rm -f $tmp" exit
1576 pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1577 man -l "$tmp" || error "man failed"
1583 if [ -s ~/.mrlog ]; then
1584 info "running offline commands"
1585 mv -f ~/.mrlog ~/.mrlog.old
1586 if ! sh -e ~/.mrlog.old; then
1587 error "offline command failed; left in ~/.mrlog.old"
1591 info "no offline commands to run"
1596 info "offline mode enabled"
1598 info "remembering command: 'mr $@'"
1599 command="mr -d '$(pwd)' $MR_SWITCHES"
1601 command="$command '$w'"
1603 if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
1604 echo "$command" >> ~/.mrlog
1607 ed = echo "A horse is a horse, of course, of course.."
1608 T = echo "I pity the fool."
1609 right = echo "Not found."
1611 # vim:sw=8:sts=0:ts=8:noet