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

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