]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

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