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

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