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

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