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

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