]> 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:

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