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

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