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