]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

bootstrap: add support to local files
[code/myrepos.git] / mr
1 #!/usr/bin/env perl
2
3 =head1 NAME
4
5 mr - a tool to manage all your version control repos
6
7 =head1 SYNOPSIS
8
9 B<mr> [options] checkout
10
11 B<mr> [options] update
12
13 B<mr> [options] status
14
15 B<mr> [options] commit [-m "message"]
16
17 B<mr> [options] record [-m "message"]
18
19 B<mr> [options] fetch
20
21 B<mr> [options] push
22
23 B<mr> [options] diff
24
25 B<mr> [options] log
26
27 B<mr> [options] grep pattern
28
29 B<mr> [options] run command [param ...]
30
31 B<mr> [options] bootstrap url [directory]
32
33 B<mr> [options] register [repository]
34
35 B<mr> [options] config section ["parameter=[value]" ...]
36
37 B<mr> [options] action [params ...]
38
39 B<mr> [options] [online|offline]
40
41 B<mr> [options] remember action [params ...]
42
43 =head1 DESCRIPTION
44
45 B<mr> is a tool to manage all your version control repos. It can checkout,
46 update, or perform other actions on a set of repositories as if they were
47 one combined repository. It supports any combination of subversion, git,
48 cvs, mercurial, bzr, darcs, fossil and veracity repositories, and support
49 for other version control systems can easily be added.
50
51 B<mr> cds into and operates on all registered repositories at or below your
52 working directory. Or, if you are in a subdirectory of a repository that
53 contains no other registered repositories, it will stay in that directory,
54 and work on only that repository,
55
56 B<mr> is configured by .mrconfig files, which list the repositories. It
57 starts by reading the .mrconfig file in your home directory, and this can
58 in turn chain load .mrconfig files from repositories. It also automatically
59 looks for a .mrconfig file in the current directory, or in one of its
60 parent directories.
61
62 These predefined commands should be fairly familiar to users of any version
63 control system:
64
65 =over 4
66
67 =item checkout (or co)
68
69 Checks out any repositories that are not already checked out.
70
71 =item update
72
73 Updates each repository from its configured remote repository.
74
75 If a repository isn't checked out yet, it will first check it out.
76
77 =item status
78
79 Displays a status report for each repository, showing what
80 uncommitted changes are present in the repository. For distributed version
81 control systems, also shows unpushed local branches.
82
83 =item commit (or ci)
84
85 Commits changes to each repository. (By default, changes are pushed to the
86 remote repository too, when using distributed systems like git. If you
87 don't like this default, you can change it in your .mrconfig, or use record
88 instead.)
89
90 The optional -m parameter allows specifying a commit message.
91
92 =item record
93
94 Records changes to the local repository, but does not push them to the
95 remote repository. Only supported for distributed version control systems.
96
97 The optional -m parameter allows specifying a commit message.
98
99 =item fetch
100
101 Fetches from each repository's remote repository, but does not
102 update the working copy. Only supported for some distributed version
103 control systems.
104
105 =item push
106
107 Pushes committed local changes to the remote repository. A no-op for
108 centralized version control systems.
109
110 =item diff
111
112 Show a diff of uncommitted changes.
113
114 =item log
115
116 Show the commit log.
117
118 =item grep pattern
119
120 Searches for a pattern in each repository using the grep subcommand. Uses
121 ack-grep on VCS that do not have their own.
122
123 =item run command [param ...]
124
125 Runs the specified command in each repository.
126
127 =back
128
129 These commands are also available:
130
131 =over 4
132
133 =item bootstrap url [directory]
134
135 Causes mr to download the url, and use it as a .mrconfig file to checkout
136 the repositories listed in it, into the specified directory.
137
138 To use scp to download, the url may have the form ssh://[user@]host:file
139
140 The directory will be created if it does not exist. If no directory is
141 specified, the current directory will be used.
142
143 If the .mrconfig file includes a repository named ".", that
144 is checked out into the top of the specified directory.
145
146 =item list (or ls)
147
148 List the repositories that mr will act on.
149
150 =item register
151
152 Register an existing repository in a mrconfig file. By default, the
153 repository in the current directory is registered, or you can specify a
154 directory to register.
155
156 The mrconfig file that is modified is chosen by either the -c option, or by
157 looking for the closest known one at or in a parent of the current directory.
158
159 =item config
160
161 Adds, modifies, removes, or prints a value from a mrconfig file. The next
162 parameter is the name of the section the value is in. To add or modify
163 values, use one or more instances of "parameter=value". Use "parameter=" to
164 remove a parameter. Use just "parameter" to get the value of a parameter.
165
166 For example, to add (or edit) a repository in src/foo:
167
168   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
169
170 To show the command that mr uses to update the repository in src/foo:
171
172   mr config src/foo update
173
174 To see the built-in library of shell functions contained in mr:
175
176   mr config DEFAULT lib
177
178 The mrconfig file that is used is chosen by either the -c option, or by
179 looking for the closest known one at or in a parent of the current directory.
180
181 =item offline
182
183 Advises mr that it is in offline mode. Any commands that fail in
184 offline mode will be remembered, and retried when mr is told it's online.
185
186 =item online
187
188 Advices mr that it is in online mode again. Commands that failed while in
189 offline mode will be re-run.
190
191 =item remember
192
193 Remember a command, to be run later when mr re-enters online mode. This
194 implicitly puts mr into offline mode. The command can be any regular mr
195 command. This is useful when you know that a command will fail due to being
196 offline, and so don't want to run it right now at all, but just remember
197 to run it when you go back online.
198
199 =item help
200
201 Displays this help.
202
203 =back
204
205 Actions can be abbreviated to any unambiguous substring, so
206 "mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
207 update"
208
209 Additional parameters can be passed to most commands, and are passed on
210 unchanged to the underlying version control system. This is mostly useful
211 if the repositories mr will act on all use the same version control
212 system.
213
214 =head1 OPTIONS
215
216 =over 4
217
218 =item -d directory
219
220 =item --directory directory
221
222 Specifies the topmost directory that B<mr> should work in. The default is
223 the current working directory.
224
225 =item -c mrconfig
226
227 =item --config mrconfig
228
229 Use the specified mrconfig file. The default is to use both F<~/.mrconfig>
230 as well as look for a F<.mrconfig> file in the current directory, or in one
231 of its parent directories.
232
233 =item -f
234
235 =item --force
236
237 Force mr to act on repositories that would normally be skipped due to their
238 configuration.
239
240 =item -v
241
242 =item --verbose
243
244 Be verbose.
245
246 =item -q
247
248 =item --quiet
249
250 Be quiet. This suppresses mr's usual output, as well as any output from
251 commands that are run (including stderr output). If a command fails,
252 the output will be shown.
253
254 =item -k
255
256 =item --insecure
257
258 Accept untrusted SSL certificates when bootstrapping.
259
260 =item -s
261
262 =item --stats
263
264 Expand the statistics line displayed at the end to include information
265 about exactly which repositories failed and were skipped, if any.
266
267 =item -i
268
269 =item --interactive
270
271 Interactive mode. If a repository fails to be processed, a subshell will be
272 started which you can use to resolve or investigate the problem. Exit the
273 subshell to continue the mr run.
274
275 =item -n [number]
276
277 =item --no-recurse [number]
278
279 If no number if specified, just operate on the repository for the current
280 directory, do not recurse into deeper repositories.
281
282 If a number is specified, will recurse into repositories at most that many
283 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
284 but not ./src/packages/bar.
285
286 =item -j [number]
287
288 =item --jobs [number]
289
290 Run the specified number of jobs in parallel, or an unlimited number of jobs
291 with no number specified. This can greatly speed up operations such as updates.
292 It is not recommended for interactive operations.
293
294 Note that running more than 10 jobs at a time is likely to run afoul of
295 ssh connection limits. Running between 3 and 5 jobs at a time will yield
296 a good speedup in updates without loading the machine too much.
297
298 =item -t
299
300 =item --trust-all
301
302 Trust all mrconfig files even if they are not listed in F<~/.mrtrust>.
303 Use with caution.
304
305 =item -p
306
307 =item --path
308
309 This obsolete flag is ignored.
310
311 =back
312
313 =head1 MRCONFIG FILES
314
315 Here is an example F<.mrconfig> file:
316
317   [src]
318   checkout = svn checkout svn://svn.example.com/src/trunk src
319   chain = true
320
321   [src/linux-2.6]
322   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
323         cd linux-2.6 &&
324         git checkout -b mybranch origin/master
325
326 The F<.mrconfig> file uses a variant of the INI file format. Lines
327 starting with "#" are comments. Values can be continued to the
328 following line by indenting the line with whitespace.
329
330 The C<DEFAULT> section allows setting default values for the sections that
331 come after it.
332
333 The C<ALIAS> section allows adding aliases for actions. Each parameter
334 is an alias, and its value is the action to use.
335
336 All other sections add repositories. The section header specifies the
337 directory where the repository is located. This is relative to the directory
338 that contains the mrconfig file, but you can also choose to use absolute
339 paths. (Note that you can use environment variables in section names; they
340 will be passed through the shell for expansion. For example, 
341 C<[$HOSTNAME]>, or C<[${HOSTNAME}foo]>).
342
343 Within a section, each parameter defines a shell command to run to handle a
344 given action. mr contains default handlers for "update", "status",
345 "commit", and other standard actions.
346
347 Normally you only need to specify what to do for "checkout". Here you
348 specify the command to run in order to create a checkout of the repository.
349 The command will be run in the parent directory, and must create the
350 repository's directory. So use C<git clone>, C<svn checkout>, C<bzr branch>
351 or C<bzr checkout> (for a bound branch), etc.
352
353 Note that these shell commands are run in a C<set -e> shell
354 environment, where any additional parameters you pass are available in
355 C<$@>. All commands other than "checkout" are run inside the repository,
356 though not necessarily at the top of it.
357
358 The C<MR_REPO> environment variable is set to the path to the top of the
359 repository. (For the "register" action, "MR_REPO" is instead set to the 
360 basename of the directory that should be created when checking the
361 repository out.)
362
363 The C<MR_CONFIG> environment variable is set to the .mrconfig file
364 that defines the repo being acted on, or, if the repo is not yet in a config
365 file, the F<.mrconfig> file that should be modified to register the repo.
366
367 The C<MR_ACTION> environment variable is set to the command being run
368 (update, checkout, etc).
369
370 A few parameters have special meanings:
371
372 =over 4
373
374 =item skip
375
376 If the "skip" parameter is set and its command returns true, then B<mr>
377 will skip acting on that repository. The command is passed the action
378 name in C<$1>.
379
380 Here are two examples. The first skips the repo unless
381 mr is run by joey. The second uses the hours_since function
382 (included in mr's built-in library) to skip updating the repo unless it's
383 been at least 12 hours since the last update.
384
385   [mystuff]
386   checkout = ...
387   skip = test `whoami` != joey
388
389   [linux]
390   checkout = ...
391   skip = [ "$1" = update ] && ! hours_since "$1" 12
392  
393 Another way to use skip is for a lazy checkout. This makes mr skip
394 operating on a repo unless it already exists. To enable the 
395 repo, you have to explicitly check it out (using "mr --force -d foo checkout").
396
397   [foo]
398   checkout = ...
399   skip = lazy
400
401 =item order
402
403 The "order" parameter can be used to override the default ordering of
404 repositories. The default order value is 10. Use smaller values to make
405 repositories be processed earlier, and larger values to make repositories
406 be processed later.
407
408 Note that if a repository is located in a subdirectory of another
409 repository, ordering it to be processed earlier is not recommended.
410
411 =item chain
412
413 If the "chain" parameter is set and its command returns true, then B<mr>
414 will try to load a F<.mrconfig> file from the root of the repository.
415
416 =item include
417
418 If the "include" parameter is set, its command is ran, and should output
419 additional mrconfig file content. The content is included as if it were
420 part of the including file.
421
422 Unlike all other parameters, this parameter does not need to be placed
423 within a section.
424
425 B<mr> ships several libraries that can be included to add support for
426 additional version control type things (unison, git-svn, git-fake-bare,
427 git-subtree). To include them all, you could use:
428
429   include = cat /usr/share/mr/*
430
431 See the individual files for details.
432
433 =item deleted
434
435 If the "deleted" parameter is set and its command returns true, then
436 B<mr> will treat the repository as deleted. It won't ever actually delete
437 the repository, but it will warn if it sees the repository's directory.
438 This is useful when one mrconfig file is shared among multiple machines,
439 to keep track of and remember to delete old repositories.
440
441 =item lib
442
443 The "lib" parameter can specify some shell code that will be run
444 before each command, this can be a useful way to define shell
445 functions for other commands to use. 
446
447 Unlike most other parameters, this can be specified multiple times, in
448 which case the chunks of shell code are accumulatively concatenated
449 together.
450
451 =item fixups
452
453 If the "fixups" parameter is set, its command is run whenever a repository
454 is checked out, or updated. This provides an easy way to do things
455 like permissions fixups, or other tweaks to the repository content,
456 whenever the repository is changed.
457
458 =item VCS_action
459
460 When looking for a command to run for a given action, mr first looks for
461 a parameter with the same name as the action. If that is not found, it
462 looks for a parameter named "VCS_action" (substituting in the name of the
463 version control system and the action).
464
465 Internally, mr has settings for "git_update", "svn_update", etc. To change
466 the action that is performed for a given version control system, you can
467 override these VCS specific actions. To add a new version control system,
468 you can just add VCS specific actions for it.
469
470 =item pre_ and post_
471
472 If a "pre_action" parameter is set, its command is run before mr performs the
473 specified action. Similarly, "post_action" parameters are run after mr
474 successfully performs the specified action. For example, "pre_commit" is
475 run before committing; "post_update" is run after updating.
476
477 =item _append
478
479 Any parameter can be suffixed with C<_append>, to add an additional value
480 to the existing value of the parameter. In this way, actions 
481 can be constructed accumulatively.
482
483 =item VCS_test
484
485 The name of the version control system is itself determined by
486 running each defined "VCS_test" action, until one succeeds.
487
488 =back
489
490 =head1 UNTRUSTED MRCONFIG FILES
491
492 Since mrconfig files can contain arbitrary shell commands, they can do
493 anything. This flexibility is good, but it also allows a malicious mrconfig
494 file to delete your whole home directory. Such a file might be contained
495 inside a repository that your main F<~/.mrconfig> checks out. To
496 avoid worries about evil commands in a mrconfig file, mr defaults to
497 reading all mrconfig files other than the main F<~/.mrconfig> in untrusted
498 mode. In untrusted mode, mrconfig files are limited to running only known
499 safe commands (like "git clone") in a carefully checked manner.
500
501 To configure mr to trust other mrconfig files, list them in F<~/.mrtrust>.
502 One mrconfig file should be listed per line. Either the full pathname
503 should be listed, or the pathname can start with F<~/> to specify a file
504 relative to your home directory.
505
506 =head1 OFFLINE LOG FILE
507
508 The F<~/.mrlog> file contains commands that mr has remembered to run later,
509 due to being offline. You can delete or edit this file to remove commands,
510 or even to add other commands for 'mr online' to run. If the file is
511 present, mr assumes it is in offline mode.
512
513 =head1 EXTENSIONS
514
515 mr can be extended to support things such as unison and git-svn. Some
516 files providing such extensions are available in F</usr/share/mr/>. See
517 the documentation in the files for details about using them.
518
519 =head1 EXIT STATUS
520
521 mr returns nonzero if a command failed in any of the repositories.
522
523 =head1 AUTHOR
524
525 Copyright 2007-2011 Joey Hess <joey@kitenet.net>
526
527 Licensed under the GNU GPL version 2 or higher.
528
529 http://myrepos.branchable.com/
530
531 =cut
532
533 use warnings;
534 use strict;
535 use Getopt::Long;
536 use Cwd qw(getcwd abs_path);
537
538 # things that can happen when mr runs a command
539 use constant {
540         OK => 0,
541         FAILED => 1,
542         SKIPPED => 2,
543         ABORT => 3,
544 };
545
546 # configurables
547 my $config_overridden=0;
548 my $verbose=0;
549 my $quiet=0;
550 my $stats=0;
551 my $force=0;
552 my $insecure=0;
553 my $interactive=0;
554 my $max_depth;
555 my $no_chdir=0;
556 my $jobs=1;
557 my $trust_all=0;
558 my $directory=getcwd();
559
560 my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
561 $ENV{MR_CONFIG}=find_mrconfig();
562
563 # globals :-(
564 my %config;
565 my %configfiles;
566 my %knownactions;
567 my %alias;
568 my (@ok, @failed, @skipped);
569
570 main();
571
572 sub shellquote {
573         my $i=shift;
574         $i=~s/'/'"'"'/g;
575         return "'$i'";
576 }
577
578 # Runs a shell command using a supplied function.
579 # The lib will be included in the shell command line, and any params
580 # will be available in the shell as $1, $2, etc.
581 my $lastlib;
582 sub runsh {
583         my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
584
585         # optimisation: avoid running the shell for true and false
586         if ($command =~ /^\s*true\s*$/) {
587                 $?=0;
588                 return 0;
589         }
590         elsif ($command =~ /^\s*false\s*$/) {
591                 $?=0;
592                 return 1;
593         }
594         
595         my $quotedparams=join(" ", (map { shellquote($_) } @$params));
596         my $lib=exists $config{$topdir}{$subdir}{lib} ?
597                        $config{$topdir}{$subdir}{lib}."\n" : "";
598         if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
599                 print "mr library now: >>$lib<<\n";
600                 $lastlib=$lib;
601         }
602         my $shellcode="set -e;".$lib.
603                 "my_sh(){ $command\n }; my_sh $quotedparams";
604         print "mr $action: running $action >>$command<<\n" if $verbose;
605         $runner->($shellcode);
606 }
607
608 my %perl_cache;
609 sub perl {
610         my $id=shift;
611         my $s=shift;
612         if ($s =~ m/^perl:\s+(.*)/s) {
613                 return $perl_cache{$1} if exists $perl_cache{$1};
614                 my $sub=eval "sub {$1}";
615                 if (! defined $sub) {
616                         print STDERR "mr: bad perl code in $id: $@\n";
617                 }
618                 return $perl_cache{$1} = $sub;
619         }
620         return undef;
621 }
622
623 my %vcs;
624 sub vcs_test {
625         my ($action, $dir, $topdir, $subdir) = @_;
626
627         if (exists $vcs{$dir}) {
628                 return $vcs{$dir};
629         }
630
631         my $test="";
632         my %perltest;
633         foreach my $vcs_test (
634                         sort {
635                                 length $a <=> length $b 
636                                           ||
637                                        $a cmp $b
638                         } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
639                 my ($vcs)=$vcs_test =~ /(.*)_test/;
640                 my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
641                 if (defined $p) {
642                         $perltest{$vcs}=$p;
643                 }
644                 else {
645                         $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
646                         $test.="if my_$vcs_test; then echo $vcs; fi\n";
647                 }
648         }
649
650         my @vcs;
651         foreach my $vcs (keys %perltest) {
652                 if ($perltest{$vcs}->()) {
653                         push @vcs, $vcs;
654                 }
655         }
656
657         push @vcs, split(/\n/,
658                 runsh("vcs test", $topdir, $subdir, $test, [], sub {
659                         my $sh=shift;
660                         my $ret=`$sh`;
661                         return $ret;
662                 })) if length $test;
663         if (@vcs > 1) {
664                 print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
665                 return undef;
666         }
667         if (! @vcs) {
668                 return $vcs{$dir}=undef;
669         }
670         else {
671                 return $vcs{$dir}=$vcs[0];
672         }
673 }
674         
675 sub findcommand {
676         my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
677         
678         if (exists $config{$topdir}{$subdir}{$action}) {
679                 return $config{$topdir}{$subdir}{$action};
680         }
681
682         if ($is_checkout) {
683                 return undef;
684         }
685
686         my $vcs=vcs_test(@_);
687
688         if (defined $vcs && 
689             exists $config{$topdir}{$subdir}{$vcs."_".$action}) {
690                 return $config{$topdir}{$subdir}{$vcs."_".$action};
691         }
692         else {
693                 return undef;
694         }
695 }
696
697 sub fulldir {
698         my ($topdir, $subdir) = @_;
699         return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
700 }
701
702 sub action {
703         my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
704         my $fulldir=fulldir($topdir, $subdir);
705         my $checkout_dir;
706
707         $ENV{MR_CONFIG}=$configfiles{$topdir};
708         my $is_checkout=($action eq 'checkout');
709         my $is_update=($action =~ /update/);
710
711         ($ENV{MR_REPO}=$dir) =~ s!/$!!;
712         $ENV{MR_ACTION}=$action;
713         
714         foreach my $testname ("skip", "deleted") {
715                 next if $force && $testname eq "skip";
716
717                 my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
718
719                 if (defined $testcommand) {
720                         my $ret=runsh "$testname test", $topdir, $subdir,
721                                 $testcommand, [$action],
722                                 sub { system(shift()) };
723                         if ($ret != 0) {
724                                 if (($? & 127) == 2) {
725                                         print STDERR "mr $action: interrupted\n";
726                                         return ABORT;
727                                 }
728                                 elsif ($? & 127) {
729                                         print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
730                                         return ABORT;
731                                 }
732                         }
733                         if ($ret >> 8 == 0) {
734                                 if ($testname eq "deleted") {
735                                         if (-d $dir) {
736                                                 print STDERR "mr error: $dir should be deleted yet still exists\n";
737                                                 return FAILED;
738                                         }
739                                 }
740                                 print "mr $action: skip $dir skipped\n" if $verbose;
741                                 return SKIPPED;
742                         }
743                 }
744         }
745
746         if ($is_checkout) {
747                 $checkout_dir=$dir;
748                 if (! $force_checkout) {
749                         if (-d $dir) {
750                                 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
751                                 return SKIPPED;
752                         }
753         
754                         $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
755                 }
756         }
757         elsif ($is_update) {
758                 if (! -d $dir) {
759                         return action("checkout", $dir, $topdir, $subdir);
760                 }
761         }
762
763         my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
764
765         if ($is_checkout && ! -d $dir) {
766                 print "mr $action: creating parent directory $dir\n" if $verbose;
767                 system("mkdir", "-p", $dir);
768         }
769
770         if (! $no_chdir && ! chdir($dir)) {
771                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
772                 return FAILED;
773         }
774         elsif (! defined $command) {
775                 my $vcs=vcs_test(@_);
776                 if (! defined $vcs) {
777                         print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
778                         return FAILED;
779                 }
780                 else {
781                         print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
782                         return SKIPPED;
783                 }
784         }
785         else {
786                 my $actionmsg;
787                 if (! $no_chdir) {
788                         $actionmsg="mr $action: $fulldir";
789                 }
790                 else {
791                         my $s=$directory;
792                         $s=~s/^\Q$fulldir\E\/?//;
793                         $actionmsg="mr $action: $fulldir (in subdir $s)";
794                 }
795                 print "$actionmsg\n" unless $quiet;
796
797                 my $hookret=hook("pre_$action", $topdir, $subdir);
798                 return $hookret if $hookret != OK;
799
800                 my $ret=runsh $action, $topdir, $subdir,
801                         $command, \@ARGV, sub {
802                                 my $sh=shift;
803                                 if ($quiet) {
804                                         my $output = qx/$sh 2>&1/;
805                                         my $ret = $?;
806                                         if ($ret != 0) {
807                                                 print "$actionmsg\n";
808                                                 print STDERR $output;
809                                         }
810                                         return $ret;
811                                 }
812                                 else {
813                                         system($sh);
814                                 }
815                         };
816                 if ($ret != 0) {
817                         if (($? & 127) == 2) {
818                                 print STDERR "mr $action: interrupted\n";
819                                 return ABORT;
820                         }
821                         elsif ($? & 127) {
822                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
823                                 return ABORT;
824                         }
825                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
826                         if ($ret >> 8 != 0) {
827                                 print STDERR "mr $action: command failed\n";
828                                 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
829                                         # recreate original command line to
830                                         # remember, and avoid recursing
831                                         my @orig=@ARGV;
832                                         @ARGV=('-n', $action, @orig);
833                                         action("remember", $dir, $topdir, $subdir);
834                                         @ARGV=@orig;
835                                 }
836                         }
837                         elsif ($ret != 0) {
838                                 print STDERR "mr $action: command died ($ret)\n";
839                         }
840                         return FAILED;
841                 }
842                 else {
843                         if ($is_checkout && ! -d $dir) {
844                                 print STDERR "mr $action: $dir missing after checkout\n";;
845                                 return FAILED;
846                         }
847
848                         my $ret=hook("post_$action", $topdir, $subdir);
849                         return $ret if $ret != OK;
850                         
851                         if ($is_checkout || $is_update) {
852                                 if ($is_checkout && ! $no_chdir) {
853                                         if (! chdir($checkout_dir)) {
854                                                 print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
855                                                 return FAILED;
856                                         }
857                                 }
858                                 my $ret=hook("fixups", $topdir, $subdir);
859                                 return $ret if $ret != OK;
860                         }
861                         
862                         return OK;
863                 }
864         }
865 }
866
867 sub hook {
868         my ($hook, $topdir, $subdir) = @_;
869
870         my $command=$config{$topdir}{$subdir}{$hook};
871         return OK unless defined $command;
872         my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
873                         my $sh=shift;
874                         if ($quiet) {
875                                 my $output = qx/$sh 2>&1/;
876                                 my $ret = $?;
877                                 if ($ret != 0) {
878                                         print STDERR $output;
879                                 }
880                                 return $ret;
881                         }
882                         else {
883                                 system($sh);
884                         }
885                 };
886         if ($ret != 0) {
887                 if (($? & 127) == 2) {
888                         print STDERR "mr $hook: interrupted\n";
889                         return ABORT;
890                 }
891                 elsif ($? & 127) {
892                         print STDERR "mr $hook: received signal ".($? & 127)."\n";
893                         return ABORT;
894                 }
895                 else {
896                         return FAILED;
897                 }
898         }
899
900         return OK;
901 }
902
903 # run actions on multiple repos, in parallel
904 sub mrs {
905         my $action=shift;
906         my @repos=@_;
907
908         $| = 1;
909         my @active;
910         my @fhs;
911         my @out;
912         my $running=0;
913         while (@fhs or @repos) {
914                 while ((!$jobs || $running < $jobs) && @repos) {
915                         $running++;
916                         my $repo = shift @repos;
917                         pipe(my $outfh, CHILD_STDOUT);
918                         pipe(my $errfh, CHILD_STDERR);
919                         my $pid;
920                         unless ($pid = fork) {
921                                 die "mr $action: cannot fork: $!" unless defined $pid;
922                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
923                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
924                                 close CHILD_STDOUT;
925                                 close CHILD_STDERR;
926                                 close $outfh;
927                                 close $errfh;
928                                 exit action($action, @$repo);
929                         }
930                         close CHILD_STDOUT;
931                         close CHILD_STDERR;
932                         push @active, [$pid, $repo];
933                         push @fhs, [$outfh, $errfh];
934                         push @out, ['',     ''];
935                 }
936                 my ($rin, $rout) = ('','');
937                 my $nfound;
938                 foreach my $fh (@fhs) {
939                         next unless defined $fh;
940                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
941                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
942                 }
943                 $nfound = select($rout=$rin, undef, undef, 1);
944                 foreach my $channel (0, 1) {
945                         foreach my $i (0..$#fhs) {
946                                 next unless defined $fhs[$i];
947                                 my $fh = $fhs[$i][$channel];
948                                 next unless defined $fh;
949                                 if (vec($rout, fileno($fh), 1) == 1) {
950                                         my $r = '';
951                                         if (sysread($fh, $r, 1024) == 0) {
952                                                 close($fh);
953                                                 $fhs[$i][$channel] = undef;
954                                                 if (! defined $fhs[$i][0] &&
955                                                     ! defined $fhs[$i][1]) {
956                                                         waitpid($active[$i][0], 0);
957                                                         print STDOUT $out[$i][0];
958                                                         print STDERR $out[$i][1];
959                                                         record($active[$i][1], $? >> 8);
960                                                         splice(@fhs, $i, 1);
961                                                         splice(@active, $i, 1);
962                                                         splice(@out, $i, 1);
963                                                         $running--;
964                                                 }
965                                         }
966                                         $out[$i][$channel] .= $r;
967                                 }
968                         }
969                 }
970         }
971 }
972
973 sub record {
974         my $dir=shift()->[0];
975         my $ret=shift;
976
977         if ($ret == OK) {
978                 push @ok, $dir;
979                 print "\n" unless $quiet;
980         }
981         elsif ($ret == FAILED) {
982                 if ($interactive) {
983                         chdir($dir) unless $no_chdir;
984                         print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
985                         system((getpwuid($<))[8], "-i");
986                 }
987                 push @failed, $dir;
988                 print "\n" unless $quiet;
989         }
990         elsif ($ret == SKIPPED) {
991                 push @skipped, $dir;
992         }
993         elsif ($ret == ABORT) {
994                 exit 1;
995         }
996         else {
997                 die "unknown exit status $ret";
998         }
999 }
1000
1001 sub showstats {
1002         my $action=shift;
1003         if (! @ok && ! @failed && ! @skipped) {
1004                 die "mr $action: no repositories found to work on\n";
1005         }
1006         print "mr $action: finished (".join("; ",
1007                 showstat($#ok+1, "ok", "ok"),
1008                 showstat($#failed+1, "failed", "failed"),
1009                 showstat($#skipped+1, "skipped", "skipped"),
1010         ).")\n" unless $quiet;
1011         if ($stats) {
1012                 if (@skipped) {
1013                         print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
1014                 }
1015                 if (@failed) {
1016                         print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
1017                 }
1018         }
1019 }
1020
1021 sub showstat {
1022         my $count=shift;
1023         my $singular=shift;
1024         my $plural=shift;
1025         if ($count) {
1026                 return "$count ".($count > 1 ? $plural : $singular);
1027         }
1028         return;
1029 }
1030
1031 # an ordered list of repos
1032 sub repolist {
1033         my @list;
1034         foreach my $topdir (sort keys %config) {
1035                 foreach my $subdir (sort keys %{$config{$topdir}}) {
1036                         push @list, {
1037                                 topdir => $topdir,
1038                                 subdir => $subdir,
1039                                 order => $config{$topdir}{$subdir}{order},
1040                         };
1041                 }
1042         }
1043         return sort {
1044                 $a->{order}  <=> $b->{order}
1045                              ||
1046                 $a->{topdir} cmp $b->{topdir}
1047                              ||
1048                 $a->{subdir} cmp $b->{subdir}
1049         } @list;
1050 }
1051
1052 sub repodir {
1053         my $repo=shift;
1054         my $topdir=$repo->{topdir};
1055         my $subdir=$repo->{subdir};
1056         my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
1057         $ret=~s/\/\.$//;
1058         return $ret;
1059 }
1060
1061 # Figure out which repos to act on.  Returns a list of array refs
1062 # in the format:
1063 #
1064 #   [ "$full_repo_path/", "$mr_config_path/", $section_header ]
1065 sub selectrepos {
1066         my @repos;
1067         foreach my $repo (repolist()) {
1068                 my $topdir=$repo->{topdir};
1069                 my $subdir=$repo->{subdir};
1070
1071                 next if $subdir eq 'DEFAULT';
1072                 my $dir=repodir($repo);
1073                 my $d=$directory;
1074                 $dir.="/" unless $dir=~/\/$/;
1075                 $d.="/" unless $d=~/\/$/;
1076                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
1077                 if (defined $max_depth) {
1078                         my @a=split('/', $dir);
1079                         my @b=split('/', $d);
1080                         do { } while (@a && @b && shift(@a) eq shift(@b));
1081                         next if @a > $max_depth || @b > $max_depth;
1082                 }
1083                 push @repos, [$dir, $topdir, $subdir];
1084         }
1085         if (! @repos) {
1086                 # fallback to find a leaf repo
1087                 foreach my $repo (reverse repolist()) {
1088                         my $topdir=$repo->{topdir};
1089                         my $subdir=$repo->{subdir};
1090                         
1091                         next if $subdir eq 'DEFAULT';
1092                         my $dir=repodir($repo);
1093                         my $d=$directory;
1094                         $dir.="/" unless $dir=~/\/$/;
1095                         $d.="/" unless $d=~/\/$/;
1096                         if ($d=~/^\Q$dir\E/) {
1097                                 push @repos, [$dir, $topdir, $subdir];
1098                                 last;
1099                         }
1100                 }
1101                 $no_chdir=1;
1102         }
1103         return @repos;
1104 }
1105
1106 sub expandenv {
1107         my $val=shift;
1108         
1109
1110         if ($val=~/\$/) {
1111                 $val=`echo "$val"`;
1112                 chomp $val;
1113         }
1114         
1115         return $val;
1116 }
1117
1118 my %trusted;
1119 sub is_trusted_config {
1120         my $config=shift; # must be abs_pathed already
1121
1122         # We always trust ~/.mrconfig.
1123         return 1 if $config eq abs_path($HOME_MR_CONFIG);
1124
1125         return 1 if $trust_all;
1126
1127         my $trustfile=$ENV{HOME}."/.mrtrust";
1128
1129         if (! %trusted) {
1130                 $trusted{$HOME_MR_CONFIG}=1;
1131                 if (open (TRUST, "<", $trustfile)) {
1132                         while (<TRUST>) {
1133                                 chomp;
1134                                 s/^~\//$ENV{HOME}\//;
1135                                 $trusted{abs_path($_)}=1;
1136                         }
1137                         close TRUST;
1138                 }
1139         }
1140
1141         return $trusted{$config};
1142 }
1143
1144
1145 sub is_trusted_repo {
1146         my $repo=shift;
1147         
1148         # Tightly limit what is allowed in a repo name.
1149         # No ../, no absolute paths, and no unusual filenames
1150         # that might try to escape to the shell.
1151         return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
1152                $repo !~ /\.\./ && $repo !~ /^\//;
1153 }
1154
1155 sub is_trusted_checkout {
1156         my $command=shift;
1157         
1158         # To determine if the command is safe, compare it with the
1159         # *_trusted_checkout config settings. Those settings are
1160         # templates for allowed commands, so make sure that each word
1161         # of the command matches the corresponding word of the template.
1162         
1163         my @words;
1164         foreach my $word (split(' ', $command)) {
1165                 # strip quoting
1166                 if ($word=~/^'(.*)'$/) {
1167                         $word=$1;
1168                 }
1169                 elsif ($word=~/^"(.*)"$/) {
1170                         $word=$1;
1171                 }
1172
1173                 push @words, $word;
1174         }
1175
1176         foreach my $key (grep { /_trusted_checkout$/ }
1177                          keys %{$config{''}{DEFAULT}}) {
1178                 my @twords=split(' ', $config{''}{DEFAULT}{$key});
1179                 next if @words > @twords;
1180
1181                 my $match=1;
1182                 my $url;
1183                 for (my $c=0; $c < @twords && $match; $c++) {
1184                         if ($twords[$c] eq '$url') {
1185                                 # Match all the typical characters found in
1186                                 # urls, plus @ which svn can use. Note
1187                                 # that the "url" might also be a local
1188                                 # directory.
1189                                 $match=(
1190                                         defined $words[$c] &&
1191                                         $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
1192                                 );
1193                                 $url=$words[$c];
1194                         }
1195                         elsif ($twords[$c] eq '$repo') {
1196                                 # If a repo is not specified, assume it
1197                                 # will be the last path component of the
1198                                 # url, or something derived from it, and
1199                                 # check that.
1200                                 if (! defined $words[$c] && defined $url) {
1201                                         ($words[$c])=$url=~/\/([^\/]+)\/?$/;
1202                                 }
1203
1204                                 $match=(
1205                                         defined $words[$c] &&
1206                                         is_trusted_repo($words[$c])
1207                                 );
1208                         }
1209                         elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
1210                                 $match=1;
1211                         }
1212                         else {
1213                                 $match=0;
1214                         }
1215                 }
1216                 return 1 if $match;
1217         }
1218
1219         return 0;
1220 }
1221
1222 my %loaded;
1223 sub loadconfig {
1224         my $f=shift;
1225         my $dir=shift;
1226         my $bootstrap_url=shift;
1227
1228         my @toload;
1229
1230         my $in;
1231         my $trusted;
1232         if (ref $f eq 'GLOB') {
1233                 $dir="";
1234                 $in=$f;
1235                 $trusted=1;
1236         }
1237         else {
1238                 my $absf=abs_path($f);
1239                 if ($loaded{$absf}) {
1240                         return;
1241                 }
1242                 $loaded{$absf}=1;
1243
1244                 $trusted=is_trusted_config($absf);
1245
1246                 if (! defined $dir) {
1247                         ($dir)=$f=~/^(.*\/)[^\/]+$/;
1248                         if (! defined $dir) {
1249                                 $dir=".";
1250                         }
1251                 }
1252
1253                 $dir=abs_path($dir)."/";
1254                 
1255                 if (! exists $configfiles{$dir}) {
1256                         $configfiles{$dir}=$f;
1257                 }
1258
1259                 # copy in defaults from first parent
1260                 my $parent=$dir;
1261                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
1262                         if ($parent eq '/') {
1263                                 $parent="";
1264                         }
1265                         if (exists $config{$parent} &&
1266                             exists $config{$parent}{DEFAULT}) {
1267                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
1268                                 last;
1269                         }
1270                 }
1271                 
1272                 if (! -e $f) {
1273                         return;
1274                 }
1275
1276                 print "mr: loading config $f\n" if $verbose;
1277                 open($in, "<", $f) || die "mr: open $f: $!\n";
1278         }
1279         my @lines=<$in>;
1280         close $in unless ref $f eq 'GLOB';
1281
1282         my $section;
1283
1284         # Keep track of the current line in the config file;
1285         # when a file is included track the current line from the include.
1286         my $lineno=0;
1287         my $included=undef;
1288
1289         my $line;
1290         my $nextline = sub {
1291                 if ($included) {
1292                         $included--;
1293                 }
1294                 else {
1295                         $included=undef;
1296                         $lineno++;
1297                 }
1298                 $line=shift @lines;
1299                 chomp $line;
1300                 return $line;
1301         };
1302         my $lineerror = sub {
1303                 my $msg=shift;
1304                 if (defined $included) {
1305                         die "mr: $msg at $f line $lineno, included line: $line\n";
1306                 }
1307                 else {
1308                         die "mr: $msg at $f line $lineno\n";
1309                 }
1310         };
1311         my $trusterror = sub {
1312                 my $msg=shift;
1313         
1314                 if (defined $bootstrap_url) {
1315                         die "mr: $msg in untrusted $bootstrap_url line $lineno\n".
1316                                 "(To trust this url, --trust-all can be used; but please use caution;\n".
1317                                 "this can allow arbitrary code execution!)\n";
1318                 }
1319                 else {
1320                         die "mr: $msg in untrusted $f line $lineno\n".
1321                                 "(To trust this file, list it in ~/.mrtrust.)\n";
1322                 }
1323         };
1324
1325         while (@lines) {
1326                 $_=$nextline->();
1327
1328                 next if /^\s*\#/ || /^\s*$/;
1329
1330                 if (! $trusted && /[[:cntrl:]]/) {
1331                         $trusterror->("illegal control character");
1332                 }
1333
1334                 if (/^\[([^\]]*)\]\s*$/) {
1335                         $section=$1;
1336
1337                         if (! $trusted) {
1338                                 if (! is_trusted_repo($section) ||
1339                                     $section eq 'ALIAS' ||
1340                                     $section eq 'DEFAULT') {
1341                                         $trusterror->("illegal section \"[$section]\"");
1342                                 }
1343                         }
1344                         $section=expandenv($section) if $trusted;
1345                         if ($section ne 'ALIAS' &&
1346                             ! exists $config{$dir}{$section} &&
1347                             exists $config{$dir}{DEFAULT}) {
1348                                 # copy in defaults
1349                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1350                         }
1351                 }
1352                 elsif (/^(\w+)\s*=\s*(.*)/) {
1353                         my $parameter=$1;
1354                         my $value=$2;
1355
1356                         # continued value
1357                         while (@lines && $lines[0]=~/^\s(.+)/) {
1358                                 $value.="\n$1";
1359                                 chomp $value;
1360                                 $nextline->();
1361                         }
1362
1363                         if (! $trusted) {
1364                                 # Untrusted files can only contain a few
1365                                 # settings in specific known-safe formats.
1366                                 if ($parameter eq 'checkout') {
1367                                         if (! is_trusted_checkout($value)) {
1368                                                 $trusterror->("illegal checkout command \"$value\"");
1369                                         }
1370                                 }
1371                                 elsif ($parameter eq 'order') {
1372                                         # not interpreted as a command, so
1373                                         # safe.
1374                                 }
1375                                 elsif ($value eq 'true' || $value eq 'false') {
1376                                         # skip=true , deleted=true etc are
1377                                         # safe.
1378                                 }
1379                                 else {
1380                                         $trusterror->("illegal setting \"$parameter=$value\"");
1381                                 }
1382                         }
1383
1384                         if ($parameter eq "include") {
1385                                 print "mr: including output of \"$value\"\n" if $verbose;
1386                                 my @inc=`$value`;
1387                                 if ($?) {
1388                                         print STDERR "mr: include command exited nonzero ($?)\n";
1389                                 }
1390                                 $included += @inc;
1391                                 unshift @lines, @inc;
1392                                 next;
1393                         }
1394
1395                         if (! defined $section) {
1396                                 $lineerror->("parameter ($parameter) not in section");
1397                         }
1398                         if ($section eq 'ALIAS') {
1399                                 $alias{$parameter}=$value;
1400                         }
1401                         elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
1402                                 $config{$dir}{$section}{$parameter}.="\n".$value."\n";
1403                         }
1404                         else {
1405                                 $config{$dir}{$section}{$parameter}=$value;
1406                                 if ($parameter =~ /.*_(.*)/) {
1407                                         $knownactions{$1}=1;
1408                                 }
1409                                 else {
1410                                         $knownactions{$parameter}=1;
1411                                 }
1412                                 if ($parameter eq 'chain' &&
1413                                     length $dir && $section ne "DEFAULT") {
1414                                         my $chaindir="$section";
1415                                         if ($chaindir !~ m!^/!) {
1416                                                 $chaindir=$dir.$chaindir;
1417                                         }
1418                                         if (-e "$chaindir/.mrconfig") {
1419                                                 my $ret=system($value);
1420                                                 if ($ret != 0) {
1421                                                         if (($? & 127) == 2) {
1422                                                                 print STDERR "mr: chain test interrupted\n";
1423                                                                 exit 2;
1424                                                         }
1425                                                         elsif ($? & 127) {
1426                                                                 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1427                                                         }
1428                                                 }
1429                                                 else {
1430                                                         push @toload, ["$chaindir/.mrconfig", $chaindir];
1431                                                 }
1432                                         }
1433                                 }
1434                         }
1435                 }
1436                 else {
1437                         $lineerror->("parse error");
1438                 }
1439         }
1440
1441         foreach my $c (@toload) {
1442                 loadconfig(@$c);
1443         }
1444 }
1445
1446 sub startingconfig {
1447         %alias=%config=%configfiles=%knownactions=%loaded=();
1448         my $datapos=tell(DATA);
1449         loadconfig(\*DATA);
1450         seek(DATA,$datapos,0); # rewind
1451 }
1452
1453 sub modifyconfig {
1454         my $f=shift;
1455         # the section to modify or add
1456         my $targetsection=shift;
1457         # fields to change in the section
1458         # To remove a field, set its value to "".
1459         my %changefields=@_;
1460
1461         my @lines;
1462         my @out;
1463
1464         if (-e $f) {
1465                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1466                 @lines=<$in>;
1467                 close $in;
1468         }
1469
1470         my $formatfield=sub {
1471                 my $field=shift;
1472                 my @value=split(/\n/, shift);
1473
1474                 return "$field = ".shift(@value)."\n".
1475                         join("", map { "\t$_\n" } @value);
1476         };
1477         my $addfields=sub {
1478                 my @blanks;
1479                 while ($out[$#out] =~ /^\s*$/) {
1480                         unshift @blanks, pop @out;
1481                 }
1482                 foreach my $field (sort keys %changefields) {
1483                         if (length $changefields{$field}) {
1484                                 push @out, "$field = $changefields{$field}\n";
1485                                 delete $changefields{$field};
1486                         }
1487                 }
1488                 push @out, @blanks;
1489         };
1490
1491         my $section;
1492         while (@lines) {
1493                 $_=shift(@lines);
1494
1495                 if (/^\s*\#/ || /^\s*$/) {
1496                         push @out, $_;
1497                 }
1498                 elsif (/^\[([^\]]*)\]\s*$/) {
1499                         if (defined $section && 
1500                             $section eq $targetsection) {
1501                                 $addfields->();
1502                         }
1503
1504                         $section=expandenv($1);
1505
1506                         push @out, $_;
1507                 }
1508                 elsif (/^(\w+)\s*=\s(.*)/) {
1509                         my $parameter=$1;
1510                         my $value=$2;
1511
1512                         # continued value
1513                         while (@lines && $lines[0]=~/^\s(.+)/) {
1514                                 shift(@lines);
1515                                 $value.="\n$1";
1516                                 chomp $value;
1517                         }
1518
1519                         if ($section eq $targetsection) {
1520                                 if (exists $changefields{$parameter}) {
1521                                         if (length $changefields{$parameter}) {
1522                                                 $value=$changefields{$parameter};
1523                                         }
1524                                         delete $changefields{$parameter};
1525                                 }
1526                         }
1527
1528                         push @out, $formatfield->($parameter, $value);
1529                 }
1530         }
1531
1532         if (defined $section && 
1533             $section eq $targetsection) {
1534                 $addfields->();
1535         }
1536         elsif (%changefields) {
1537                 push @out, "\n[$targetsection]\n";
1538                 foreach my $field (sort keys %changefields) {
1539                         if (length $changefields{$field}) {
1540                                 push @out, $formatfield->($field, $changefields{$field});
1541                         }
1542                 }
1543         }
1544
1545         open(my $out, ">", $f) || die "mr: write $f: $!\n";
1546         print $out @out;
1547         close $out;     
1548 }
1549
1550 sub dispatch {
1551         my $action=shift;
1552
1553         # actions that do not operate on all repos
1554         if ($action eq 'help') {
1555                 help(@ARGV);
1556         }
1557         elsif ($action eq 'config') {
1558                 config(@ARGV);
1559         }
1560         elsif ($action eq 'register') {
1561                 register(@ARGV);
1562         }
1563         elsif ($action eq 'bootstrap') {
1564                 bootstrap();
1565         }
1566         elsif ($action eq 'remember' ||
1567                $action eq 'offline' ||
1568                $action eq 'online') {
1569                 my @repos=selectrepos;
1570                 action($action, @{$repos[0]}) if @repos;
1571                 exit 0;
1572         }
1573
1574         if (!$jobs || $jobs > 1) {
1575                 mrs($action, selectrepos());
1576         }
1577         else {
1578                 foreach my $repo (selectrepos()) {
1579                         record($repo, action($action, @$repo));
1580                 }
1581         }
1582 }
1583
1584 sub help {
1585         exec($config{''}{DEFAULT}{help}) || die "exec: $!";
1586 }
1587
1588 sub config {
1589         if (@_ < 2) {
1590                 die "mr config: not enough parameters\n";
1591         }
1592         my $section=shift;
1593         if ($section=~/^\//) {
1594                 # try to convert to a path relative to the config file
1595                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1596                 $dir=abs_path($dir);
1597                 $dir.="/" unless $dir=~/\/$/;
1598                 if ($section=~/^\Q$dir\E(.*)/) {
1599                         $section=$1;
1600                 }
1601         }
1602         my %changefields;
1603         foreach (@_) {
1604                 if (/^([^=]+)=(.*)$/) {
1605                         $changefields{$1}=$2;
1606                 }
1607                 else {
1608                         my $found=0;
1609                         foreach my $topdir (sort keys %config) {
1610                                 if (exists $config{$topdir}{$section} &&
1611                                     exists $config{$topdir}{$section}{$_}) {
1612                                         print $config{$topdir}{$section}{$_}."\n";
1613                                         $found=1;
1614                                         last if $section eq 'DEFAULT';
1615                                 }
1616                         }
1617                         if (! $found) {
1618                                 die "mr config: $section $_ not set\n";
1619                         }
1620                 }
1621         }
1622         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1623         exit 0;
1624 }
1625
1626 sub register {
1627         if ($config_overridden) {
1628                 # Find the directory that the specified config file is
1629                 # located in.
1630                 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1631         }
1632         else {
1633                 # Find the closest known mrconfig file to the current
1634                 # directory.
1635                 $directory.="/" unless $directory=~/\/$/;
1636                 my $foundconfig=0;
1637                 foreach my $topdir (reverse sort keys %config) {
1638                         next unless length $topdir;
1639                         if ($directory=~/^\Q$topdir\E/) {
1640                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
1641                                 $directory=$topdir;
1642                                 $foundconfig=1;
1643                                 last;
1644                         }
1645                 }
1646                 if (! $foundconfig) {
1647                         $directory=""; # no config file, use builtin
1648                 }
1649         }
1650         if (@ARGV) {
1651                 my $subdir=shift @ARGV;
1652                 if (! chdir($subdir)) {
1653                         print STDERR "mr register: failed to chdir to $subdir: $!\n";
1654                 }
1655         }
1656
1657         $ENV{MR_REPO}=getcwd();
1658         my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1659         if (! defined $command) {
1660                 die "mr register: unknown repository type\n";
1661         }
1662
1663         $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1664         $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1665                 "my_action(){ $command\n }; my_action ".
1666                 join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1667         print "mr register: running >>$command<<\n" if $verbose;
1668         exec($command) || die "exec: $!";
1669 }
1670
1671 sub bootstrap {
1672         eval q{use File::Copy};
1673         die $@ if $@;
1674
1675         my $url=shift @ARGV;
1676         my $dir=shift @ARGV || ".";
1677         
1678         if (! defined $url || ! length $url) {
1679                 die "mr: bootstrap requires url\n";
1680         }
1681
1682         # Retrieve config file.
1683         eval q{use File::Temp};
1684         die $@ if $@;
1685         my $tmpconfig=File::Temp->new();
1686         if ($url =~ m!^[\w\d]+://!) {
1687                 # Download the config file to a temporary location.
1688                 my @downloader;
1689                 if ($url =~ m!^ssh://(.*)!) {
1690                         @downloader = ("scp", $1, $tmpconfig);
1691                 }
1692                 else {
1693                         @downloader = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
1694                         push(@downloader, "-k") if $insecure;
1695                 }
1696                 my $status = system(@downloader);
1697                 die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n"
1698                         if $downloader[0] eq 'curl' && $status >> 8 == 60;
1699                 die "mr bootstrap: download of $url failed\n" if $status != 0;
1700         }
1701         else {
1702                 # Config file is local.
1703                 die "mr bootstrap: cannot read file '$url'"
1704                         unless -r $url;
1705                 copy($url, $tmpconfig) || die "copy: $!";
1706         }
1707
1708         # Sanity check on destination directory.
1709         if (! -e $dir) {
1710                 system("mkdir", "-p", $dir);
1711         }
1712         chdir($dir) || die "chdir $dir: $!";
1713
1714         # Special case to handle checkout of the "." repo, which 
1715         # would normally be skipped.
1716         my $topdir=abs_path(".")."/";
1717         my @repo=($topdir, $topdir, ".");
1718         loadconfig($tmpconfig, $topdir, $url);
1719         record(\@repo, action("checkout", @repo, 1))
1720                 if exists $config{$topdir}{"."}{"checkout"};
1721
1722         if (-e ".mrconfig") {
1723                 print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n";
1724         }
1725         else {
1726                 move($tmpconfig, ".mrconfig") || die "rename: $!";
1727         }
1728
1729         # Reload the config file (in case we got a different version)
1730         # and checkout everything else.
1731         startingconfig();
1732         loadconfig(".mrconfig");
1733         dispatch("checkout");
1734         @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped;
1735         showstats("bootstrap");
1736         exitstats();
1737 }
1738
1739 # alias expansion and command stemming
1740 sub expandaction {
1741         my $action=shift;
1742         if (exists $alias{$action}) {
1743                 $action=$alias{$action};
1744         }
1745         if (! exists $knownactions{$action}) {
1746                 my @matches = grep { /^\Q$action\E/ }
1747                         keys %knownactions, keys %alias;
1748                 if (@matches == 1) {
1749                         $action=$matches[0];
1750                 }
1751                 elsif (@matches == 0) {
1752                         die "mr: unknown action \"$action\" (known actions: ".
1753                                 join(", ", sort keys %knownactions).")\n";
1754                 }
1755                 else {
1756                         die "mr: ambiguous action \"$action\" (matches: ".
1757                                 join(", ", @matches).")\n";
1758                 }
1759         }
1760         return $action;
1761 }
1762
1763 sub find_mrconfig {
1764         my $dir=getcwd();
1765         while (length $dir) {
1766                 if (-e "$dir/.mrconfig") {
1767                         return "$dir/.mrconfig";
1768                 }
1769                 $dir=~s/\/[^\/]*$//;
1770         }
1771         return $HOME_MR_CONFIG;
1772 }
1773
1774 sub getopts {
1775         my @saved=@ARGV;
1776         Getopt::Long::Configure("bundling", "no_permute");
1777         my $result=GetOptions(
1778                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1779                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1780                 "p|path" => sub { }, # now default, ignore
1781                 "f|force" => \$force,
1782                 "v|verbose" => \$verbose,
1783                 "q|quiet" => \$quiet,
1784                 "s|stats" => \$stats,
1785                 "k|insecure" => \$insecure,
1786                 "i|interactive" => \$interactive,
1787                 "n|no-recurse:i" => \$max_depth,
1788                 "j|jobs:i" => \$jobs,
1789                 "t|trust-all" => \$trust_all,
1790         );
1791         if (! $result || @ARGV < 1) {
1792                 die("Usage: mr [options] action [params ...]\n".
1793                     "(Use mr help for man page.)\n");
1794         }
1795         
1796         $ENV{MR_SWITCHES}="";
1797         foreach my $option (@saved) {
1798                 last if $option eq $ARGV[0];
1799                 $ENV{MR_SWITCHES}.="$option ";
1800         }
1801 }
1802
1803 sub init {
1804         $SIG{INT}=sub {
1805                 print STDERR "mr: interrupted\n";
1806                 exit 2;
1807         };
1808         
1809         # This can happen if it's run in a directory that was removed
1810         # or other strangeness.
1811         if (! defined $directory) {
1812                 die("mr: failed to determine working directory\n");
1813         }
1814         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1815         # the config file might be a symlink to elsewhere, and the directory it's
1816         # in is significant.
1817         if ($ENV{MR_CONFIG} !~ /^\//) {
1818                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1819         }
1820         # Try to set MR_PATH to the path to the program.
1821         eval {
1822                 use FindBin qw($Bin $Script);
1823                 $ENV{MR_PATH}=$Bin."/".$Script;
1824         };
1825 }
1826         
1827 sub exitstats {
1828         if (@failed) {
1829                 exit 1;
1830         }
1831         else {
1832                 exit 0;
1833         }
1834 }
1835
1836 sub main {
1837         getopts();
1838         init();
1839
1840         startingconfig();
1841         loadconfig($HOME_MR_CONFIG);
1842         loadconfig($ENV{MR_CONFIG});
1843         #use Data::Dumper; print Dumper(\%config);
1844         
1845         my $action=expandaction(shift @ARGV);
1846         dispatch($action);
1847
1848         showstats($action);
1849         exitstats();
1850 }
1851
1852 # Finally, some useful actions that mr knows about by default.
1853 # These can be overridden in ~/.mrconfig.
1854 __DATA__
1855 [ALIAS]
1856 co = checkout
1857 ci = commit
1858 ls = list
1859
1860 [DEFAULT]
1861 order = 10
1862 lib =
1863         error() {
1864                 echo "mr: $@" >&2
1865                 exit 1
1866         }
1867         warning() {
1868                 echo "mr (warning): $@" >&2
1869         }
1870         info() {
1871                 echo "mr: $@" >&2
1872         }
1873         hours_since() {
1874                 if [ -z "$1" ] || [ -z "$2" ]; then
1875                         error "mr: usage: hours_since action num"
1876                 fi
1877                 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
1878                         if [ -e "$MR_REPO/$dir" ]; then
1879                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1880                                 break
1881                         fi
1882                 done
1883                 if [ -z "$flagfile" ]; then
1884                         error "cannot determine flag filename"
1885                 fi
1886                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1887                 if [ "$delta" -lt "$2" ]; then
1888                         return 1
1889                 else
1890                         touch "$flagfile"
1891                         return 0
1892                 fi
1893         }
1894         is_bzr_checkout() {
1895                 LANG=C bzr info | egrep -q '^Checkout'
1896         }
1897         lazy() {
1898                 if [ -d "$MR_REPO" ]; then
1899                         return 1
1900                 else
1901                         return 0
1902                 fi
1903         }
1904
1905 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
1906 git_test = perl: -e "$ENV{MR_REPO}/.git"
1907 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
1908 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
1909 hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
1910 darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
1911 fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
1912 git_bare_test = perl: 
1913         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
1914         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
1915         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
1916 vcsh_test = perl:
1917         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
1918         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
1919         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
1920 veracity_test  = perl: -d "$ENV{MR_REPO}/.sgdrawer"
1921
1922 svn_update = svn update "$@"
1923 git_update = git pull "$@"
1924 bzr_update = 
1925         if is_bzr_checkout; then
1926                 bzr update "$@"
1927         else
1928                 bzr merge --pull "$@"
1929         fi
1930 cvs_update = cvs -q update "$@"
1931 hg_update  = hg pull "$@"; hg update "$@"
1932 darcs_update = darcs pull -a "$@"
1933 fossil_update = fossil pull "$@"
1934 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
1935 veracity_update = vv pull "$@" && vv update "$@"
1936
1937 git_fetch = git fetch --all --prune --tags
1938 git_svn_fetch = git svn fetch
1939 darcs_fetch = darcs fetch
1940 hg_fetch = hg pull
1941
1942 svn_status = svn status "$@"
1943 git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
1944 bzr_status = bzr status --short "$@"; bzr missing
1945 cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
1946 hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
1947 darcs_status = darcs whatsnew -ls "$@" || true
1948 fossil_status = fossil changes "$@"
1949 vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
1950 veracity_status = vv status "$@"
1951
1952 svn_commit = svn commit "$@"
1953 git_commit = git commit -a "$@" && git push --all
1954 bzr_commit = 
1955         if is_bzr_checkout; then
1956                 bzr commit "$@"
1957         else
1958                 bzr commit "$@" && bzr push
1959         fi
1960 cvs_commit = cvs commit "$@"
1961 hg_commit  = hg commit "$@" && hg push
1962 darcs_commit = darcs record -a "$@" && darcs push -a
1963 fossil_commit = fossil commit "$@"
1964 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
1965 veracity_commit = vv commit "$@" && vv push
1966
1967 git_record = git commit -a "$@"
1968 bzr_record =
1969         if is_bzr_checkout; then
1970                 bzr commit --local "$@"
1971         else
1972                 bzr commit "$@"
1973         fi
1974 hg_record  = hg commit "$@"
1975 darcs_record = darcs record -a "$@"
1976 fossil_record = fossil commit "$@"
1977 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
1978 veracity_record = vv commit "$@"
1979
1980 svn_push = :
1981 git_push = git push "$@"
1982 bzr_push = bzr push "$@"
1983 cvs_push = :
1984 hg_push = hg push "$@"
1985 darcs_push = darcs push -a "$@"
1986 fossil_push = fossil push "$@"
1987 vcsh_push = vcsh run "$MR_REPO" git push "$@"
1988 veracity_push = vv push "$@"
1989
1990 svn_diff = svn diff "$@"
1991 git_diff = git diff "$@"
1992 bzr_diff = bzr diff "$@"
1993 cvs_diff = cvs -q diff "$@"
1994 hg_diff  = hg diff "$@"
1995 darcs_diff = darcs diff -u "$@"
1996 fossil_diff = fossil diff "$@"
1997 vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
1998 veracity_diff = vv diff "$@"
1999
2000 svn_log = svn log "$@"
2001 git_log = git log "$@"
2002 bzr_log = bzr log "$@"
2003 cvs_log = cvs log "$@"
2004 hg_log  = hg log "$@"
2005 darcs_log = darcs changes "$@"
2006 git_bare_log = git log "$@"
2007 fossil_log = fossil timeline "$@"
2008 vcsh_log = vcsh run "$MR_REPO" git log "$@"
2009 veracity_log = vv log "$@"
2010
2011 hg_grep = hg grep "$@"
2012 cvs_grep = ack-grep "$@"
2013 svn_grep = ack-grep "$@"
2014 git_svn_grep = git grep "$@"
2015 git_grep = git grep "$@"
2016 bzr_grep = ack-grep "$@"
2017
2018 run = "$@"
2019
2020 svn_register =
2021         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
2022         if [ -z "$url" ]; then
2023                 error "cannot determine svn url"
2024         fi
2025         echo "Registering svn url: $url in $MR_CONFIG"
2026         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
2027 git_register = 
2028         url="`LC_ALL=C git config --get remote.origin.url`" || true
2029         if [ -z "$url" ]; then
2030                 error "cannot determine git url"
2031         fi
2032         echo "Registering git url: $url in $MR_CONFIG"
2033         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
2034 bzr_register =
2035         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
2036         if [ -z "$url" ]; then
2037                 error "cannot determine bzr url"
2038         fi
2039         echo "Registering bzr url: $url in $MR_CONFIG"
2040         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
2041 cvs_register =
2042         repo=`cat CVS/Repository`
2043         root=`cat CVS/Root`
2044         if [ -z "$root" ]; then
2045                 error "cannot determine cvs root"
2046                 fi
2047         echo "Registering cvs repository $repo at root $root"
2048         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
2049 hg_register = 
2050         url=`hg showconfig paths.default`
2051         echo "Registering mercurial repo url: $url in $MR_CONFIG"
2052         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
2053 darcs_register = 
2054         url=`cat _darcs/prefs/defaultrepo`
2055         echo "Registering darcs repository $url in $MR_CONFIG"
2056         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
2057 git_bare_register = 
2058         url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
2059         if [ -z "$url" ]; then
2060                 error "cannot determine git url"
2061         fi
2062         echo "Registering git url: $url in $MR_CONFIG"
2063         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
2064 vcsh_register =
2065         url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
2066         if [ -z "$url" ]; then
2067                 error "cannot determine git url"
2068         fi
2069         echo "Registering git url: $url in $MR_CONFIG"
2070         mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$MR_REPO'"
2071 fossil_register =
2072         url=`fossil remote-url`
2073         repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
2074         echo "Registering fossil repository $url in $MR_CONFIG"
2075         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
2076 veracity_register =
2077         url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
2078         repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
2079         echo "Registering veracity repository $url in $MR_CONFIG"
2080         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
2081
2082 svn_trusted_checkout = svn co $url $repo
2083 svn_alt_trusted_checkout = svn checkout $url $repo
2084 git_trusted_checkout = git clone $url $repo
2085 bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
2086 # cvs: too hard
2087 hg_trusted_checkout = hg clone $url $repo
2088 darcs_trusted_checkout = darcs get $url $repo
2089 git_bare_trusted_checkout = git clone --bare $url $repo
2090 vcsh_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
2091 # fossil: messy to do
2092 veracity_trusted_checkout = vv clone $url $repo
2093
2094
2095 help =
2096         case `uname -s` in
2097                 SunOS)
2098                 SHOWMANFILE="man -f"
2099                 ;;
2100                 Darwin)
2101                 SHOWMANFILE="man"
2102                 ;;
2103                 *)
2104                 SHOWMANFILE="man"
2105                 ;;
2106         esac
2107         if [ ! -e "$MR_PATH" ]; then
2108                 error "cannot find program path"
2109         fi
2110         tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
2111         trap "rm -f $tmp" exit
2112         pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
2113         $SHOWMANFILE "$tmp" || error "man failed"
2114 list = true
2115 config = 
2116 bootstrap = 
2117
2118 online =
2119         if [ -s ~/.mrlog ]; then
2120                 info "running offline commands"
2121                 mv -f ~/.mrlog ~/.mrlog.old
2122                 if ! sh -e ~/.mrlog.old; then
2123                         error "offline command failed; left in ~/.mrlog.old"
2124                 fi
2125                 rm -f ~/.mrlog.old
2126         else
2127                 info "no offline commands to run"
2128         fi
2129 offline =
2130         umask 077
2131         touch ~/.mrlog
2132         info "offline mode enabled"
2133 remember =
2134         info "remembering command: 'mr $@'"
2135         command="mr -d '$(pwd)' $MR_SWITCHES"
2136         for w in "$@"; do
2137                 command="$command '$w'"
2138         done
2139         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
2140                 echo "$command" >> ~/.mrlog
2141         fi
2142
2143 ed = echo "A horse is a horse, of course, of course.."
2144 T = echo "I pity the fool."
2145 right = echo "Not found."
2146
2147 # vim:sw=8:sts=0:ts=8:noet
2148 # Local variables:
2149 # indent-tabs-mode: t
2150 # cperl-indent-level: 8
2151 # End: